Compare commits

...

10 Commits

1179 changed files with 1228806 additions and 1964 deletions
+94 -1
View File
@@ -1,4 +1,4 @@
Microsoft Visual Studio Solution File, Format Version 12.00
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FinlyticAssets", "FinlyticAssets\FinlyticAssets.csproj", "{B5AC2C29-44D5-4538-815F-F02CDDE9D01F}"
EndProject
@@ -11,23 +11,116 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FinlyticCore", "FinlyticCor
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FinlyticNews", "FinlyticNews\FinlyticNews.csproj", "{03B4D920-6173-44E8-A1E2-8945D8393CEA}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FinlyticFundamentals", "FinlyticFundamentals\FinlyticFundamentals.csproj", "{D458A1B3-16CF-45E8-859D-87542A4A83A7}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FinlyticSentiment", "FinlyticSentiment\FinlyticSentiment.csproj", "{9C3BB705-86AD-4A89-AA0F-A52C87A4950B}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FinlyticTechnicalAnalysis", "FinlyticTechnicalAnalysis\FinlyticTechnicalAnalysis.csproj", "{A1C82F63-4482-4E99-9231-1184FA2E001F}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FinlyticAnalyzer", "FinlyticAnalyzer\FinlyticAnalyzer.csproj", "{E9F7C091-62C4-417A-B981-8977DF82A1B0}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FinlyticTrades", "FinlyticTrades\FinlyticTrades.csproj", "{57D84C2E-73E1-4231-A91B-6B620FCE5289}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FinlyticBackend", "FinlyticBackend\FinlyticBackend.csproj", "{C1A924B8-904E-436D-B07E-4E621F51C1AA}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
Release|x64 = Release|x64
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{B5AC2C29-44D5-4538-815F-F02CDDE9D01F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B5AC2C29-44D5-4538-815F-F02CDDE9D01F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B5AC2C29-44D5-4538-815F-F02CDDE9D01F}.Debug|x64.ActiveCfg = Debug|Any CPU
{B5AC2C29-44D5-4538-815F-F02CDDE9D01F}.Debug|x64.Build.0 = Debug|Any CPU
{B5AC2C29-44D5-4538-815F-F02CDDE9D01F}.Debug|x86.ActiveCfg = Debug|Any CPU
{B5AC2C29-44D5-4538-815F-F02CDDE9D01F}.Debug|x86.Build.0 = Debug|Any CPU
{B5AC2C29-44D5-4538-815F-F02CDDE9D01F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B5AC2C29-44D5-4538-815F-F02CDDE9D01F}.Release|Any CPU.Build.0 = Release|Any CPU
{B5AC2C29-44D5-4538-815F-F02CDDE9D01F}.Release|x64.ActiveCfg = Release|Any CPU
{B5AC2C29-44D5-4538-815F-F02CDDE9D01F}.Release|x64.Build.0 = Release|Any CPU
{B5AC2C29-44D5-4538-815F-F02CDDE9D01F}.Release|x86.ActiveCfg = Release|Any CPU
{B5AC2C29-44D5-4538-815F-F02CDDE9D01F}.Release|x86.Build.0 = Release|Any CPU
{03E2ECBB-0FF9-43E4-994E-F6A522860AD5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{03E2ECBB-0FF9-43E4-994E-F6A522860AD5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{03E2ECBB-0FF9-43E4-994E-F6A522860AD5}.Debug|x64.ActiveCfg = Debug|Any CPU
{03E2ECBB-0FF9-43E4-994E-F6A522860AD5}.Debug|x64.Build.0 = Debug|Any CPU
{03E2ECBB-0FF9-43E4-994E-F6A522860AD5}.Debug|x86.ActiveCfg = Debug|Any CPU
{03E2ECBB-0FF9-43E4-994E-F6A522860AD5}.Debug|x86.Build.0 = Debug|Any CPU
{03E2ECBB-0FF9-43E4-994E-F6A522860AD5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{03E2ECBB-0FF9-43E4-994E-F6A522860AD5}.Release|Any CPU.Build.0 = Release|Any CPU
{03E2ECBB-0FF9-43E4-994E-F6A522860AD5}.Release|x64.ActiveCfg = Release|Any CPU
{03E2ECBB-0FF9-43E4-994E-F6A522860AD5}.Release|x64.Build.0 = Release|Any CPU
{03E2ECBB-0FF9-43E4-994E-F6A522860AD5}.Release|x86.ActiveCfg = Release|Any CPU
{03E2ECBB-0FF9-43E4-994E-F6A522860AD5}.Release|x86.Build.0 = Release|Any CPU
{03B4D920-6173-44E8-A1E2-8945D8393CEA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{03B4D920-6173-44E8-A1E2-8945D8393CEA}.Debug|Any CPU.Build.0 = Debug|Any CPU
{03B4D920-6173-44E8-A1E2-8945D8393CEA}.Debug|x64.ActiveCfg = Debug|Any CPU
{03B4D920-6173-44E8-A1E2-8945D8393CEA}.Debug|x64.Build.0 = Debug|Any CPU
{03B4D920-6173-44E8-A1E2-8945D8393CEA}.Debug|x86.ActiveCfg = Debug|Any CPU
{03B4D920-6173-44E8-A1E2-8945D8393CEA}.Debug|x86.Build.0 = Debug|Any CPU
{03B4D920-6173-44E8-A1E2-8945D8393CEA}.Release|Any CPU.ActiveCfg = Release|Any CPU
{03B4D920-6173-44E8-A1E2-8945D8393CEA}.Release|Any CPU.Build.0 = Release|Any CPU
{03B4D920-6173-44E8-A1E2-8945D8393CEA}.Release|x64.ActiveCfg = Release|Any CPU
{03B4D920-6173-44E8-A1E2-8945D8393CEA}.Release|x64.Build.0 = Release|Any CPU
{03B4D920-6173-44E8-A1E2-8945D8393CEA}.Release|x86.ActiveCfg = Release|Any CPU
{03B4D920-6173-44E8-A1E2-8945D8393CEA}.Release|x86.Build.0 = Release|Any CPU
{999DB199-69C5-46D2-BDAD-59C84F5769F1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{999DB199-69C5-46D2-BDAD-59C84F5769F1}.Debug|Any CPU.Build.0 = Debug|Any CPU
{999DB199-69C5-46D2-BDAD-59C84F5769F1}.Debug|x64.ActiveCfg = Debug|Any CPU
{999DB199-69C5-46D2-BDAD-59C84F5769F1}.Debug|x64.Build.0 = Debug|Any CPU
{999DB199-69C5-46D2-BDAD-59C84F5769F1}.Debug|x86.ActiveCfg = Debug|Any CPU
{999DB199-69C5-46D2-BDAD-59C84F5769F1}.Debug|x86.Build.0 = Debug|Any CPU
{999DB199-69C5-46D2-BDAD-59C84F5769F1}.Release|Any CPU.ActiveCfg = Release|Any CPU
{999DB199-69C5-46D2-BDAD-59C84F5769F1}.Release|Any CPU.Build.0 = Release|Any CPU
{999DB199-69C5-46D2-BDAD-59C84F5769F1}.Release|x64.ActiveCfg = Release|Any CPU
{999DB199-69C5-46D2-BDAD-59C84F5769F1}.Release|x64.Build.0 = Release|Any CPU
{999DB199-69C5-46D2-BDAD-59C84F5769F1}.Release|x86.ActiveCfg = Release|Any CPU
{999DB199-69C5-46D2-BDAD-59C84F5769F1}.Release|x86.Build.0 = Release|Any CPU
{D458A1B3-16CF-45E8-859D-87542A4A83A7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D458A1B3-16CF-45E8-859D-87542A4A83A7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D458A1B3-16CF-45E8-859D-87542A4A83A7}.Debug|x64.ActiveCfg = Debug|Any CPU
{D458A1B3-16CF-45E8-859D-87542A4A83A7}.Debug|x64.Build.0 = Debug|Any CPU
{D458A1B3-16CF-45E8-859D-87542A4A83A7}.Debug|x86.ActiveCfg = Debug|Any CPU
{D458A1B3-16CF-45E8-859D-87542A4A83A7}.Debug|x86.Build.0 = Debug|Any CPU
{D458A1B3-16CF-45E8-859D-87542A4A83A7}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D458A1B3-16CF-45E8-859D-87542A4A83A7}.Release|Any CPU.Build.0 = Release|Any CPU
{D458A1B3-16CF-45E8-859D-87542A4A83A7}.Release|x64.ActiveCfg = Release|Any CPU
{D458A1B3-16CF-45E8-859D-87542A4A83A7}.Release|x64.Build.0 = Release|Any CPU
{D458A1B3-16CF-45E8-859D-87542A4A83A7}.Release|x86.ActiveCfg = Release|Any CPU
{D458A1B3-16CF-45E8-859D-87542A4A83A7}.Release|x86.Build.0 = Release|Any CPU
{9C3BB705-86AD-4A89-AA0F-A52C87A4950B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{9C3BB705-86AD-4A89-AA0F-A52C87A4950B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9C3BB705-86AD-4A89-AA0F-A52C87A4950B}.Debug|x64.ActiveCfg = Debug|Any CPU
{9C3BB705-86AD-4A89-AA0F-A52C87A4950B}.Debug|x64.Build.0 = Debug|Any CPU
{9C3BB705-86AD-4A89-AA0F-A52C87A4950B}.Debug|x86.ActiveCfg = Debug|Any CPU
{9C3BB705-86AD-4A89-AA0F-A52C87A4950B}.Debug|x86.Build.0 = Debug|Any CPU
{9C3BB705-86AD-4A89-AA0F-A52C87A4950B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9C3BB705-86AD-4A89-AA0F-A52C87A4950B}.Release|Any CPU.Build.0 = Release|Any CPU
{9C3BB705-86AD-4A89-AA0F-A52C87A4950B}.Release|x64.ActiveCfg = Release|Any CPU
{9C3BB705-86AD-4A89-AA0F-A52C87A4950B}.Release|x64.Build.0 = Release|Any CPU
{9C3BB705-86AD-4A89-AA0F-A52C87A4950B}.Release|x86.ActiveCfg = Release|Any CPU
{9C3BB705-86AD-4A89-AA0F-A52C87A4950B}.Release|x86.Build.0 = Release|Any CPU
{A1C82F63-4482-4E99-9231-1184FA2E001F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A1C82F63-4482-4E99-9231-1184FA2E001F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E9F7C091-62C4-417A-B981-8977DF82A1B0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E9F7C091-62C4-417A-B981-8977DF82A1B0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E9F7C091-62C4-417A-B981-8977DF82A1B0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E9F7C091-62C4-417A-B981-8977DF82A1B0}.Release|Any CPU.Build.0 = Release|Any CPU
{57D84C2E-73E1-4231-A91B-6B620FCE5289}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{57D84C2E-73E1-4231-A91B-6B620FCE5289}.Debug|Any CPU.Build.0 = Debug|Any CPU
{57D84C2E-73E1-4231-A91B-6B620FCE5289}.Release|Any CPU.ActiveCfg = Release|Any CPU
{57D84C2E-73E1-4231-A91B-6B620FCE5289}.Release|Any CPU.Build.0 = Release|Any CPU
{C1A924B8-904E-436D-B07E-4E621F51C1AA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C1A924B8-904E-436D-B07E-4E621F51C1AA}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C1A924B8-904E-436D-B07E-4E621F51C1AA}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C1A924B8-904E-436D-B07E-4E621F51C1AA}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
EndGlobal
@@ -0,0 +1,190 @@
using System;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using FinlyticAnalyzer.Database;
using FinlyticAnalyzer.Entities;
using FinlyticAnalyzer.Services;
using FinlyticCore.Models.Analyzer;
using FinlyticCore.Models.Trades;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace FinlyticAnalyzer.Controllers;
public class ManualAnalysisRequest
{
public string Symbol { get; set; } = string.Empty;
public string Isin { get; set; } = string.Empty;
public string Sector { get; set; } = "Technology";
public string Headline { get; set; } = "Manual User Request";
public decimal CurrentPrice { get; set; } = 100.0m;
public int RiskScore { get; set; } = 50; // 0 to 100
public int MinTimeframeValue { get; set; } = 4;
public int MaxTimeframeValue { get; set; } = 6;
public string TimeframeUnit { get; set; } = "Tage";
public string InstrumentType { get; set; } = "Stock";
public string UserNotes { get; set; } = string.Empty;
}
[ApiController]
[Route("api/v1/analyze")]
public class ManualAnalysisController : ControllerBase
{
private readonly IVixTrackerService _vixTracker;
private readonly IN8nEvaluationService _n8nService;
private readonly IWinRateCalculator _winRateCalculator;
private readonly AnalyzerDbContext _dbContext;
private readonly ILogger<ManualAnalysisController> _logger;
public ManualAnalysisController(
IVixTrackerService vixTracker,
IN8nEvaluationService n8nService,
IWinRateCalculator winRateCalculator,
AnalyzerDbContext dbContext,
ILogger<ManualAnalysisController> logger)
{
_vixTracker = vixTracker;
_n8nService = n8nService;
_winRateCalculator = winRateCalculator;
_dbContext = dbContext;
_logger = logger;
}
/// <summary>
/// Runs a manual analysis based on the provided request.
/// </summary>
[HttpPost("manual")]
public async Task<IActionResult> RunManualAnalysis([FromBody] ManualAnalysisRequest request, CancellationToken cancellationToken)
{
if (string.IsNullOrWhiteSpace(request.Symbol) && string.IsNullOrWhiteSpace(request.Isin))
{
return BadRequest(new { error = "Symbol or ISIN is required." });
}
var regime = _vixTracker.GetCurrentRegime();
var currentVix = _vixTracker.GetCurrentVix();
string analysisId = Guid.NewGuid().ToString("N");
double winRate = _winRateCalculator.CalculateWinRate(request.Sector, request.Symbol, regime);
string riskLabel = request.RiskScore > 70 ? $"Aggressiv ({request.RiskScore}/100)" : (request.RiskScore > 30 ? $"Balanced ({request.RiskScore}/100)" : $"Konservativ ({request.RiskScore}/100)");
string timeframeFormatted = $"{request.MinTimeframeValue}-{request.MaxTimeframeValue} {request.TimeframeUnit}";
var n8nRequest = new N8nAnalysisRequestDto
{
RequestId = analysisId,
Timestamp = DateTime.UtcNow,
TriggerType = "Manual",
TargetAsset = new TargetAssetInfo
{
Symbol = request.Symbol.ToUpperInvariant(),
Isin = request.Isin.ToUpperInvariant(),
Sector = request.Sector
},
MarketContext = new MarketContextInfo
{
Vix = currentVix,
MarketRegime = regime.ToString()
},
FilterContext = new FilterContextInfo
{
ImpactScore = 1.0,
RawNewsHeadline = string.IsNullOrWhiteSpace(request.Headline) ? "Manual User Trigger" : request.Headline
},
UserPreferences = new UserPreferencesInfo
{
RiskScore = request.RiskScore,
RiskTolerance = riskLabel,
MinTimeframeValue = request.MinTimeframeValue,
MaxTimeframeValue = request.MaxTimeframeValue,
TimeframeUnit = request.TimeframeUnit,
TimeframeFormatted = timeframeFormatted,
InstrumentType = request.InstrumentType,
UserNotes = request.UserNotes
},
TradeFeedback = new TradeFeedbackInfo
{
TotalAssetTrades = 12,
AssetWinRate = winRate,
AvgReturnPercent = 3.4,
LastTradeResult = "WIN"
}
};
var n8nResponse = await _n8nService.EvaluateAssetAsync(n8nRequest, cancellationToken);
bool shouldProceed = n8nResponse != null && string.Equals(n8nResponse.AiDecision, "Proceed", StringComparison.OrdinalIgnoreCase);
TradeProposalDto? proposal = null;
if (shouldProceed && n8nResponse != null)
{
proposal = new TradeProposalDto
{
AnalysisId = analysisId,
EventId = analysisId,
Sector = request.Sector,
Symbol = request.Symbol.ToUpperInvariant(),
Isin = request.Isin.ToUpperInvariant(),
CompanyName = request.Symbol,
EntryPrice = request.CurrentPrice,
SignalType = string.Equals(n8nResponse.SuggestedDirection, "Short", StringComparison.OrdinalIgnoreCase) ? "SELL" : "BUY",
RiskTolerance = n8nResponse.SuggestedRisk,
Timeframe = timeframeFormatted,
InstrumentType = request.InstrumentType,
WinRate = winRate,
VixRegime = regime,
VixValue = currentVix,
TtlMinutes = 60,
Reasoning = $"Manual n8n Evaluation ({n8nResponse.AiDecision}): {n8nResponse.AiReasoning}",
CreatedAt = DateTime.UtcNow
};
}
var analysisEntity = new AnalysisEntity
{
AnalysisId = analysisId,
EventId = analysisId,
Sector = request.Sector,
Symbol = request.Symbol.ToUpperInvariant(),
Isin = request.Isin.ToUpperInvariant(),
VixRegime = regime,
VixValue = currentVix,
ImpactScore = 1.0,
WinRate = winRate,
RawDataJson = JsonSerializer.Serialize(request),
AiOutputJson = proposal != null ? JsonSerializer.Serialize(proposal) : "{}",
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);
if (!shouldProceed)
{
return Ok(new
{
analysisId,
isTradeProposed = false,
status = "Rejected",
recommendation = "NOT_RECOMMENDED",
reasoning = n8nResponse?.AiReasoning ?? "Die KI stuft diesen Trade als zu riskant ein und empfiehlt keine Positionierung.",
n8nResponse,
proposal = (object?)null
});
}
return Ok(new
{
analysisId,
isTradeProposed = true,
status = "Success",
recommendation = "RECOMMENDED",
n8nResponse,
proposal
});
}
}
@@ -0,0 +1,33 @@
using FinlyticAnalyzer.Entities;
using Microsoft.EntityFrameworkCore;
namespace FinlyticAnalyzer.Database;
public class AnalyzerDbContext : DbContext
{
public AnalyzerDbContext(DbContextOptions<AnalyzerDbContext> options) : base(options) { }
public DbSet<AnalysisEntity> Analyses => Set<AnalysisEntity>();
public DbSet<AnalyzerSettingsEntity> Settings => Set<AnalyzerSettingsEntity>();
public DbSet<TradeProposalEntity> TradeProposals => Set<TradeProposalEntity>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<AnalysisEntity>(entity =>
{
entity.HasIndex(e => e.AnalysisId).IsUnique();
entity.HasIndex(e => e.EventId);
entity.HasIndex(e => e.Isin);
entity.HasIndex(e => e.Sector);
entity.HasIndex(e => e.CreatedAt);
});
modelBuilder.Entity<TradeProposalEntity>(entity =>
{
entity.HasIndex(e => e.Isin);
entity.HasIndex(e => e.ExpiresAt);
});
}
}
+16
View File
@@ -0,0 +1,16 @@
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src
COPY ["FinlyticCore/FinlyticCore.csproj", "FinlyticCore/"]
COPY ["FinlyticAnalyzer/FinlyticAnalyzer.csproj", "FinlyticAnalyzer/"]
RUN dotnet restore "FinlyticAnalyzer/FinlyticAnalyzer.csproj"
COPY . .
WORKDIR "/src/FinlyticAnalyzer"
RUN dotnet build "FinlyticAnalyzer.csproj" -c Release -o /app/build
FROM build AS publish
RUN dotnet publish "FinlyticAnalyzer.csproj" -c Release -o /app/publish /p:UseAppHost=false
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "FinlyticAnalyzer.dll"]
@@ -0,0 +1,60 @@
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using FinlyticCore.Models.Analyzer;
namespace FinlyticAnalyzer.Entities;
/// <summary>
/// Persisted raw news, market context, AI prompt payload & response in PostgreSQL.
/// </summary>
[Table("analyses")]
public class AnalysisEntity
{
[Key]
public Guid Id { get; set; } = Guid.NewGuid();
[Required]
[MaxLength(100)]
public string AnalysisId { get; set; } = string.Empty;
[Required]
[MaxLength(100)]
public string EventId { get; set; } = string.Empty;
[Required]
[MaxLength(50)]
public string Sector { get; set; } = string.Empty;
[Required]
[MaxLength(30)]
public string Symbol { get; set; } = string.Empty;
[Required]
[MaxLength(30)]
public string Isin { get; set; } = string.Empty;
public VixMarketRegime VixRegime { get; set; }
public decimal VixValue { get; set; }
public double ImpactScore { get; set; }
public double WinRate { get; set; }
[Column(TypeName = "jsonb")]
public string RawDataJson { get; set; } = "{}";
[Column(TypeName = "jsonb")]
public string AiOutputJson { get; set; } = "{}";
[Column(TypeName = "jsonb")]
public string N8nResponseJson { get; set; } = "{}";
public double N8nEvalScore { get; set; }
[MaxLength(30)]
public string N8nDecision { get; set; } = string.Empty;
public bool IsTradeProposed { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
}
@@ -0,0 +1,21 @@
using System;
using System.ComponentModel.DataAnnotations;
namespace FinlyticAnalyzer.Entities;
public class AnalyzerSettingsEntity
{
[Key]
public Guid Id { get; set; }
public string ScanCronSchedule { get; set; } = "0 */1 * * *";
public double MinSignalScore { get; set; } = 75.0;
public bool EnableLogMqttHealthPing { get; set; } = false;
public bool EnableLogMqttGeneral { get; set; } = true;
public bool EnableLogAnalyzerAuto { get; set; } = true;
public bool EnableLogAnalyzerManual { get; set; } = true;
public bool EnableLogDatabaseOps { get; set; } = true;
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
}
@@ -0,0 +1,95 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using FinlyticCore.Models.Analyzer;
using FinlyticCore.Models.Assets;
namespace FinlyticAnalyzer.Entities;
[Table("trade_proposals")]
public class TradeProposalEntity
{
[Key]
public Guid Id { get; set; } = Guid.NewGuid();
[Required]
[MaxLength(100)]
public string AnalysisId { get; set; } = string.Empty;
[Required]
[MaxLength(100)]
public string EventId { get; set; } = string.Empty;
[Required]
[MaxLength(30)]
public string Isin { get; set; } = string.Empty;
[MaxLength(30)]
public string Symbol { get; set; } = string.Empty;
[MaxLength(150)]
public string Name { get; set; } = string.Empty;
[MaxLength(50)]
public string Sector { get; set; } = "General";
public AssetType Type { get; set; } = AssetType.Stock;
/// <summary>
/// KI-Entscheidung ("BUY", "SELL", "HOLD", "REJECTED")
/// </summary>
[MaxLength(20)]
public string ProposedAction { get; set; } = "BUY";
public double ConfidenceScore { get; set; }
// --- KI Execution Plan (Vorgeschlagene Preismarken) ---
[Column(TypeName = "decimal(18,4)")]
public decimal EntryPrice { get; set; }
[Column(TypeName = "decimal(18,4)")]
public decimal StopLoss { get; set; }
[Column(TypeName = "decimal(18,4)")]
public decimal TakeProfit { get; set; }
[Column(TypeName = "decimal(18,4)")]
public decimal? EntryZoneMin { get; set; }
[Column(TypeName = "decimal(18,4)")]
public decimal? EntryZoneMax { get; set; }
public string? TakeProfitTargets { get; set; } // Comma-separated or JSON
[Column(TypeName = "decimal(18,4)")]
public decimal? RiskRewardRatio { get; set; }
[Column(TypeName = "decimal(18,4)")]
public decimal? MaxLeverage { get; set; }
// --- Kontext aus Request & KI ---
public string ReasonSummary { get; set; } = string.Empty;
public string TechnicalRationale { get; set; } = string.Empty;
public string FundamentalRationale { get; set; } = string.Empty;
public string RiskWarning { get; set; } = string.Empty;
[MaxLength(30)]
public string RiskTolerance { get; set; } = "Balanced";
[MaxLength(20)]
public string Timeframe { get; set; } = "1-7 Tage";
[MaxLength(30)]
public string InstrumentType { get; set; } = "KnockOut";
public VixMarketRegime VixRegime { get; set; }
[Column(TypeName = "decimal(18,4)")]
public decimal VixValue { get; set; }
public double WinRate { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime ExpiresAt { get; set; } = DateTime.UtcNow.AddHours(3);
}
+26
View File
@@ -0,0 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.9" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.9">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.9" />
<PackageReference Include="Microsoft.Extensions.Http" Version="10.0.9" />
<PackageReference Include="MQTTnet" Version="5.1.0.1559" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.2" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\FinlyticCore\FinlyticCore.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,136 @@
// <auto-generated />
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("20260801073402_Init")]
partial class Init
{
/// <inheritdoc />
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<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("AiOutputJson")
.IsRequired()
.HasColumnType("jsonb");
b.Property<string>("AnalysisId")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("EventId")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<double>("ImpactScore")
.HasColumnType("double precision");
b.Property<bool>("IsTradeProposed")
.HasColumnType("boolean");
b.Property<string>("Isin")
.IsRequired()
.HasMaxLength(30)
.HasColumnType("character varying(30)");
b.Property<string>("N8nDecision")
.IsRequired()
.HasMaxLength(30)
.HasColumnType("character varying(30)");
b.Property<double>("N8nEvalScore")
.HasColumnType("double precision");
b.Property<string>("N8nResponseJson")
.IsRequired()
.HasColumnType("jsonb");
b.Property<string>("RawDataJson")
.IsRequired()
.HasColumnType("jsonb");
b.Property<string>("Sector")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<string>("Symbol")
.IsRequired()
.HasMaxLength(30)
.HasColumnType("character varying(30)");
b.Property<int>("VixRegime")
.HasColumnType("integer");
b.Property<decimal>("VixValue")
.HasColumnType("numeric");
b.Property<double>("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<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<double>("MinSignalScore")
.HasColumnType("double precision");
b.Property<string>("ScanCronSchedule")
.IsRequired()
.HasColumnType("text");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.ToTable("Settings");
});
#pragma warning restore 612, 618
}
}
}
@@ -0,0 +1,92 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace FinlyticAnalyzer.Migrations
{
/// <inheritdoc />
public partial class Init : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "analyses",
columns: table => new
{
Id = table.Column<Guid>(type: "uuid", nullable: false),
AnalysisId = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false),
EventId = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false),
Sector = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: false),
Symbol = table.Column<string>(type: "character varying(30)", maxLength: 30, nullable: false),
Isin = table.Column<string>(type: "character varying(30)", maxLength: 30, nullable: false),
VixRegime = table.Column<int>(type: "integer", nullable: false),
VixValue = table.Column<decimal>(type: "numeric", nullable: false),
ImpactScore = table.Column<double>(type: "double precision", nullable: false),
WinRate = table.Column<double>(type: "double precision", nullable: false),
RawDataJson = table.Column<string>(type: "jsonb", nullable: false),
AiOutputJson = table.Column<string>(type: "jsonb", nullable: false),
N8nResponseJson = table.Column<string>(type: "jsonb", nullable: false),
N8nEvalScore = table.Column<double>(type: "double precision", nullable: false),
N8nDecision = table.Column<string>(type: "character varying(30)", maxLength: 30, nullable: false),
IsTradeProposed = table.Column<bool>(type: "boolean", nullable: false),
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_analyses", x => x.Id);
});
migrationBuilder.CreateTable(
name: "Settings",
columns: table => new
{
Id = table.Column<Guid>(type: "uuid", nullable: false),
ScanCronSchedule = table.Column<string>(type: "text", nullable: false),
MinSignalScore = table.Column<double>(type: "double precision", nullable: false),
UpdatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Settings", x => x.Id);
});
migrationBuilder.CreateIndex(
name: "IX_analyses_AnalysisId",
table: "analyses",
column: "AnalysisId",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_analyses_CreatedAt",
table: "analyses",
column: "CreatedAt");
migrationBuilder.CreateIndex(
name: "IX_analyses_EventId",
table: "analyses",
column: "EventId");
migrationBuilder.CreateIndex(
name: "IX_analyses_Isin",
table: "analyses",
column: "Isin");
migrationBuilder.CreateIndex(
name: "IX_analyses_Sector",
table: "analyses",
column: "Sector");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "analyses");
migrationBuilder.DropTable(
name: "Settings");
}
}
}
@@ -0,0 +1,151 @@
// <auto-generated />
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("20260803185020_AddLogFilterSettings")]
partial class AddLogFilterSettings
{
/// <inheritdoc />
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<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("AiOutputJson")
.IsRequired()
.HasColumnType("jsonb");
b.Property<string>("AnalysisId")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("EventId")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<double>("ImpactScore")
.HasColumnType("double precision");
b.Property<bool>("IsTradeProposed")
.HasColumnType("boolean");
b.Property<string>("Isin")
.IsRequired()
.HasMaxLength(30)
.HasColumnType("character varying(30)");
b.Property<string>("N8nDecision")
.IsRequired()
.HasMaxLength(30)
.HasColumnType("character varying(30)");
b.Property<double>("N8nEvalScore")
.HasColumnType("double precision");
b.Property<string>("N8nResponseJson")
.IsRequired()
.HasColumnType("jsonb");
b.Property<string>("RawDataJson")
.IsRequired()
.HasColumnType("jsonb");
b.Property<string>("Sector")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<string>("Symbol")
.IsRequired()
.HasMaxLength(30)
.HasColumnType("character varying(30)");
b.Property<int>("VixRegime")
.HasColumnType("integer");
b.Property<decimal>("VixValue")
.HasColumnType("numeric");
b.Property<double>("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<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<bool>("EnableLogAnalyzerAuto")
.HasColumnType("boolean");
b.Property<bool>("EnableLogAnalyzerManual")
.HasColumnType("boolean");
b.Property<bool>("EnableLogDatabaseOps")
.HasColumnType("boolean");
b.Property<bool>("EnableLogMqttGeneral")
.HasColumnType("boolean");
b.Property<bool>("EnableLogMqttHealthPing")
.HasColumnType("boolean");
b.Property<double>("MinSignalScore")
.HasColumnType("double precision");
b.Property<string>("ScanCronSchedule")
.IsRequired()
.HasColumnType("text");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.ToTable("Settings");
});
#pragma warning restore 612, 618
}
}
}
@@ -0,0 +1,73 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace FinlyticAnalyzer.Migrations
{
/// <inheritdoc />
public partial class AddLogFilterSettings : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<bool>(
name: "EnableLogAnalyzerAuto",
table: "Settings",
type: "boolean",
nullable: false,
defaultValue: false);
migrationBuilder.AddColumn<bool>(
name: "EnableLogAnalyzerManual",
table: "Settings",
type: "boolean",
nullable: false,
defaultValue: false);
migrationBuilder.AddColumn<bool>(
name: "EnableLogDatabaseOps",
table: "Settings",
type: "boolean",
nullable: false,
defaultValue: false);
migrationBuilder.AddColumn<bool>(
name: "EnableLogMqttGeneral",
table: "Settings",
type: "boolean",
nullable: false,
defaultValue: false);
migrationBuilder.AddColumn<bool>(
name: "EnableLogMqttHealthPing",
table: "Settings",
type: "boolean",
nullable: false,
defaultValue: false);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "EnableLogAnalyzerAuto",
table: "Settings");
migrationBuilder.DropColumn(
name: "EnableLogAnalyzerManual",
table: "Settings");
migrationBuilder.DropColumn(
name: "EnableLogDatabaseOps",
table: "Settings");
migrationBuilder.DropColumn(
name: "EnableLogMqttGeneral",
table: "Settings");
migrationBuilder.DropColumn(
name: "EnableLogMqttHealthPing",
table: "Settings");
}
}
}
@@ -0,0 +1,151 @@
// <auto-generated />
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("20260804184350_CheckPendingMigrations")]
partial class CheckPendingMigrations
{
/// <inheritdoc />
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<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("AiOutputJson")
.IsRequired()
.HasColumnType("jsonb");
b.Property<string>("AnalysisId")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("EventId")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<double>("ImpactScore")
.HasColumnType("double precision");
b.Property<bool>("IsTradeProposed")
.HasColumnType("boolean");
b.Property<string>("Isin")
.IsRequired()
.HasMaxLength(30)
.HasColumnType("character varying(30)");
b.Property<string>("N8nDecision")
.IsRequired()
.HasMaxLength(30)
.HasColumnType("character varying(30)");
b.Property<double>("N8nEvalScore")
.HasColumnType("double precision");
b.Property<string>("N8nResponseJson")
.IsRequired()
.HasColumnType("jsonb");
b.Property<string>("RawDataJson")
.IsRequired()
.HasColumnType("jsonb");
b.Property<string>("Sector")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<string>("Symbol")
.IsRequired()
.HasMaxLength(30)
.HasColumnType("character varying(30)");
b.Property<int>("VixRegime")
.HasColumnType("integer");
b.Property<decimal>("VixValue")
.HasColumnType("numeric");
b.Property<double>("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<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<bool>("EnableLogAnalyzerAuto")
.HasColumnType("boolean");
b.Property<bool>("EnableLogAnalyzerManual")
.HasColumnType("boolean");
b.Property<bool>("EnableLogDatabaseOps")
.HasColumnType("boolean");
b.Property<bool>("EnableLogMqttGeneral")
.HasColumnType("boolean");
b.Property<bool>("EnableLogMqttHealthPing")
.HasColumnType("boolean");
b.Property<double>("MinSignalScore")
.HasColumnType("double precision");
b.Property<string>("ScanCronSchedule")
.IsRequired()
.HasColumnType("text");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.ToTable("Settings");
});
#pragma warning restore 612, 618
}
}
}
@@ -0,0 +1,22 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace FinlyticAnalyzer.Migrations
{
/// <inheritdoc />
public partial class CheckPendingMigrations : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
}
}
}
@@ -0,0 +1,194 @@
// <auto-generated />
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("20260805184638_AddTradeProposals")]
partial class AddTradeProposals
{
/// <inheritdoc />
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<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("AiOutputJson")
.IsRequired()
.HasColumnType("jsonb");
b.Property<string>("AnalysisId")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("EventId")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<double>("ImpactScore")
.HasColumnType("double precision");
b.Property<bool>("IsTradeProposed")
.HasColumnType("boolean");
b.Property<string>("Isin")
.IsRequired()
.HasMaxLength(30)
.HasColumnType("character varying(30)");
b.Property<string>("N8nDecision")
.IsRequired()
.HasMaxLength(30)
.HasColumnType("character varying(30)");
b.Property<double>("N8nEvalScore")
.HasColumnType("double precision");
b.Property<string>("N8nResponseJson")
.IsRequired()
.HasColumnType("jsonb");
b.Property<string>("RawDataJson")
.IsRequired()
.HasColumnType("jsonb");
b.Property<string>("Sector")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<string>("Symbol")
.IsRequired()
.HasMaxLength(30)
.HasColumnType("character varying(30)");
b.Property<int>("VixRegime")
.HasColumnType("integer");
b.Property<decimal>("VixValue")
.HasColumnType("numeric");
b.Property<double>("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<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<bool>("EnableLogAnalyzerAuto")
.HasColumnType("boolean");
b.Property<bool>("EnableLogAnalyzerManual")
.HasColumnType("boolean");
b.Property<bool>("EnableLogDatabaseOps")
.HasColumnType("boolean");
b.Property<bool>("EnableLogMqttGeneral")
.HasColumnType("boolean");
b.Property<bool>("EnableLogMqttHealthPing")
.HasColumnType("boolean");
b.Property<double>("MinSignalScore")
.HasColumnType("double precision");
b.Property<string>("ScanCronSchedule")
.IsRequired()
.HasColumnType("text");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.ToTable("Settings");
});
modelBuilder.Entity("FinlyticAnalyzer.Entities.TradeProposalEntity", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<double>("ConfidenceScore")
.HasColumnType("double precision");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<DateTime>("ExpiresAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Isin")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.Property<string>("ProposedAction")
.IsRequired()
.HasColumnType("text");
b.Property<string>("ReasonSummary")
.IsRequired()
.HasColumnType("text");
b.Property<int>("Type")
.HasColumnType("integer");
b.HasKey("Id");
b.HasIndex("ExpiresAt");
b.HasIndex("Isin");
b.ToTable("TradeProposals");
});
#pragma warning restore 612, 618
}
}
}
@@ -0,0 +1,51 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace FinlyticAnalyzer.Migrations
{
/// <inheritdoc />
public partial class AddTradeProposals : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "TradeProposals",
columns: table => new
{
Id = table.Column<Guid>(type: "uuid", nullable: false),
Isin = table.Column<string>(type: "text", nullable: false),
Name = table.Column<string>(type: "text", nullable: false),
Type = table.Column<int>(type: "integer", nullable: false),
ProposedAction = table.Column<string>(type: "text", nullable: false),
ConfidenceScore = table.Column<double>(type: "double precision", nullable: false),
ReasonSummary = table.Column<string>(type: "text", nullable: false),
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
ExpiresAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_TradeProposals", x => x.Id);
});
migrationBuilder.CreateIndex(
name: "IX_TradeProposals_ExpiresAt",
table: "TradeProposals",
column: "ExpiresAt");
migrationBuilder.CreateIndex(
name: "IX_TradeProposals_Isin",
table: "TradeProposals",
column: "Isin");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "TradeProposals");
}
}
}
@@ -0,0 +1,191 @@
// <auto-generated />
using System;
using FinlyticAnalyzer.Database;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace FinlyticAnalyzer.Migrations
{
[DbContext(typeof(AnalyzerDbContext))]
partial class AnalyzerDbContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(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<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("AiOutputJson")
.IsRequired()
.HasColumnType("jsonb");
b.Property<string>("AnalysisId")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("EventId")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<double>("ImpactScore")
.HasColumnType("double precision");
b.Property<bool>("IsTradeProposed")
.HasColumnType("boolean");
b.Property<string>("Isin")
.IsRequired()
.HasMaxLength(30)
.HasColumnType("character varying(30)");
b.Property<string>("N8nDecision")
.IsRequired()
.HasMaxLength(30)
.HasColumnType("character varying(30)");
b.Property<double>("N8nEvalScore")
.HasColumnType("double precision");
b.Property<string>("N8nResponseJson")
.IsRequired()
.HasColumnType("jsonb");
b.Property<string>("RawDataJson")
.IsRequired()
.HasColumnType("jsonb");
b.Property<string>("Sector")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<string>("Symbol")
.IsRequired()
.HasMaxLength(30)
.HasColumnType("character varying(30)");
b.Property<int>("VixRegime")
.HasColumnType("integer");
b.Property<decimal>("VixValue")
.HasColumnType("numeric");
b.Property<double>("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<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<bool>("EnableLogAnalyzerAuto")
.HasColumnType("boolean");
b.Property<bool>("EnableLogAnalyzerManual")
.HasColumnType("boolean");
b.Property<bool>("EnableLogDatabaseOps")
.HasColumnType("boolean");
b.Property<bool>("EnableLogMqttGeneral")
.HasColumnType("boolean");
b.Property<bool>("EnableLogMqttHealthPing")
.HasColumnType("boolean");
b.Property<double>("MinSignalScore")
.HasColumnType("double precision");
b.Property<string>("ScanCronSchedule")
.IsRequired()
.HasColumnType("text");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.ToTable("Settings");
});
modelBuilder.Entity("FinlyticAnalyzer.Entities.TradeProposalEntity", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<double>("ConfidenceScore")
.HasColumnType("double precision");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<DateTime>("ExpiresAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Isin")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.Property<string>("ProposedAction")
.IsRequired()
.HasColumnType("text");
b.Property<string>("ReasonSummary")
.IsRequired()
.HasColumnType("text");
b.Property<int>("Type")
.HasColumnType("integer");
b.HasKey("Id");
b.HasIndex("ExpiresAt");
b.HasIndex("Isin");
b.ToTable("TradeProposals");
});
#pragma warning restore 612, 618
}
}
}
+63
View File
@@ -0,0 +1,63 @@
using System;
using FinlyticAnalyzer.Database;
using FinlyticAnalyzer.Services;
using FinlyticAnalyzer.Util;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
var builder = Host.CreateApplicationBuilder(args);
// Register DB Context
builder.Services.AddDbContext<AnalyzerDbContext>(options =>
options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection")));
// Register HTTP Clients for external scrapers/webhooks
builder.Services.AddHttpClient<IVixTrackerService, VixTrackerService>();
builder.Services.AddHttpClient<IN8nEvaluationService, N8nEvaluationService>();
// Register Domain Services
builder.Services.AddSingleton<IVixTrackerService, VixTrackerService>();
builder.Services.AddSingleton<IThreeLayerFilterEngine, ThreeLayerFilterEngine>();
builder.Services.AddSingleton<IWinRateCalculator, WinRateCalculator>();
builder.Services.AddSingleton<IN8nEvaluationService, N8nEvaluationService>();
builder.Services.AddScoped<ISettingsDbService, SettingsDbService>();
// Unified MQTT Client (Handles both Events and RPC)
builder.Services.AddSingleton<AnalyzerMqttClient>();
builder.Services.AddHostedService(provider => provider.GetRequiredService<AnalyzerMqttClient>());
// Register Active Trade Monitor
builder.Services.AddHostedService<ActiveTradeMonitorWorker>();
var host = builder.Build();
// Run DB Migrations
using (var scope = host.Services.CreateScope())
{
try
{
var context = scope.ServiceProvider.GetRequiredService<AnalyzerDbContext>();
await context.Database.MigrateAsync();
Console.WriteLine("Database migrations successfully executed for FinlyticAnalyzer.");
var settingsService = scope.ServiceProvider.GetRequiredService<ISettingsDbService>();
await settingsService.GetSettingsAsync();
}
catch (Exception ex)
{
var logger = scope.ServiceProvider.GetRequiredService<ILogger<Program>>();
logger.LogError(ex, "An error occurred during database migration for FinlyticAnalyzer on startup.");
}
}
// Initial VIX Poll
using (var scope = host.Services.CreateScope())
{
var vixService = scope.ServiceProvider.GetRequiredService<IVixTrackerService>();
await vixService.PollVixAsync();
}
await host.RunAsync();
+37
View File
@@ -0,0 +1,37 @@
# Finlytic Analyzer Service
Finlytic Analyzer is the core quantitative decision engine of the Finlytic ecosystem. It evaluates multi-layered market filters, tracks VIX volatility regimes, evaluates AI win rates, and generates actionable trade proposals.
---
## Core Features & Architecture
1. **3-Layer Filter Engine (`IThreeLayerFilterEngine`)**:
- **Layer 1 (Macro VIX Regime)**: Evaluates overall volatility conditions via `IVixTrackerService`.
- **Layer 2 (Asset Technical Analysis & Indicators)**: Evaluates RSI, MACD, Moving Averages, and Supertrend alignment.
- **Layer 3 (AI Sentiment & Event Context)**: Evaluates FinBERT news sentiment scores and corporate earnings proximity.
2. **VIX Volatility Tracker (`IVixTrackerService`)**:
- Polls external VIX volatility sources and categorizes market regimes (`Low`, `Normal`, `Elevated`, `High`).
3. **Win-Rate Calculator (`IWinRateCalculator`)**:
- Calculates historical probability of success based on trade feedback records.
4. **MQTT Signal Publisher (`AnalyzerMqttClient`)**:
- Publishes generated trade proposals to `finlytic/trades/proposed/{symbol}`.
---
## Feature Status
### Implemented Features
- [x] 3-Layer Quantitative Filter Engine (`ThreeLayerFilterEngine`).
- [x] VIX Volatility Regime Tracker (`VixTrackerService`).
- [x] Win-Rate Probability Calculator (`WinRateCalculator`).
- [x] n8n AI Evaluation Integration (`N8nEvaluationService`).
- [x] Pure Worker Service Architecture (`Host.CreateApplicationBuilder`, Kestrel webserver removed).
- [x] Zero-Allocation MQTT Signal Publishing (`AnalyzerMqttClient`).
### Planned Features
- [ ] Multi-year historical Backtesting Engine with Monte Carlo simulation.
- [ ] Portfolio Risk Allocation & Kelly Criterion Position Sizing Engine.
@@ -0,0 +1,357 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using FinlyticAnalyzer.Util;
using FinlyticCore.Dtos;
using FinlyticCore.Dtos.TechnicalAnalysis;
using FinlyticCore.Models.Analyzer;
using FinlyticCore.Models.Trades;
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<ActiveTradeMonitorWorker> _logger;
private readonly IServiceScopeFactory _scopeFactory;
private readonly AnalyzerMqttClient _mqttClient;
public ActiveTradeMonitorWorker(ILogger<ActiveTradeMonitorWorker> logger, IServiceScopeFactory scopeFactory,
AnalyzerMqttClient mqttClient)
{
_logger = logger;
_scopeFactory = scopeFactory;
_mqttClient = mqttClient;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
_logger.LogInformation("[{Channel}] ActiveTradeMonitorWorker started.", "AnalyzerChannel");
try
{
await Task.Delay(TimeSpan.FromSeconds(30), stoppingToken);
}
catch (OperationCanceledException)
{
return;
}
while (!stoppingToken.IsCancellationRequested)
{
try
{
await MonitorActiveTradesAsync(stoppingToken);
}
catch (Exception ex) when (!stoppingToken.IsCancellationRequested)
{
_logger.LogError(ex, "[{Channel}] Error in ActiveTradeMonitorWorker loop.", "AnalyzerChannel");
}
try
{
await Task.Delay(TimeSpan.FromMinutes(60), stoppingToken);
}
catch (OperationCanceledException)
{
break;
}
}
_logger.LogInformation("[{Channel}] ActiveTradeMonitorWorker stopped.", "AnalyzerChannel");
}
private async Task MonitorActiveTradesAsync(CancellationToken cancellationToken)
{
if (!_mqttClient.IsConnected)
{
_logger.LogWarning("[{Channel}] Skipping trade monitoring. RPC client not connected.", "AnalyzerChannel");
return;
}
// Fetch active trades
var activeTrades = await _mqttClient.SendRpcRequestAsync<List<TradeProposalDto>, GetTradesRequest>(
"trades_Get",
new GetTradesRequest(null, "Active"),
TimeSpan.FromSeconds(10));
// Fetch proposed global trades
var proposedTrades = await _mqttClient.SendRpcRequestAsync<List<TradeProposalDto>, GetTradesRequest>(
"trades_Get",
new GetTradesRequest(null, "Proposed"),
TimeSpan.FromSeconds(10));
var trades = new List<TradeProposalDto>();
if (activeTrades != null) trades.AddRange(activeTrades);
if (proposedTrades != null) trades.AddRange(proposedTrades.Where(t => t.IsGlobalProposal));
if (trades.Count == 0)
{
_logger.LogInformation("[{Channel}] No active or proposed global trades found to monitor.",
"AnalyzerChannel");
return;
}
_logger.LogInformation("[{Channel}] Found {Count} trades to monitor. Starting evaluation...", "AnalyzerChannel",
trades.Count);
using var scope = _scopeFactory.CreateScope();
var n8nService = scope.ServiceProvider.GetRequiredService<IN8nEvaluationService>();
foreach (var trade in trades)
{
if (cancellationToken.IsCancellationRequested) break;
try
{
await ProcessTradeAsync(trade, n8nService, cancellationToken);
}
catch (Exception ex)
{
_logger.LogError(ex, "[{Channel}] Failed to monitor trade {TradeId} ({Symbol}).", "AnalyzerChannel",
trade.TradeId, trade.Symbol);
}
}
}
private async Task ProcessTradeAsync(TradeProposalDto trade, IN8nEvaluationService n8nService,
CancellationToken cancellationToken)
{
// 1. Get Live Price
var livePriceReq = new IsinRequest(trade.Isin);
var livePriceDto = await _mqttClient.SendRpcRequestAsync<LivePriceDto, IsinRequest>(
"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",
$"Time-Stop getriggert: Setup ist invalidiert. Der Trade bewegt sich zu lange seitwärts (Offen seit {(int)daysOpen} Tagen, anvisiert waren max. {maxHoldingDays} Tage).");
return;
}
if (isLong)
{
if (trade.StopLoss > 0 && currentPrice <= trade.StopLoss)
{
await SendUpdateAsync(trade, currentPrice, "Close", "Hard Stop-Loss getriggert.");
return;
}
if (trade.TakeProfit > 0 && currentPrice >= trade.TakeProfit)
{
await SendUpdateAsync(trade, currentPrice, "Close", "Hard Take-Profit erreicht.");
return;
}
}
else
{
if (trade.StopLoss > 0 && currentPrice >= trade.StopLoss)
{
await SendUpdateAsync(trade, currentPrice, "Close", "Hard Stop-Loss getriggert.");
return;
}
if (trade.TakeProfit > 0 && currentPrice <= trade.TakeProfit)
{
await SendUpdateAsync(trade, currentPrice, "Close", "Hard Take-Profit erreicht.");
return;
}
}
// 3. Run AI evaluation for soft/dynamic updates
var taResult = await _mqttClient.SendRpcRequestAsync<TechnicalAnalysisDto, IsinRequest>(
"ta_GetAnalysis", livePriceReq, TimeSpan.FromSeconds(5));
var latestIndicator = taResult?.Indicators?.LastOrDefault();
var taInfo = new TechnicalContextInfo
{
Rsi = latestIndicator?.Rsi14?.ToString("F1") ?? "N/A",
SupertrendStatus = latestIndicator?.SupertrendDirection ?? "N/A",
Atr = latestIndicator?.Atr14?.ToString("F2") ?? "N/A",
Sma50 = (double?)latestIndicator?.Sma50,
Sma200 = (double?)latestIndicator?.Sma200,
DetectedPatterns = taResult?.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<PatternContextInfo>()
};
var n8nReq = new N8nAnalysisRequestDto
{
RequestId = Guid.NewGuid().ToString("N"),
Timestamp = DateTime.UtcNow,
TriggerType = "HourlyMonitor",
TargetAsset = new TargetAssetInfo
{
Symbol = trade.Symbol,
Isin = trade.Isin,
Sector = trade.Sector
},
MarketContext = new MarketContextInfo
{
Vix = trade.VixValue,
MarketRegime = trade.VixRegime.ToString()
},
UserPreferences = new UserPreferencesInfo
{
InstrumentType = trade.InstrumentType,
TimeframeFormatted = trade.Timeframe
},
TechnicalContext = taInfo
};
var aiResponse = await n8nService.EvaluateAssetAsync(n8nReq, cancellationToken);
if (aiResponse == null)
{
_logger.LogWarning("[{Channel}] AI evaluation returned null for {TradeId}. Skipping update.",
"AnalyzerChannel", trade.TradeId);
return;
}
string newRecommendation = "Hold";
string reasoning = aiResponse.AiReasoning;
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);
bool aiSuggestsLong =
string.Equals(aiResponse.SuggestedDirection, "Long", StringComparison.OrdinalIgnoreCase) ||
string.Equals(aiResponse.SuggestedDirection, "Buy", StringComparison.OrdinalIgnoreCase);
if ((isLong && aiSuggestsShort) || (!isLong && aiSuggestsLong))
{
newRecommendation = "Close";
reasoning =
$"Trendwende detektiert: KI empfiehlt {aiResponse.SuggestedDirection}, Trade ist aber {(isLong ? "Long" : "Short")}.";
}
else if (string.Equals(aiResponse.AiDecision, "Reject", StringComparison.OrdinalIgnoreCase))
{
newRecommendation = "Close";
reasoning = $"Risiko zu hoch: KI empfiehlt Exit. ({aiResponse.AiReasoning})";
}
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;
if (proposedSl > trade.StopLoss && trade.StopLoss > 0) newRecommendation = "AdjustSL";
}
}
else
{
// Bei Short darf der StopLoss nur NACH UNTEN angepasst werden
if (trade.StopLoss <= 0 || proposedSl < trade.StopLoss)
{
newStopLoss = proposedSl;
if (proposedSl < trade.StopLoss && trade.StopLoss > 0) newRecommendation = "AdjustSL";
}
}
}
if (aiResponse.ExecutionPlan.TakeProfitTargets != null &&
aiResponse.ExecutionPlan.TakeProfitTargets.Count > 0)
{
var proposedTp = aiResponse.ExecutionPlan.TakeProfitTargets[0];
if (proposedTp > 0 && proposedTp != trade.TakeProfit)
{
newTakeProfit = proposedTp;
if (newRecommendation == "Hold") newRecommendation = "AdjustTP";
}
}
}
await SendUpdateAsync(trade, currentPrice, newRecommendation, reasoning, newStopLoss, newTakeProfit);
}
private async Task SendUpdateAsync(TradeProposalDto trade, decimal currentPrice, string recommendation,
string reasoning, decimal? suggestedStopLoss = null, decimal? suggestedTakeProfit = null)
{
var update = new TradeHourlyUpdateDto
{
TradeId = trade.TradeId,
Recommendation = recommendation,
CurrentPrice = currentPrice,
SuggestedStopLoss = suggestedStopLoss,
SuggestedTakeProfit = suggestedTakeProfit,
VixValue = trade.VixValue,
Reasoning = reasoning,
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);
}
private static int EstimateMaxHoldingDays(string timeframe)
{
if (string.IsNullOrWhiteSpace(timeframe)) return 14; // Default
string tfLower = timeframe.ToLowerInvariant();
int multiplier = 1;
if (tfLower.Contains("woche") || tfLower.Contains("week")) multiplier = 7;
else if (tfLower.Contains("monat") || tfLower.Contains("month")) multiplier = 30;
else if (tfLower.Contains("jahr") || tfLower.Contains("year")) multiplier = 365;
var numbers = new List<int>();
string currentNum = "";
foreach (char c in timeframe)
{
if (char.IsDigit(c))
{
currentNum += c;
}
else if (currentNum.Length > 0)
{
if (int.TryParse(currentNum, out int n)) numbers.Add(n);
currentNum = "";
}
}
if (currentNum.Length > 0 && int.TryParse(currentNum, out int lastN)) numbers.Add(lastN);
int maxNum = numbers.Count > 0 ? numbers.Max() : 14;
if (maxNum == 0) maxNum = 14;
if (multiplier == 1 && maxNum < 3) maxNum = 3; // Mindestens 3 Tage Kulanz
return maxNum * multiplier;
}
}
@@ -0,0 +1,13 @@
using System.Threading;
using System.Threading.Tasks;
using FinlyticCore.Models.Analyzer;
namespace FinlyticAnalyzer.Services;
public interface IN8nEvaluationService
{
/// <summary>
/// Evaluates an asset asynchronously using N8n.
/// </summary>
Task<N8nAnalysisResponseDto?> EvaluateAssetAsync(N8nAnalysisRequestDto request, CancellationToken cancellationToken = default);
}
@@ -0,0 +1,29 @@
using FinlyticCore.Models.Analyzer;
using FinlyticCore.Dtos.News;
namespace FinlyticAnalyzer.Services;
public class FilterResult
{
public bool Passed { get; set; }
public string RejectReason { get; set; } = string.Empty;
public string Sector { get; set; } = string.Empty;
public string Symbol { get; set; } = string.Empty;
public string Isin { get; set; } = string.Empty;
public double ImpactScore { get; set; }
public double ThresholdApplied { get; set; }
public string RiskTolerance { get; set; } = "Moderate";
public string Timeframe { get; set; } = "1D";
public string InstrumentType { get; set; } = "Stock";
}
public interface IThreeLayerFilterEngine
{
/// <summary>
/// Evaluates news based on market regime and returns a filter result.
/// </summary>
FilterResult EvaluateNews(NewsArticleDto newsEvent, VixMarketRegime regime);
}
@@ -0,0 +1,28 @@
using System.Threading;
using System.Threading.Tasks;
using FinlyticCore.Models.Analyzer;
namespace FinlyticAnalyzer.Services;
public interface IVixTrackerService
{
/// <summary>
/// Gets the current VIX value.
/// </summary>
decimal GetCurrentVix();
/// <summary>
/// Gets the current market regime based on VIX.
/// </summary>
VixMarketRegime GetCurrentRegime();
/// <summary>
/// Updates the VIX tracker with a new tick value.
/// </summary>
void UpdateVixFromTick(decimal vixValue);
/// <summary>
/// Polls the VIX asynchronously and returns its value.
/// </summary>
Task<decimal> PollVixAsync(CancellationToken cancellationToken = default);
}
@@ -0,0 +1,11 @@
using FinlyticCore.Models.Analyzer;
namespace FinlyticAnalyzer.Services;
public interface IWinRateCalculator
{
/// <summary>
/// Calculates the win rate for a given sector and symbol under the specified market regime.
/// </summary>
double CalculateWinRate(string sector, string symbol, VixMarketRegime regime);
}
@@ -0,0 +1,36 @@
namespace FinlyticAnalyzer.Services;
public enum LogCategory
{
MqttHealthPing,
MqttGeneral,
AnalyzerAuto,
AnalyzerManual,
DatabaseOps,
General
}
public static class LogCategoryFilter
{
public static bool EnableLogMqttHealthPing { get; set; } = false;
public static bool EnableLogMqttGeneral { get; set; } = true;
public static bool EnableLogAnalyzerAuto { get; set; } = true;
public static bool EnableLogAnalyzerManual { get; set; } = true;
public static bool EnableLogDatabaseOps { get; set; } = true;
/// <summary>
/// Checks if a given log category is enabled.
/// </summary>
public static bool IsEnabled(LogCategory category)
{
return category switch
{
LogCategory.MqttHealthPing => EnableLogMqttHealthPing,
LogCategory.MqttGeneral => EnableLogMqttGeneral,
LogCategory.AnalyzerAuto => EnableLogAnalyzerAuto,
LogCategory.AnalyzerManual => EnableLogAnalyzerManual,
LogCategory.DatabaseOps => EnableLogDatabaseOps,
_ => true
};
}
}
@@ -0,0 +1,104 @@
using System.Text.Json;
using FinlyticCore.Models.Analyzer;
using FinlyticCore.Util;
namespace FinlyticAnalyzer.Services;
public class N8nEvaluationService : IN8nEvaluationService
{
private readonly HttpClient _httpClient;
private readonly ILogger<N8nEvaluationService> _logger;
private readonly string _webhookUrl;
public N8nEvaluationService(HttpClient httpClient, IConfiguration configuration, ILogger<N8nEvaluationService> logger)
{
_httpClient = httpClient;
_logger = logger;
_webhookUrl = configuration["N8N:WebhookUrl"] ?? configuration["N8N__WebhookUrl"] ?? "https://n8n.kleidukos.me/webhook/gemini/analysis/auto";
// Timeout auf 45 Sekunden erhöht für komplexere LLM/Gemini Chains in n8n
_httpClient.Timeout = TimeSpan.FromSeconds(45);
}
/// <summary>
/// Evaluates an asset asynchronously using N8n / Gemini workflows.
/// </summary>
public async Task<N8nAnalysisResponseDto?> EvaluateAssetAsync(N8nAnalysisRequestDto request, CancellationToken cancellationToken = default)
{
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);
// Typsichere AOT-Serialisierung verwenden
using var content = JsonContent.Create(
request,
FinlyticJsonSerializerContext.Default.N8nAnalysisRequestDto);
using var response = await _httpClient.PostAsync(_webhookUrl, content, cancellationToken);
if (response.IsSuccessStatusCode)
{
var contentStr = await response.Content.ReadAsStringAsync(cancellationToken);
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);
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(']'))
{
using var doc = JsonDocument.Parse(jsonToDeserialize);
if (doc.RootElement.ValueKind == JsonValueKind.Array && doc.RootElement.GetArrayLength() > 0)
{
jsonToDeserialize = doc.RootElement[0].GetRawText();
}
}
var responseDto = JsonSerializer.Deserialize(
jsonToDeserialize,
FinlyticJsonSerializerContext.Default.N8nAnalysisResponseDto);
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);
return responseDto;
}
}
else
{
_logger.LogWarning("[{Channel}] n8n Webhook returned HTTP {StatusCode} for Request {RequestId}",
"AnalyzerChannel", 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);
}
catch (Exception ex)
{
_logger.LogError(ex, "[{Channel}] Error calling n8n AI Evaluation Webhook for Request {RequestId}", "AnalyzerChannel", request.RequestId);
}
return null; // Signals RPC/Service failure to caller
}
private static N8nAnalysisResponseDto CreateRejectionFallback(N8nAnalysisRequestDto request, string reasoning)
{
return new N8nAnalysisResponseDto
{
RequestId = request.RequestId,
AiDecision = "Rejected",
EvalScore = 0.0,
SuggestedDirection = "NONE",
SuggestedRisk = request.UserPreferences?.RiskTolerance ?? "Moderate",
SuggestedTimeframe = request.UserPreferences?.TimeframeFormatted ?? "1D",
AiReasoning = reasoning
};
}
}
@@ -0,0 +1,113 @@
using FinlyticAnalyzer.Database;
using FinlyticAnalyzer.Entities;
using Microsoft.EntityFrameworkCore;
namespace FinlyticAnalyzer.Services;
public interface ISettingsDbService
{
/// <summary>
/// Gets the analyzer settings asynchronously.
/// </summary>
Task<AnalyzerSettingsEntity> GetSettingsAsync();
/// <summary>
/// Saves the analyzer settings asynchronously.
/// </summary>
Task<AnalyzerSettingsEntity> SaveSettingsAsync(AnalyzerSettingsEntity settings);
/// <summary>
/// Updates settings from a dictionary asynchronously.
/// </summary>
Task UpdateSettingsFromDictionaryAsync(Dictionary<string, string> dictionary);
}
public class SettingsDbService : ISettingsDbService
{
private readonly AnalyzerDbContext _context;
public SettingsDbService(AnalyzerDbContext context)
{
_context = context;
}
/// <summary>
/// Gets the analyzer settings asynchronously.
/// </summary>
public async Task<AnalyzerSettingsEntity> GetSettingsAsync()
{
var settings = await _context.Settings.AsNoTracking().FirstOrDefaultAsync();
if (settings == null)
{
settings = new AnalyzerSettingsEntity { Id = Guid.NewGuid() };
_context.Settings.Add(settings);
await _context.SaveChangesAsync();
_context.ChangeTracker.Clear();
}
return settings;
}
/// <summary>
/// Saves the analyzer settings asynchronously.
/// </summary>
public async Task<AnalyzerSettingsEntity> SaveSettingsAsync(AnalyzerSettingsEntity settings)
{
var existing = await _context.Settings.FirstOrDefaultAsync();
if (existing == null)
{
if (settings.Id == Guid.Empty) settings.Id = Guid.NewGuid();
_context.Settings.Add(settings);
}
else
{
existing.ScanCronSchedule = settings.ScanCronSchedule;
existing.MinSignalScore = settings.MinSignalScore;
existing.EnableLogMqttHealthPing = settings.EnableLogMqttHealthPing;
existing.EnableLogMqttGeneral = settings.EnableLogMqttGeneral;
existing.EnableLogAnalyzerAuto = settings.EnableLogAnalyzerAuto;
existing.EnableLogAnalyzerManual = settings.EnableLogAnalyzerManual;
existing.EnableLogDatabaseOps = settings.EnableLogDatabaseOps;
existing.UpdatedAt = settings.UpdatedAt;
_context.Settings.Update(existing);
}
await _context.SaveChangesAsync();
// Synchronize in-memory static filter values
LogCategoryFilter.EnableLogMqttHealthPing = settings.EnableLogMqttHealthPing;
LogCategoryFilter.EnableLogMqttGeneral = settings.EnableLogMqttGeneral;
LogCategoryFilter.EnableLogAnalyzerAuto = settings.EnableLogAnalyzerAuto;
LogCategoryFilter.EnableLogAnalyzerManual = settings.EnableLogAnalyzerManual;
LogCategoryFilter.EnableLogDatabaseOps = settings.EnableLogDatabaseOps;
return settings;
}
/// <summary>
/// Updates settings from a dictionary asynchronously.
/// </summary>
public async Task UpdateSettingsFromDictionaryAsync(Dictionary<string, string> dictionary)
{
var settings = await GetSettingsAsync();
foreach (var (key, value) in dictionary)
{
if (string.Equals(key, "ScanCronSchedule", StringComparison.OrdinalIgnoreCase) && !string.IsNullOrWhiteSpace(value))
settings.ScanCronSchedule = value.Trim();
else if (string.Equals(key, "MinSignalScore", StringComparison.OrdinalIgnoreCase) && double.TryParse(value, out var score))
settings.MinSignalScore = score;
else if (string.Equals(key, "EnableLog_MqttHealthPing", StringComparison.OrdinalIgnoreCase) && bool.TryParse(value, out var b1))
settings.EnableLogMqttHealthPing = b1;
else if (string.Equals(key, "EnableLog_MqttGeneral", StringComparison.OrdinalIgnoreCase) && bool.TryParse(value, out var b2))
settings.EnableLogMqttGeneral = b2;
else if (string.Equals(key, "EnableLog_AnalyzerAuto", StringComparison.OrdinalIgnoreCase) && bool.TryParse(value, out var b3))
settings.EnableLogAnalyzerAuto = b3;
else if (string.Equals(key, "EnableLog_AnalyzerManual", StringComparison.OrdinalIgnoreCase) && bool.TryParse(value, out var b4))
settings.EnableLogAnalyzerManual = b4;
else if (string.Equals(key, "EnableLog_DatabaseOps", StringComparison.OrdinalIgnoreCase) && bool.TryParse(value, out var b5))
settings.EnableLogDatabaseOps = b5;
}
settings.UpdatedAt = DateTime.UtcNow;
await SaveSettingsAsync(settings);
}
}
@@ -0,0 +1,138 @@
using System;
using System.Collections.Concurrent;
using FinlyticCore.Dtos.News;
using FinlyticCore.Models.Analyzer;
using Microsoft.Extensions.Logging;
namespace FinlyticAnalyzer.Services;
public class ThreeLayerFilterEngine : IThreeLayerFilterEngine
{
private readonly ILogger<ThreeLayerFilterEngine> _logger;
private readonly ConcurrentDictionary<string, DateTime> _seenEvents = new();
private DateTime _lastCleanupTime = DateTime.UtcNow;
public ThreeLayerFilterEngine(ILogger<ThreeLayerFilterEngine> logger)
{
_logger = logger;
}
/// <summary>
/// Evaluates news strictly based on ISIN and dynamic VIX market regime.
/// </summary>
public FilterResult EvaluateNews(NewsArticleDto newsEvent, VixMarketRegime regime)
{
var result = new FilterResult();
// -------------------------------------------------------------
// Layer 1: Relevance, ISIN & Deduplication
// -------------------------------------------------------------
if (newsEvent == null || newsEvent.Id == Guid.Empty)
{
result.Passed = false;
result.RejectReason = "Layer 1: Missing or Empty NewsArticle / EventId";
return result;
}
string eventId = newsEvent.Id.ToString();
var now = DateTime.UtcNow;
// Safely clean up dictionary every 30 minutes
if ((now - _lastCleanupTime).TotalMinutes > 30 || _seenEvents.Count > 10000)
{
CleanupSeenEvents(now);
}
// Deduplication check (keep history for 12 hours)
if (_seenEvents.TryGetValue(eventId, out var prevTime) && (now - prevTime).TotalHours < 12.0)
{
result.Passed = false;
result.RejectReason = "Layer 1: Duplicate EventId within 12h window";
return result;
}
_seenEvents[eventId] = now;
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];
isin = !string.IsNullOrWhiteSpace(firstAsset.Isin) ? firstAsset.Isin.Trim().ToUpperInvariant() : string.Empty;
assetName = !string.IsNullOrWhiteSpace(firstAsset.Name) ? firstAsset.Name.Trim() : string.Empty;
}
// Mandatory check: Must have a valid ISIN
if (string.IsNullOrWhiteSpace(isin))
{
result.Passed = false;
result.RejectReason = "Layer 1: Missing mandatory ISIN for news item";
return result;
}
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
// -------------------------------------------------------------
// Layer 2: Impact & Dynamic VIX Threshold
// -------------------------------------------------------------
double impactScore = newsEvent.Confidence ?? 0.75;
if (impactScore <= 0) impactScore = 0.75;
double requiredThreshold = regime switch
{
VixMarketRegime.LowVol => 0.55,
VixMarketRegime.Normal => 0.65,
VixMarketRegime.HighVol => 0.80,
VixMarketRegime.Panic => 0.90,
_ => 0.65
};
result.ImpactScore = impactScore;
result.ThresholdApplied = requiredThreshold;
if (impactScore < requiredThreshold)
{
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);
return result;
}
// -------------------------------------------------------------
// Layer 3: Dynamic Parameter & Risk Engine
// -------------------------------------------------------------
result.RiskTolerance = regime switch
{
VixMarketRegime.Panic => "Conservative",
VixMarketRegime.HighVol => "Moderate",
_ => "Aggressive"
};
result.Timeframe = impactScore >= 0.85 ? "4H" : "1D";
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);
return result;
}
private void CleanupSeenEvents(DateTime now)
{
_lastCleanupTime = now;
foreach (var kv in _seenEvents)
{
if ((now - kv.Value).TotalHours > 12.0)
{
_seenEvents.TryRemove(kv.Key, out _);
}
}
}
}
@@ -0,0 +1,101 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using FinlyticCore.Models.Analyzer;
using FinlyticCore.Services.Yahoo;
using Microsoft.Extensions.Logging;
namespace FinlyticAnalyzer.Services;
public class VixTrackerService : IVixTrackerService
{
private readonly YahooFinanceClient _yahooClient;
private readonly ILogger<VixTrackerService> _logger;
private decimal _currentVix = 18.5m; // Default: Normal Regime
private VixMarketRegime _currentRegime = VixMarketRegime.Normal;
private readonly object _lock = new();
public VixTrackerService(YahooFinanceClient yahooClient, ILogger<VixTrackerService> logger)
{
_yahooClient = yahooClient;
_logger = logger;
}
public decimal GetCurrentVix()
{
lock (_lock)
{
return _currentVix;
}
}
public VixMarketRegime GetCurrentRegime()
{
lock (_lock)
{
return _currentRegime;
}
}
public void UpdateVixFromTick(decimal vixValue)
{
if (vixValue <= 0m) return;
lock (_lock)
{
var oldRegime = _currentRegime;
var oldVix = _currentVix;
_currentVix = vixValue;
_currentRegime = CalculateRegime(vixValue);
if (oldRegime != _currentRegime)
{
_logger.LogWarning("[{Channel}] [VIX REGIME SHIFT] Markt-Regime gewechselt: {OldRegime} -> {NewRegime} (VIX: {Vix:F2})",
"AnalyzerChannel", oldRegime, _currentRegime, _currentVix);
}
else if (Math.Abs(oldVix - vixValue) >= 0.5m)
{
_logger.LogInformation("[{Channel}] VIX aktualisiert: {Vix:F2} (Regime: {Regime})",
"AnalyzerChannel", _currentVix, _currentRegime);
}
}
}
public async Task<decimal> PollVixAsync(CancellationToken cancellationToken = default)
{
try
{
var vix = await _yahooClient.GetLivePriceAsync("^VIX", cancellationToken);
if (vix.HasValue && vix.Value > 0m)
{
UpdateVixFromTick(vix.Value);
return vix.Value;
}
}
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());
}
return GetCurrentVix();
}
private static VixMarketRegime CalculateRegime(decimal vix)
{
return vix switch
{
< 15.0m => VixMarketRegime.LowVol,
>= 15.0m and < 20.0m => VixMarketRegime.Normal,
>= 20.0m and < 30.0m => VixMarketRegime.HighVol,
_ => VixMarketRegime.Panic
};
}
}
@@ -0,0 +1,73 @@
using System;
using System.IO;
using System.Linq;
using System.Text.Json;
using FinlyticCore.Models.Analyzer;
using FinlyticCore.Models.Trades;
using Microsoft.Extensions.Logging;
namespace FinlyticAnalyzer.Services;
public class WinRateCalculator : IWinRateCalculator
{
private readonly ILogger<WinRateCalculator> _logger;
private readonly string _feedbackDir;
public WinRateCalculator(ILogger<WinRateCalculator> logger)
{
_logger = logger;
_feedbackDir = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "data", "feedback");
if (!Directory.Exists(_feedbackDir))
{
Directory.CreateDirectory(_feedbackDir);
}
}
/// <summary>
/// Calculates the win rate for a given sector and symbol under the specified market regime.
/// </summary>
public double CalculateWinRate(string sector, string symbol, VixMarketRegime regime)
{
try
{
if (!Directory.Exists(_feedbackDir)) return 65.0;
var jsonFiles = Directory.GetFiles(_feedbackDir, "*.json", SearchOption.AllDirectories);
if (jsonFiles.Length == 0) return 65.0;
int totalTrades = 0;
int winningTrades = 0;
foreach (var file in jsonFiles)
{
var content = File.ReadAllText(file);
var records = JsonSerializer.Deserialize<TradeFeedbackRecord[]>(content);
if (records == null || records.Length == 0) continue;
var matching = records.Where(r =>
string.Equals(r.Sector, sector, StringComparison.OrdinalIgnoreCase) &&
r.VixRegime == regime).ToList();
foreach (var rec in matching)
{
totalTrades++;
if (rec.IsWin) winningTrades++;
}
}
if (totalTrades > 0)
{
double calculatedWinRate = (double)winningTrades / totalTrades * 100.0;
_logger.LogInformation("[{Channel}] Calculated win-rate for Sector '{Sector}' in Regime '{Regime}': {WinRate:F1}% ({Wins}/{Total})",
"AnalyzerChannel", sector, regime, calculatedWinRate, winningTrades, totalTrades);
return Math.Round(calculatedWinRate, 1);
}
}
catch (Exception ex)
{
_logger.LogWarning(ex, "[{Channel}] Error reading feedback files for win-rate calculation. Falling back to default.", "AnalyzerChannel");
}
return 65.0; // Default baseline win-rate
}
}
+804
View File
@@ -0,0 +1,804 @@
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.Models;
using FinlyticCore.Models.Analyzer;
using FinlyticCore.Models.Trades;
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;
/// <summary>
/// Unified Managed MQTT Client for FinlyticAnalyzer.
/// Handles event subscriptions, market screening, manual AI evaluation triggers,
/// and dispatches trade proposals via MQTT.
/// </summary>
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<AnalyzerMqttClient> _logger;
public AnalyzerMqttClient(
IConfiguration configuration,
IServiceScopeFactory scopeFactory,
IVixTrackerService vixTracker,
IThreeLayerFilterEngine filterEngine,
IWinRateCalculator winRateCalculator,
IN8nEvaluationService n8nService,
ILogger<AnalyzerMqttClient> 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("[{Channel}] Starting Unified Analyzer MQTT Client. Host: {Host}, ClientId: {ClientId}", "AnalyzerChannel", config.Host, config.ClientId);
await ConnectAsync(config);
}
public async Task StopAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("[{Channel}] Stopping Unified Analyzer MQTT Client.", "AnalyzerChannel");
await DisconnectAsync();
}
protected override async Task OnConnectedAsync()
{
_logger.LogInformation("[{Channel}] Analyzer MQTT Client connected. Subscribing to topics and RPC response channels...", "AnalyzerChannel");
// Incoming Event Topics
await SubscribeAsync("services/news/completed");
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("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/trades_Get/#");
await SubscribeAsync("services/response/tr_GetLivePrice/#");
_logger.LogInformation("[{Channel}] Successfully subscribed to all event and RPC channels.", "AnalyzerChannel");
}
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);
if (LogCategoryFilter.IsEnabled(LogCategory.MqttHealthPing))
{
_logger.LogInformation("[{Channel}] [AnalyzerMqttClient] Responded to live health_Ping RPC request [CorrelationId: {CorrelationId}].", "AnalyzerChannel", correlationId);
}
}
return;
}
if (topic.StartsWith("services/config/updated", StringComparison.OrdinalIgnoreCase))
{
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<ISettingsDbService>();
await settingsDb.UpdateSettingsFromDictionaryAsync(configUpdate.Settings);
_logger.LogInformation("[{Channel}] [AnalyzerMqttClient] Persisted {Count} updated settings to FinlyticAnalyzer database.", "AnalyzerChannel", configUpdate.Settings.Count);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "[{Channel}] [AnalyzerMqttClient] Error processing MQTT config update event.", "AnalyzerChannel");
}
}
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("finlytic/trades/closed/"))
{
await HandleClosedTradeFeedbackAsync(payloadStr);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "[{Channel}] Error processing incoming MQTT message on topic {Topic}", "AnalyzerChannel", topic);
}
}
private async Task HandleClosedTradeFeedbackAsync(string payloadStr)
{
try
{
var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
var closedDto = JsonSerializer.Deserialize<TradeProposalDto>(payloadStr, options);
if (closedDto != null && !string.IsNullOrWhiteSpace(closedDto.TradeId))
{
bool isWin = closedDto.Status.Contains("Profit", StringComparison.OrdinalIgnoreCase) ||
closedDto.Status.Contains("Win", StringComparison.OrdinalIgnoreCase);
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));
_logger.LogInformation("[{Channel}] Processed closed trade feedback for {TradeId}. Saved to {FilePath}", "AnalyzerChannel", closedDto.TradeId, filePath);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "[{Channel}] Error processing closed trade feedback.", "AnalyzerChannel");
}
}
private async Task HandleManualTriggerAsync(string correlationId, string payloadStr, CancellationToken cancellationToken)
{
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");
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);
}
using var scope = _scopeFactory.CreateScope();
var dbContext = scope.ServiceProvider.GetRequiredService<AnalyzerDbContext>();
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?.Ticker ?? manualReq.Symbol.ToUpperInvariant(),
Name = manualReq.FundamentalsData?.CompanyName ?? 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<PatternContextInfo>()
},
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?.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
}
};
var n8nResponse = await _n8nService.EvaluateAssetAsync(n8nRequest, cancellationToken);
var settingsService = scope.ServiceProvider.GetRequiredService<ISettingsDbService>();
var settings = await settingsService.GetSettingsAsync();
double minSignalScore = settings.MinSignalScore;
double confidenceScore = n8nResponse?.EvalScore > 0 ? n8nResponse.EvalScore : 0.75;
bool shouldProceed = n8nResponse != null &&
string.Equals(n8nResponse.AiDecision, "Proceed", StringComparison.OrdinalIgnoreCase) &&
(confidenceScore * 100.0) >= minSignalScore &&
winRate >= 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 = manualReq.FundamentalsData?.CompanyName ?? 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 = winRate,
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 = winRate,
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);
_logger.LogInformation("[{Channel}] [ManualAnalyzer] [DISPATCHED] Dispatched Manual Trade Proposal {AnalysisId} to topic {Topic}", "AnalyzerChannel", analysisId, propTopic);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "[{Channel}] Failed to handle manual trigger.", "AnalyzerChannel");
}
}
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, "[{Channel}] Failed to parse VIX tick message.", "AnalyzerChannel");
}
}
}
private async Task ProcessNewsMessageAsync(string payloadStr, CancellationToken cancellationToken)
{
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)
{
if (LogCategoryFilter.IsEnabled(LogCategory.AnalyzerAuto))
{
_logger.LogInformation("[{Channel}] [AutoScreener] [SKIPPED] News message skipped for ISIN '{Isin}'. Reason: {Reason}", "AnalyzerChannel", filterResult.Isin, filterResult.RejectReason);
}
return;
}
if (LogCategoryFilter.IsEnabled(LogCategory.AnalyzerAuto))
{
_logger.LogInformation("[{Channel}] [AutoScreener] [TRIGGERED] Screening market opportunity for ISIN '{Isin}'...", "AnalyzerChannel", 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;
try
{
if (IsConnected)
{
var isinReq = new IsinRequest(filterResult.Isin);
livePriceResp = await SendRpcRequestAsync<FinlyticCore.Dtos.TechnicalAnalysis.LivePriceDto, IsinRequest>(
"tr_GetLivePrice", isinReq, TimeSpan.FromSeconds(3));
taResp = await SendRpcRequestAsync<FinlyticCore.Dtos.TechnicalAnalysis.TechnicalAnalysisDto, IsinRequest>(
"ta_GetAnalysis", isinReq, TimeSpan.FromSeconds(3));
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<PatternContextInfo>()
};
}
fundResp = await SendRpcRequestAsync<FinlyticCore.Dtos.Fundamentals.AssetFundamentalsDto, IsinRequest>(
"fundamentals_Get", isinReq, TimeSpan.FromSeconds(3));
if (fundResp != null)
{
resolvedSymbol = !string.IsNullOrWhiteSpace(fundResp.Ticker) ? fundResp.Ticker : resolvedSymbol;
resolvedName = !string.IsNullOrWhiteSpace(fundResp.CompanyName) ? fundResp.CompanyName : 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
};
}
var sentResp = await SendRpcRequestAsync<FinlyticCore.Dtos.Sentiment.IsinSentimentSummaryDto, IsinRequest>(
"sentiment_GetIsin", isinReq, TimeSpan.FromSeconds(3));
if (sentResp != null)
{
sentInfo = new SentimentContextInfo
{
AssetSentimentScore = sentResp.CurrentSummary?.CompoundScore ?? 0.0,
SectorSentimentScore = 0.5,
NewsSentimentSummary = sentResp.CurrentSummary?.SentimentLabel ?? "Neutral"
};
}
}
}
catch (Exception ex)
{
_logger.LogWarning(ex, "[{Channel}] Failed to fetch context data for auto screener analysis.", "AnalyzerChannel");
}
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);
double minSignalScore = 75.0;
using (var scope = _scopeFactory.CreateScope())
{
var settingsService = scope.ServiceProvider.GetRequiredService<ISettingsDbService>();
var settings = await settingsService.GetSettingsAsync();
minSignalScore = settings.MinSignalScore;
}
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<double>();
var resistanceLevels = new List<double>();
double currentPrice = (double)(livePriceResp?.CurrentPrice > 0 ? livePriceResp.CurrentPrice : (fundResp?.CurrentPrice > 0 ? fundResp.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"
};
using (var scope = _scopeFactory.CreateScope())
{
var dbContext = scope.ServiceProvider.GetRequiredService<AnalyzerDbContext>();
var analysisEntity = new AnalysisEntity
{
AnalysisId = analysisId,
EventId = eventId,
Sector = filterResult.Sector,
Symbol = finalSymbol,
Isin = filterResult.Isin,
VixRegime = regime,
VixValue = currentVix,
ImpactScore = filterResult.ImpactScore,
WinRate = winRate,
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 = winRate,
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);
_logger.LogInformation("[{Channel}] [AutoScreener] Dispatched High-Conviction Proposal {TradeId} to topic {Topic}", "AnalyzerChannel", 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);
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);
}
}
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);
}
}
}
private static string n8nReasoning(N8nAnalysisResponseDto? resp) => resp?.AiReasoning ?? string.Empty;
}
+16
View File
@@ -0,0 +1,16 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"ConnectionStrings": {
"DefaultConnection": "Host=localhost;Database=finlytic_analyzer;Username=admin;Password=admin"
},
"MQTT": {
"Host": "localhost",
"Port": "1883",
"ClientId": "finlytic_analyzer"
}
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,12 @@
{
"epochs": [ {
"calculation_time": "13430050656556383",
"config_version": 0,
"model_version": "0",
"padded_top_topics_start_index": 0,
"taxonomy_version": 0,
"top_topics_and_observing_domains": [ ]
} ],
"hex_encoded_hmac_key": "B6F2F708445BA6FD9AE93FC13F58B9FFE00F622BAC8C7EDCF364F58F7F466A75",
"next_scheduled_calculation_time": "13430655456556985"
}
Binary file not shown.
@@ -0,0 +1 @@
MANIFEST-000001
@@ -0,0 +1,9 @@
2026/08/05-21:31:05.209 6624 Reusing MANIFEST C:\Users\larsh\AppData\Local\Temp\flutter_tools.2993d3\flutter_tools_chrome_device.c2da8a67\Default\EdgeCoupons/coupons_data.db/MANIFEST-000001
2026/08/05-21:31:05.210 6624 Recovering log #13
2026/08/05-21:31:05.211 6624 Reusing old log C:\Users\larsh\AppData\Local\Temp\flutter_tools.2993d3\flutter_tools_chrome_device.c2da8a67\Default\EdgeCoupons/coupons_data.db/000013.log
2026/08/05-21:31:05.211 6624 Delete type=0 #4
2026/08/05-21:31:05.212 6624 Delete type=0 #7
2026/08/05-21:31:05.212 6624 Delete type=0 #10
2026/08/05-21:31:25.638 2bf0 Level-0 table #17: started
2026/08/05-21:31:25.642 2bf0 Level-0 table #17: 683332 bytes OK
2026/08/05-21:31:25.644 2bf0 Delete type=0 #13
@@ -0,0 +1,8 @@
2026/08/04-22:51:57.518 4210 Reusing MANIFEST C:\Users\larsh\AppData\Local\Temp\flutter_tools.20e8229a\flutter_tools_chrome_device.7d5ee104\Default\EdgeCoupons/coupons_data.db/MANIFEST-000001
2026/08/04-22:51:57.519 4210 Recovering log #10
2026/08/04-22:51:57.519 4210 Reusing old log C:\Users\larsh\AppData\Local\Temp\flutter_tools.20e8229a\flutter_tools_chrome_device.7d5ee104\Default\EdgeCoupons/coupons_data.db/000010.log
2026/08/04-22:51:57.520 4210 Delete type=0 #4
2026/08/04-22:51:57.520 4210 Delete type=0 #7
2026/08/04-23:39:18.422 2ad8 Level-0 table #14: started
2026/08/04-23:39:18.425 2ad8 Level-0 table #14: 683332 bytes OK
2026/08/04-23:39:18.428 2ad8 Delete type=0 #10
@@ -0,0 +1,37 @@
{"logTime": "0801/093734", "session": "START"}
{"logTime": "0801/093734", "level": "INFO", "location": "chrome\\browser\\ui\\startup\\startup_browser_creator_impl.cc:352 EdgeLogLastSessionExitTypeOnStartup", "message": "Previous Session Exit Type: PreviousSessionExitType::kNormalBrowserShutDown"}
{"logTime": "0801/093734", "level": "INFO", "location": "chrome\\browser\\ui\\startup\\startup_browser_creator_impl.cc:1398 DetermineURLsAndLaunch", "message": "Startup Preference: 0"}
{"logTime": "0801/093734", "level": "INFO", "location": "chrome\\browser\\ui\\startup\\startup_browser_creator_impl.cc:1400 DetermineURLsAndLaunch", "message": "Browser Open Behavior: 0"}
{"logTime": "0801/093736", "level": "INFO", "location": "components\\sessions\\core\\command_storage_manager.cc:275 operator()", "message": "No valid session file found: SessionRestore"}
{"logTime": "0801/093736", "level": "INFO", "location": "components\\sessions\\core\\command_storage_manager.cc:275 operator()", "message": "Last session file not found for SessionType : SessionRestore"}
{"logTime": "0801/123008", "session": "END"}
{"logTime": "0801/202055", "session": "START"}
{"logTime": "0801/202055", "level": "INFO", "location": "chrome\\browser\\ui\\startup\\startup_browser_creator_impl.cc:352 EdgeLogLastSessionExitTypeOnStartup", "message": "Previous Session Exit Type: PreviousSessionExitType::kNormalBrowserShutDown"}
{"logTime": "0801/202055", "level": "INFO", "location": "chrome\\browser\\ui\\startup\\startup_browser_creator_impl.cc:1398 DetermineURLsAndLaunch", "message": "Startup Preference: 0"}
{"logTime": "0801/202055", "level": "INFO", "location": "chrome\\browser\\ui\\startup\\startup_browser_creator_impl.cc:1400 DetermineURLsAndLaunch", "message": "Browser Open Behavior: 0"}
{"logTime": "0801/202057", "level": "INFO", "location": "components\\sessions\\core\\command_storage_manager.cc:275 operator()", "message": "Valid session file found: SessionRestore"}
{"logTime": "0801/210300", "session": "END"}
{"logTime": "0803/201011", "session": "START"}
{"logTime": "0803/201011", "level": "INFO", "location": "chrome\\browser\\ui\\startup\\startup_browser_creator_impl.cc:353 EdgeLogLastSessionExitTypeOnStartup", "message": "Previous Session Exit Type: PreviousSessionExitType::kNormalBrowserShutDown"}
{"logTime": "0803/201011", "level": "INFO", "location": "chrome\\browser\\ui\\startup\\startup_browser_creator_impl.cc:1431 DetermineURLsAndLaunch", "message": "Startup Preference: 0"}
{"logTime": "0803/201011", "level": "INFO", "location": "chrome\\browser\\ui\\startup\\startup_browser_creator_impl.cc:1433 DetermineURLsAndLaunch", "message": "Browser Open Behavior: 0"}
{"logTime": "0803/201014", "level": "INFO", "location": "components\\sessions\\core\\command_storage_manager.cc:618 operator()", "message": "Valid session file found: SessionRestore"}
{"logTime": "0803/201014", "level": "INFO", "location": "components\\sessions\\core\\command_storage_manager.cc:618 operator()", "message": "Delete session file Session_13430050656644037, for SessionType SessionRestore"}
{"logTime": "0803/202718", "session": "END"}
{"logTime": "0804/205152", "session": "START"}
{"logTime": "0804/205152", "level": "INFO", "location": "chrome\\browser\\ui\\startup\\startup_browser_creator_impl.cc:353 EdgeLogLastSessionExitTypeOnStartup", "message": "Previous Session Exit Type: PreviousSessionExitType::kNormalBrowserShutDown"}
{"logTime": "0804/205152", "level": "INFO", "location": "chrome\\browser\\ui\\startup\\startup_browser_creator_impl.cc:1431 DetermineURLsAndLaunch", "message": "Startup Preference: 0"}
{"logTime": "0804/205152", "level": "INFO", "location": "chrome\\browser\\ui\\startup\\startup_browser_creator_impl.cc:1433 DetermineURLsAndLaunch", "message": "Browser Open Behavior: 0"}
{"logTime": "0804/205154", "level": "INFO", "location": "components\\sessions\\core\\command_storage_manager.cc:618 operator()", "message": "Valid session file found: SessionRestore"}
{"logTime": "0804/205154", "level": "INFO", "location": "components\\sessions\\core\\command_storage_manager.cc:618 operator()", "message": "Delete session file Session_13430089257699509, for SessionType SessionRestore"}
{"logTime": "0804/205154", "level": "INFO", "location": "components\\sessions\\core\\command_storage_manager.cc:618 operator()", "message": "Delete session file Session_13430050656644037, for SessionType SessionRestore"}
{"logTime": "0804/213942", "session": "END"}
{"logTime": "0805/193102", "session": "START"}
{"logTime": "0805/193102", "level": "INFO", "location": "chrome\\browser\\ui\\startup\\startup_browser_creator_impl.cc:353 EdgeLogLastSessionExitTypeOnStartup", "message": "Previous Session Exit Type: PreviousSessionExitType::kNormalBrowserShutDown"}
{"logTime": "0805/193102", "level": "INFO", "location": "chrome\\browser\\ui\\startup\\startup_browser_creator_impl.cc:1431 DetermineURLsAndLaunch", "message": "Startup Preference: 0"}
{"logTime": "0805/193102", "level": "INFO", "location": "chrome\\browser\\ui\\startup\\startup_browser_creator_impl.cc:1433 DetermineURLsAndLaunch", "message": "Browser Open Behavior: 0"}
{"logTime": "0805/193104", "level": "INFO", "location": "components\\sessions\\core\\command_storage_manager.cc:618 operator()", "message": "Valid session file found: SessionRestore"}
{"logTime": "0805/193104", "level": "INFO", "location": "components\\sessions\\core\\command_storage_manager.cc:618 operator()", "message": "Delete session file Session_13430261414417274, for SessionType SessionRestore"}
{"logTime": "0805/193104", "level": "INFO", "location": "components\\sessions\\core\\command_storage_manager.cc:618 operator()", "message": "Delete session file Session_13430089257699509, for SessionType SessionRestore"}
{"logTime": "0805/193104", "level": "INFO", "location": "components\\sessions\\core\\command_storage_manager.cc:618 operator()", "message": "Delete session file Session_13430050656644037, for SessionType SessionRestore"}
{"logTime": "0805/205906", "session": "END"}
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
MANIFEST-000001
@@ -0,0 +1,3 @@
2026/08/05-21:31:02.084 6350 Reusing MANIFEST C:\Users\larsh\AppData\Local\Temp\flutter_tools.2993d3\flutter_tools_chrome_device.c2da8a67\Default\Extension Rules/MANIFEST-000001
2026/08/05-21:31:02.084 6350 Recovering log #3
2026/08/05-21:31:02.085 6350 Reusing old log C:\Users\larsh\AppData\Local\Temp\flutter_tools.2993d3\flutter_tools_chrome_device.c2da8a67\Default\Extension Rules/000003.log
@@ -0,0 +1,3 @@
2026/08/04-22:51:52.185 334 Reusing MANIFEST C:\Users\larsh\AppData\Local\Temp\flutter_tools.20e8229a\flutter_tools_chrome_device.7d5ee104\Default\Extension Rules/MANIFEST-000001
2026/08/04-22:51:52.185 334 Recovering log #3
2026/08/04-22:51:52.186 334 Reusing old log C:\Users\larsh\AppData\Local\Temp\flutter_tools.20e8229a\flutter_tools_chrome_device.7d5ee104\Default\Extension Rules/000003.log
@@ -0,0 +1 @@
MANIFEST-000001
@@ -0,0 +1,3 @@
2026/08/05-21:31:02.091 6350 Reusing MANIFEST C:\Users\larsh\AppData\Local\Temp\flutter_tools.2993d3\flutter_tools_chrome_device.c2da8a67\Default\Extension Scripts/MANIFEST-000001
2026/08/05-21:31:02.093 6350 Recovering log #3
2026/08/05-21:31:02.093 6350 Reusing old log C:\Users\larsh\AppData\Local\Temp\flutter_tools.2993d3\flutter_tools_chrome_device.c2da8a67\Default\Extension Scripts/000003.log
@@ -0,0 +1,3 @@
2026/08/04-22:51:52.191 334 Reusing MANIFEST C:\Users\larsh\AppData\Local\Temp\flutter_tools.20e8229a\flutter_tools_chrome_device.7d5ee104\Default\Extension Scripts/MANIFEST-000001
2026/08/04-22:51:52.192 334 Recovering log #3
2026/08/04-22:51:52.192 334 Reusing old log C:\Users\larsh\AppData\Local\Temp\flutter_tools.20e8229a\flutter_tools_chrome_device.7d5ee104\Default\Extension Scripts/000003.log
@@ -0,0 +1 @@
MANIFEST-000001
@@ -0,0 +1,3 @@
2026/08/05-21:31:02.379 2bf0 Reusing MANIFEST C:\Users\larsh\AppData\Local\Temp\flutter_tools.2993d3\flutter_tools_chrome_device.c2da8a67\Default\Extension State/MANIFEST-000001
2026/08/05-21:31:02.379 2bf0 Recovering log #3
2026/08/05-21:31:02.380 2bf0 Reusing old log C:\Users\larsh\AppData\Local\Temp\flutter_tools.2993d3\flutter_tools_chrome_device.c2da8a67\Default\Extension State/000003.log
@@ -0,0 +1,3 @@
2026/08/04-22:51:52.441 334 Reusing MANIFEST C:\Users\larsh\AppData\Local\Temp\flutter_tools.20e8229a\flutter_tools_chrome_device.7d5ee104\Default\Extension State/MANIFEST-000001
2026/08/04-22:51:52.441 334 Recovering log #3
2026/08/04-22:51:52.441 334 Reusing old log C:\Users\larsh\AppData\Local\Temp\flutter_tools.20e8229a\flutter_tools_chrome_device.7d5ee104\Default\Extension State/000003.log

Some files were not shown because too many files have changed in this diff Show More