using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
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;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
namespace FinlyticAnalyzer.Util;
///
/// Unified Managed MQTT Client for FinlyticAnalyzer.
/// Handles event subscriptions, market screening, manual AI evaluation triggers,
/// and dispatches trade proposals via MQTT.
///
public class AnalyzerMqttClient : ManagedMqttClient, IHostedService
{
private readonly IConfiguration _configuration;
private readonly IServiceScopeFactory _scopeFactory;
private readonly IVixTrackerService _vixTracker;
private readonly IThreeLayerFilterEngine _filterEngine;
private readonly IWinRateCalculator _winRateCalculator;
private readonly IN8nEvaluationService _n8nService;
private readonly ILogger _logger;
public AnalyzerMqttClient(
IConfiguration configuration,
IServiceScopeFactory scopeFactory,
IVixTrackerService vixTracker,
IThreeLayerFilterEngine filterEngine,
IWinRateCalculator winRateCalculator,
IN8nEvaluationService n8nService,
ILogger logger) : base(logger)
{
_configuration = configuration;
_scopeFactory = scopeFactory;
_vixTracker = vixTracker;
_filterEngine = filterEngine;
_winRateCalculator = winRateCalculator;
_n8nService = n8nService;
_logger = logger;
}
public async Task StartAsync(CancellationToken cancellationToken)
{
var config = new MqttConfiguration
{
Host = _configuration["MQTT:Host"] ?? _configuration["MQTT__Host"] ?? "localhost",
Port = Convert.ToInt32(_configuration["MQTT:Port"] ?? _configuration["MQTT__Port"] ?? "1883"),
Username = _configuration["MQTT:Username"] ?? _configuration["MQTT__Username"],
Password = _configuration["MQTT:Password"] ?? _configuration["MQTT__Password"],
ClientId = $"{(_configuration["MQTT:ClientId"] ?? _configuration["MQTT__ClientId"] ?? "finlytic_analyzer")}_{Guid.NewGuid():N}"
};
_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("Stopping Unified Analyzer MQTT Client.");
await DisconnectAsync();
}
protected override async Task OnConnectedAsync()
{
_logger.LogInformation("Analyzer MQTT Client connected. Subscribing to topics and RPC response channels...");
// Incoming Event Topics
await SubscribeAsync("services/news/#");
await SubscribeAsync("finlytic/news/raw/#");
await SubscribeAsync("finlytic/market/ticks/#");
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/#");
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)
{
try
{
if (topic.Contains("health_Ping", StringComparison.OrdinalIgnoreCase))
{
var segments = topic.Split('/');
bool isForMe = segments.Length >= 5
? segments[3].Equals("FinlyticAnalyzer", StringComparison.OrdinalIgnoreCase)
: topic.Contains("FinlyticAnalyzer", StringComparison.OrdinalIgnoreCase);
if (isForMe)
{
var correlationId = segments[^1];
string respTopic = $"services/response/health_Ping/{correlationId}";
var healthResp = new ServiceHealthResponse("FinlyticAnalyzer", "Online", DateTime.UtcNow, "Connected");
await PublishAsync(respTopic, healthResp);
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;
}
if (topic.StartsWith("services/config/updated", StringComparison.OrdinalIgnoreCase))
{
if (topic.EndsWith("FinlyticAnalyzer", StringComparison.OrdinalIgnoreCase))
{
try
{
var configUpdate = JsonSerializer.Deserialize(payloadStr, FinlyticJsonSerializerContext.Default.ServiceConfigUpdatePayload);
if (configUpdate?.Settings != null && configUpdate.Settings.Count > 0)
{
using var scope = _scopeFactory.CreateScope();
var settings = scope.ServiceProvider.GetRequiredService();
var dict = configUpdate.Settings.ToDictionary(k => k.Key, v => (object?)v.Value);
await settings.UpdateSettingsAsync(dict);
}
}
catch (Exception ex)
{
using var scope = _scopeFactory.CreateScope();
var finlyticLogger = scope.ServiceProvider.GetRequiredService>();
await finlyticLogger.LogErrorAsync(SettingKeys.AnalyzerChannel, ex, "[AnalyzerMqttClient] Error processing MQTT config update event.");
}
}
return;
}
if (topic.StartsWith("finlytic/market/ticks/"))
{
ProcessTickMessage(topic, payloadStr);
}
else if (topic.StartsWith("finlytic/news/raw/", StringComparison.OrdinalIgnoreCase) ||
topic.StartsWith("services/news/", StringComparison.OrdinalIgnoreCase))
{
await ProcessNewsMessageAsync(payloadStr, CancellationToken.None);
}
else if (topic.StartsWith("services/request/analyzer_TriggerManual/"))
{
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);
}
}
catch (Exception ex)
{
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 };
var closedDto = JsonSerializer.Deserialize(payloadStr, options);
if (closedDto != null && !string.IsNullOrWhiteSpace(closedDto.TradeId))
{
bool isWin = closedDto.Status?.Contains("Profit", StringComparison.OrdinalIgnoreCase) == true ||
closedDto.Status?.Contains("Win", StringComparison.OrdinalIgnoreCase) == true;
var feedback = new TradeFeedbackRecord
{
TradeId = closedDto.TradeId,
AnalysisId = closedDto.AnalysisId,
Sector = closedDto.Sector,
Symbol = closedDto.Symbol,
Isin = closedDto.Isin,
EntryPrice = closedDto.EntryPrice,
StopLoss = closedDto.StopLoss,
TakeProfit = closedDto.TakeProfit,
IsWin = isWin,
VixRegime = closedDto.VixRegime,
VixValue = closedDto.VixValue,
CreatedAt = closedDto.CreatedAt,
ClosedAt = DateTime.UtcNow
};
string feedbackDir = System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "data", "feedback");
if (!System.IO.Directory.Exists(feedbackDir))
{
System.IO.Directory.CreateDirectory(feedbackDir);
}
string filePath = System.IO.Path.Combine(feedbackDir, $"{closedDto.TradeId}.json");
await System.IO.File.WriteAllTextAsync(filePath, JsonSerializer.Serialize(new[] { feedback }, options));
await finlyticLogger.LogInfoAsync(SettingKeys.AnalyzerChannel, "[AnalyzerMqttClient] Processed closed trade feedback for {TradeId}. Saved to {FilePath}", closedDto.TradeId, filePath);
}
}
catch (Exception ex)
{
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))
{
await finlyticLogger.LogWarningAsync(SettingKeys.AnalyzerChannel, "[AnalyzerMqttClient] Manual trigger received without valid request or ISIN.");
return;
}
await finlyticLogger.LogInfoAsync(SettingKeys.AnalyzerChannel, "[ManualAnalyzer] [TRIGGERED] Processing rich manual trigger for ISIN '{Isin}' (Symbol: {Symbol}). CorrelationId: {CorrelationId}", manualReq.Isin, manualReq.Symbol, correlationId);
var dbContext = scope.ServiceProvider.GetRequiredService();
var regime = _vixTracker.GetCurrentRegime();
var currentVix = _vixTracker.GetCurrentVix();
string analysisId = Guid.NewGuid().ToString("N");
double winRate = _winRateCalculator.CalculateWinRate(manualReq.Sector, manualReq.Symbol, regime);
string riskLabel = manualReq.RiskScore > 70 ? $"Aggressiv ({manualReq.RiskScore}/100)" : (manualReq.RiskScore > 30 ? $"Balanced ({manualReq.RiskScore}/100)" : $"Konservativ ({manualReq.RiskScore}/100)");
string timeframeFormatted = $"{manualReq.MinTimeframeValue}-{manualReq.MaxTimeframeValue} {manualReq.TimeframeUnit}";
var n8nRequest = new N8nAnalysisRequestDto
{
RequestId = analysisId,
Timestamp = DateTime.UtcNow,
TriggerType = "Manual",
TargetAsset = new TargetAssetInfo
{
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
},
MarketContext = new MarketContextInfo
{
Vix = currentVix,
MarketRegime = regime.ToString()
},
FilterContext = new FilterContextInfo
{
ImpactScore = 1.0,
RawNewsHeadline = string.IsNullOrWhiteSpace(manualReq.Headline) ? "Manual User Trigger" : manualReq.Headline
},
UserPreferences = new UserPreferencesInfo
{
RiskScore = manualReq.RiskScore,
RiskTolerance = riskLabel,
MinTimeframeValue = manualReq.MinTimeframeValue,
MaxTimeframeValue = manualReq.MaxTimeframeValue,
TimeframeUnit = manualReq.TimeframeUnit,
TimeframeFormatted = timeframeFormatted,
InstrumentType = manualReq.InstrumentType,
UserNotes = manualReq.UserNotes
},
TradeFeedback = new TradeFeedbackInfo
{
TotalAssetTrades = 0,
AssetWinRate = winRate,
AvgReturnPercent = 0.0,
LastTradeResult = "UNKNOWN"
},
TechnicalContext = new TechnicalContextInfo
{
Rsi = manualReq.TaData?.Indicators?.LastOrDefault()?.Rsi14?.ToString("F1") ?? "N/A",
SupertrendStatus = manualReq.TaData?.Indicators?.LastOrDefault()?.SupertrendDirection ?? "NEUTRAL",
Atr = manualReq.TaData?.Indicators?.LastOrDefault()?.Atr14?.ToString("F2") ?? "N/A",
Sma50 = (double?)manualReq.TaData?.Indicators?.LastOrDefault()?.Sma50,
Sma200 = (double?)manualReq.TaData?.Indicators?.LastOrDefault()?.Sma200,
DetectedPatterns = manualReq.TaData?.Patterns?.Select(p => new PatternContextInfo
{
PatternName = p.Type,
BreakoutDirection = p.BreakoutSignal?.Direction,
TargetPrice = (double?)p.BreakoutSignal?.TargetPrice,
PotentialPercent = (double?)p.BreakoutSignal?.PotentialPercent
}).ToList() ?? new List()
},
SentimentContext = new SentimentContextInfo
{
AssetSentimentScore = manualReq.SentimentData?.CurrentSummary?.CompoundScore ?? 0.0,
SectorSentimentScore = 0.0,
NewsSentimentSummary = manualReq.SentimentData?.CurrentSummary?.SentimentLabel ?? "Neutral"
},
FundamentalContext = new FundamentalContextInfo
{
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
}
};
var n8nResponse = await _n8nService.EvaluateAssetAsync(n8nRequest, cancellationToken);
var settingsService = scope.ServiceProvider.GetRequiredService();
double minSignalScore = await settingsService.GetSettingAsync(SettingKeys.MinWinRateThreshold, cancellationToken);
double dynamicWinRate = _winRateCalculator.CalculateDynamicWinRate(
manualReq.Sector,
manualReq.Symbol,
regime,
n8nEvalScore: n8nResponse?.EvalScore,
sentimentScore: manualReq.SentimentData?.CurrentSummary?.CompoundScore,
signalType: n8nResponse?.SuggestedDirection ?? "BUY");
double confidenceScore = n8nResponse?.EvalScore > 0 ? n8nResponse.EvalScore : (dynamicWinRate / 100.0);
bool shouldProceed = n8nResponse != null &&
string.Equals(n8nResponse.AiDecision, "Proceed", StringComparison.OrdinalIgnoreCase) &&
(confidenceScore * 100.0) >= minSignalScore &&
dynamicWinRate >= minSignalScore;
TradeProposalDto? proposalDto = null;
if (n8nResponse != null)
{
proposalDto = new TradeProposalDto
{
TradeId = "PROP-" + Guid.NewGuid().ToString("N")[..10].ToUpperInvariant(),
AnalysisId = analysisId,
EventId = analysisId,
Sector = manualReq.Sector,
Symbol = manualReq.Symbol.ToUpperInvariant(),
Isin = manualReq.Isin.ToUpperInvariant(),
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",
RiskTolerance = n8nResponse.SuggestedRisk,
Timeframe = timeframeFormatted,
InstrumentType = manualReq.InstrumentType,
WinRate = dynamicWinRate,
VixRegime = regime,
VixValue = currentVix,
TtlMinutes = 60,
Reasoning = $"Manual n8n Evaluation ({n8nResponse.AiDecision}): {n8nResponse.AiReasoning}",
StopLoss = n8nResponse.ExecutionPlan?.StopLoss ?? 0,
TakeProfit = n8nResponse.ExecutionPlan?.TakeProfitTargets != null && n8nResponse.ExecutionPlan.TakeProfitTargets.Count > 0 ? n8nResponse.ExecutionPlan.TakeProfitTargets[0] : 0,
EntryZoneMin = n8nResponse.ExecutionPlan?.EntryZone?.Min,
EntryZoneMax = n8nResponse.ExecutionPlan?.EntryZone?.Max,
TakeProfitTargets = n8nResponse.ExecutionPlan?.TakeProfitTargets,
RiskRewardRatio = n8nResponse.ExecutionPlan?.RiskRewardRatio,
MaxLeverage = n8nResponse.ExecutionPlan?.MaxLeverage,
TechnicalRationale = n8nResponse.DetailedAnalysis?.TechnicalRationale ?? string.Empty,
FundamentalRationale = n8nResponse.DetailedAnalysis?.FundamentalRationale ?? string.Empty,
RiskWarning = n8nResponse.DetailedAnalysis?.RiskWarning ?? string.Empty,
CreatedAt = DateTime.UtcNow
};
}
var analysisEntity = new AnalysisEntity
{
AnalysisId = analysisId,
EventId = analysisId,
Sector = manualReq.Sector,
Symbol = manualReq.Symbol.ToUpperInvariant(),
Isin = manualReq.Isin.ToUpperInvariant(),
VixRegime = regime,
VixValue = currentVix,
ImpactScore = 1.0,
WinRate = dynamicWinRate,
RawDataJson = JsonSerializer.Serialize(manualReq),
AiOutputJson = proposalDto != null ? JsonSerializer.Serialize(proposalDto) : "{}",
N8nResponseJson = n8nResponse != null ? JsonSerializer.Serialize(n8nResponse) : "{}",
N8nEvalScore = n8nResponse?.EvalScore ?? 0,
N8nDecision = n8nResponse?.AiDecision ?? "Rejected",
IsTradeProposed = shouldProceed,
CreatedAt = DateTime.UtcNow
};
dbContext.Analyses.Add(analysisEntity);
await dbContext.SaveChangesAsync(cancellationToken);
var responseTopic = $"services/response/analyzer_TriggerManual/{correlationId}";
var responsePayload = new ManualAnalysisResponseDto
{
AnalysisId = analysisId,
IsTradeProposed = shouldProceed,
Status = shouldProceed ? "Success" : "Rejected",
Recommendation = shouldProceed ? "RECOMMENDED" : "NOT_RECOMMENDED",
N8nResponse = n8nResponse,
Proposal = proposalDto
};
await PublishAsync(responseTopic, responsePayload);
if (proposalDto != null && shouldProceed)
{
string propTopic = $"finlytic/trades/proposed/{(string.IsNullOrWhiteSpace(manualReq.Sector) ? "general" : manualReq.Sector.ToLowerInvariant())}/{manualReq.Symbol.ToLowerInvariant()}";
await PublishAsync(propTopic, proposalDto);
await finlyticLogger.LogInfoAsync(SettingKeys.AnalyzerChannel, "[ManualAnalyzer] [DISPATCHED] Dispatched Manual Trade Proposal {AnalysisId} to topic {Topic}", analysisId, propTopic);
}
}
catch (Exception ex)
{
await finlyticLogger.LogErrorAsync(SettingKeys.AnalyzerChannel, ex, "[AnalyzerMqttClient] Failed to handle manual trigger for correlation {CorrelationId}.", correlationId);
try
{
var errorResponse = new ManualAnalysisResponseDto
{
Status = "ERROR",
Message = $"Analysis failed: {ex.Message}"
};
await PublishAsync($"services/response/analyzer_TriggerManual/{correlationId}", errorResponse);
}
catch (Exception pubEx)
{
await finlyticLogger.LogErrorAsync(SettingKeys.AnalyzerChannel, pubEx, "[AnalyzerMqttClient] Failed to publish error response for correlation {CorrelationId}.", correlationId);
}
}
}
private void ProcessTickMessage(string topic, string payloadStr)
{
if (topic.EndsWith("VIX", StringComparison.OrdinalIgnoreCase) || topic.EndsWith("^VIX", StringComparison.OrdinalIgnoreCase))
{
try
{
var tick = JsonSerializer.Deserialize(payloadStr, FinlyticJsonSerializerContext.Default.TickMessageDto);
if (tick != null && tick.Price > 0)
{
_vixTracker.UpdateVixFromTick(tick.Price);
}
}
catch (Exception ex)
{
_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;
var regime = _vixTracker.GetCurrentRegime();
var currentVix = _vixTracker.GetCurrentVix();
var filterResult = _filterEngine.EvaluateNews(newsArticle, regime);
if (!filterResult.Passed)
{
await finlyticLogger.LogInfoAsync(SettingKeys.AnalyzerChannel, "[AutoScreener] [SKIPPED] News message skipped for ISIN '{Isin}'. Reason: {Reason}", filterResult.Isin, filterResult.RejectReason);
return;
}
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;
string rawHeadline = newsArticle.Title ?? string.Empty;
double winRate = _winRateCalculator.CalculateWinRate(filterResult.Sector, filterResult.Symbol, regime);
int riskScore = 50;
string riskTolerance = "Balanced (50/100)";
int minTf = 4;
int maxTf = 7;
if (winRate < 45.0)
{
riskScore = 30;
riskTolerance = "Konservativ (30/100)";
minTf = 7;
maxTf = 14;
}
else if (winRate >= 65.0)
{
riskScore = 75;
riskTolerance = "Aggressiv (75/100)";
minTf = 1;
maxTf = 4;
}
TechnicalContextInfo taInfo = new();
FundamentalContextInfo fundInfo = new();
SentimentContextInfo sentInfo = new();
string resolvedSymbol = filterResult.Symbol;
string resolvedName = filterResult.Symbol;
if (newsArticle.MatchedAssets != null && newsArticle.MatchedAssets.Count > 0)
{
var firstAsset = newsArticle.MatchedAssets[0];
if (!string.IsNullOrWhiteSpace(firstAsset.Name))
{
resolvedName = firstAsset.Name;
if (resolvedSymbol == "UNKNOWN" || resolvedSymbol == filterResult.Isin)
{
resolvedSymbol = resolvedName;
}
}
}
FinlyticCore.Dtos.TechnicalAnalysis.TechnicalAnalysisDto? taResp = null;
FinlyticCore.Dtos.Fundamentals.AssetFundamentalsDto? fundResp = null;
FinlyticCore.Dtos.TechnicalAnalysis.LivePriceDto? livePriceResp = null;
FinlyticCore.Dtos.Sentiment.IsinSentimentSummaryDto? sentResp = null;
try
{
if (IsConnected)
{
var isinReq = new IsinRequest(filterResult.Isin);
var livePriceTask = SendRpcRequestAsync(
"tr_GetLivePrice", isinReq, TimeSpan.FromSeconds(5));
var taTask = SendRpcRequestAsync(
"ta_GetAnalysis", isinReq, TimeSpan.FromSeconds(5));
var fundTask = SendRpcRequestAsync(
"fundamentals_Get", isinReq, TimeSpan.FromSeconds(5));
var sentTask = SendRpcRequestAsync(
"sentiment_GetIsin", isinReq, TimeSpan.FromSeconds(5));
await Task.WhenAll(livePriceTask, taTask, fundTask, sentTask);
livePriceResp = livePriceTask.Result;
taResp = taTask.Result;
fundResp = fundTask.Result;
sentResp = sentTask.Result;
if (taResp?.Indicators != null)
{
var latestIndicator = taResp.Indicators.LastOrDefault();
taInfo = new TechnicalContextInfo
{
Rsi = latestIndicator?.Rsi14?.ToString("F1") ?? "50.0",
SupertrendStatus = latestIndicator?.SupertrendDirection ?? "NEUTRAL",
Atr = latestIndicator?.Atr14?.ToString("F2") ?? "0.0",
Sma50 = (double?)latestIndicator?.Sma50,
Sma200 = (double?)latestIndicator?.Sma200,
DetectedPatterns = taResp.Patterns?.Select(p => new PatternContextInfo
{
PatternName = p.Type,
BreakoutDirection = p.BreakoutSignal?.Direction,
TargetPrice = (double?)p.BreakoutSignal?.TargetPrice,
PotentialPercent = (double?)p.BreakoutSignal?.PotentialPercent
}).ToList() ?? new List()
};
}
if (fundResp != null)
{
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.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;
double normalizedScore = Math.Clamp((compound + 1.0) / 2.0, 0.0, 1.0);
sentInfo = new SentimentContextInfo
{
AssetSentimentScore = Math.Round(normalizedScore, 2),
SectorSentimentScore = Math.Round(normalizedScore, 2),
NewsSentimentSummary = string.IsNullOrWhiteSpace(sentResp.CurrentSummary?.SentimentLabel) ? "Neutral" : sentResp.CurrentSummary.SentimentLabel
};
}
}
}
catch (Exception ex)
{
await finlyticLogger.LogWarningAsync(SettingKeys.AnalyzerChannel, ex, "[AnalyzerMqttClient] Failed to fetch context data for auto screener analysis.");
}
var n8nRequest = new N8nAnalysisRequestDto
{
RequestId = analysisId,
Timestamp = DateTime.UtcNow,
TriggerType = "AutoScreener",
TargetAsset = new TargetAssetInfo
{
Symbol = resolvedSymbol.ToUpperInvariant(),
Name = resolvedName,
Isin = filterResult.Isin.ToUpperInvariant(),
Sector = filterResult.Sector
},
MarketContext = new MarketContextInfo
{
Vix = currentVix,
MarketRegime = regime.ToString()
},
FilterContext = new FilterContextInfo
{
ImpactScore = filterResult.ImpactScore,
RawNewsHeadline = rawHeadline
},
UserPreferences = new UserPreferencesInfo
{
RiskScore = riskScore,
RiskTolerance = riskTolerance,
MinTimeframeValue = minTf,
MaxTimeframeValue = maxTf,
TimeframeUnit = "Tage",
TimeframeFormatted = $"{minTf}-{maxTf} Tage",
InstrumentType = "KnockOut",
UserNotes = "High-Conviction Screener Mode: Evaluate underlying data for strong reliable chart moves."
},
TradeFeedback = new TradeFeedbackInfo
{
TotalAssetTrades = 0,
AssetWinRate = winRate,
AvgReturnPercent = 0.0,
LastTradeResult = "UNKNOWN"
},
TechnicalContext = taInfo,
SentimentContext = sentInfo,
FundamentalContext = fundInfo
};
var n8nResponse = await _n8nService.EvaluateAssetAsync(n8nRequest, cancellationToken);
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 &&
string.Equals(n8nResponse.AiDecision, "Proceed", StringComparison.OrdinalIgnoreCase) &&
(confidenceScore * 100.0) >= minSignalScore &&
winRate >= minSignalScore;
string finalSymbol = !string.IsNullOrWhiteSpace(resolvedSymbol) && resolvedSymbol != "UNKNOWN"
? resolvedSymbol
: (!string.IsNullOrWhiteSpace(filterResult.Symbol) && filterResult.Symbol != "UNKNOWN" ? filterResult.Symbol : filterResult.Isin);
string finalName = !string.IsNullOrWhiteSpace(resolvedName) && resolvedName != "UNKNOWN"
? resolvedName
: finalSymbol;
string marketRegion = filterResult.Isin.StartsWith("DE", StringComparison.OrdinalIgnoreCase) ? "GERMAN_EQUITIES" : "US_EQUITIES";
var supportLevels = new List();
var resistanceLevels = new List();
double currentPrice = (double)(livePriceResp?.CurrentPrice > 0 ? livePriceResp.CurrentPrice : 0.0m);
if (currentPrice > 0)
{
supportLevels.Add(Math.Round(currentPrice * 0.98, 2));
supportLevels.Add(Math.Round(currentPrice * 0.95, 2));
resistanceLevels.Add(Math.Round(currentPrice * 1.03, 2));
resistanceLevels.Add(Math.Round(currentPrice * 1.06, 2));
}
if (n8nResponse?.ExecutionPlan?.EntryZone != null)
{
if (n8nResponse.ExecutionPlan.EntryZone.Min > 0) supportLevels.Insert(0, (double)n8nResponse.ExecutionPlan.EntryZone.Min);
if (n8nResponse.ExecutionPlan.EntryZone.Max > 0) resistanceLevels.Insert(0, (double)n8nResponse.ExecutionPlan.EntryZone.Max);
}
var recommendation = new AssetRecommendationDto
{
Mode = "AUTO_SCREENER",
Timestamp = DateTime.UtcNow,
RecommendedAsset = new RecommendedAssetInfo
{
Symbol = finalSymbol,
CompanyName = finalName,
Isin = filterResult.Isin,
Market = marketRegion,
Bias = string.Equals(n8nResponse?.SuggestedDirection, "Short", StringComparison.OrdinalIgnoreCase) ? "BEARISH" : "BULLISH",
ConfidenceScore = Math.Round(confidenceScore, 2),
Timeframe = !string.IsNullOrWhiteSpace(n8nResponse?.SuggestedTimeframe) ? n8nResponse.SuggestedTimeframe : "1D"
},
Rationale = new RecommendationRationaleInfo
{
PatternDetected = taInfo.DetectedPatterns?.Count > 0
? string.Join(", ", taInfo.DetectedPatterns.Select(p => p.PatternName))
: (!string.IsNullOrWhiteSpace(n8nResponse?.DetailedAnalysis?.TechnicalRationale) ? n8nResponse.DetailedAnalysis.TechnicalRationale : "Multi-Timeframe Trend & Volume Confluence"),
VixContext = $"VIX at {currentVix:F1} ({regime} volatility environment)",
KeyTechnicalLevels = new KeyTechnicalLevelsInfo
{
Support = supportLevels.Distinct().ToList(),
Resistance = resistanceLevels.Distinct().ToList()
},
Summary = !string.IsNullOrWhiteSpace(n8nReasoning(n8nResponse))
? n8nResponse!.AiReasoning
: "High conviction setup based on multi-timeframe technical confluence, sentiment, and fundamental data."
},
ActionRequired = isHighConviction ? "PROMPT_USER_FOR_MANUAL_TRADE" : "NO_ACTION"
};
double dynamicWinRate = _winRateCalculator.CalculateDynamicWinRate(
filterResult.Sector,
finalSymbol,
regime,
n8nEvalScore: n8nResponse?.EvalScore,
sentimentScore: sentResp?.CurrentSummary?.CompoundScore,
signalType: n8nResponse?.SuggestedDirection ?? "BUY");
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)
{
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
{
TradeId = "PROP-" + Guid.NewGuid().ToString("N")[..10].ToUpperInvariant(),
AnalysisId = analysisId,
EventId = eventId,
Sector = filterResult.Sector,
Symbol = finalSymbol,
Isin = filterResult.Isin,
CompanyName = finalName,
EntryPrice = (decimal)currentPrice,
SignalType = string.Equals(n8nResponse.SuggestedDirection, "Short", StringComparison.OrdinalIgnoreCase) ? "SELL" : "BUY",
Status = "Proposed",
RiskTolerance = n8nResponse.SuggestedRisk ?? "Balanced",
Timeframe = $"{minTf}-{maxTf} Tage",
InstrumentType = "KnockOut",
WinRate = dynamicWinRate,
VixRegime = regime,
VixValue = currentVix,
TtlMinutes = 180,
Reasoning = n8nResponse.AiReasoning ?? "Auto-Screener High Conviction Trade",
StopLoss = n8nResponse.ExecutionPlan?.StopLoss ?? 0,
TakeProfit = n8nResponse.ExecutionPlan?.TakeProfitTargets != null && n8nResponse.ExecutionPlan.TakeProfitTargets.Count > 0 ? n8nResponse.ExecutionPlan.TakeProfitTargets[0] : 0,
EntryZoneMin = n8nResponse.ExecutionPlan?.EntryZone?.Min,
EntryZoneMax = n8nResponse.ExecutionPlan?.EntryZone?.Max,
TakeProfitTargets = n8nResponse.ExecutionPlan?.TakeProfitTargets,
RiskRewardRatio = n8nResponse.ExecutionPlan?.RiskRewardRatio,
MaxLeverage = n8nResponse.ExecutionPlan?.MaxLeverage,
TechnicalRationale = n8nResponse.DetailedAnalysis?.TechnicalRationale ?? string.Empty,
FundamentalRationale = n8nResponse.DetailedAnalysis?.FundamentalRationale ?? string.Empty,
RiskWarning = n8nResponse.DetailedAnalysis?.RiskWarning ?? string.Empty,
CreatedAt = DateTime.UtcNow
};
string propTopic = $"finlytic/trades/proposed/{(string.IsNullOrWhiteSpace(filterResult.Sector) ? "general" : filterResult.Sector.ToLowerInvariant())}/{finalSymbol.ToLowerInvariant()}";
await PublishAsync(propTopic, autoProposalDto);
await finlyticLogger.LogInfoAsync(SettingKeys.AnalyzerChannel, "[AutoScreener] Dispatched High-Conviction Proposal {TradeId} to topic {Topic}", autoProposalDto.TradeId, propTopic);
}
if (isHighConviction)
{
string recTopic = $"finlytic/recommendations/auto/{(string.IsNullOrWhiteSpace(filterResult.Sector) ? "general" : filterResult.Sector.ToLowerInvariant())}/{finalSymbol.ToLowerInvariant()}";
await PublishAsync(recTopic, recommendation);
await PublishAsync("finlytic/recommendations/auto", recommendation);
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
{
await finlyticLogger.LogInfoAsync(SettingKeys.AnalyzerChannel, "[AutoScreener] [DROPPED] Low-conviction signal for {Symbol} dropped (Confidence: {Score:F2}, Action: NO_ACTION)",
finalSymbol, recommendation.RecommendedAsset.ConfidenceScore);
}
}
private static string n8nReasoning(N8nAnalysisResponseDto? resp) => resp?.AiReasoning ?? string.Empty;
}