diff --git a/.dockerignore b/.dockerignore index cd967fc..f87fc4d 100644 --- a/.dockerignore +++ b/.dockerignore @@ -22,4 +22,17 @@ **/secrets.dev.yaml **/values.dev.yaml LICENSE -README.md \ No newline at end of file +README.md + +## Exported Docker image archives — never needed inside a build context (~2.7 GB) +Docker/ +**/*.tar + +## Flutter client — not referenced by any Dockerfile (~450 MB). +## The compiled web bundle ships via FinlyticBackend/wwwroot instead. +FinlyticApp/ +**/.dart_tool/ +**/build/ + +## dotnet publish output on the host (final stage copies from the publish stage) +**/publish/ \ No newline at end of file diff --git a/Docker/export_to_server.ps1 b/Docker/export_to_server.ps1 new file mode 100644 index 0000000..92a169a --- /dev/null +++ b/Docker/export_to_server.ps1 @@ -0,0 +1,127 @@ +<# +.SYNOPSIS + Exports all required Finlytic Docker images to .tar archives and saves/transfers them directly to the network share. +.DESCRIPTION + Checks all 9 Finlytic microservice images, verifies network path availability, + exports each image directly (or with copy) to \\SONA\appdata\finlytic\images, + and displays progress and total transferred size. +#> + +param ( + [string]$DestinationPath = "\\SONA\appdata\finlytic\images", + [switch]$BuildFirst = $false +) + +$ErrorActionPreference = "Stop" + +$images = @( + "finlyticassets", + "finlyticnews", + "finlyticfundamentals", + "finlyticsentiment", + "finlytictechnicals", + "finlyticengine", + "finlyticsimulation", + "finlyticbot", + "finlyticbackend" +) + +Write-Host "============================================================" -ForegroundColor Cyan +Write-Host " Finlytic Docker Images Export & Server Transfer" -ForegroundColor Cyan +Write-Host "============================================================" -ForegroundColor Cyan +Write-Host "Target Server Share : $DestinationPath" -ForegroundColor Yellow +Write-Host "" + +# 1. Check destination share connectivity +if (-not (Test-Path -Path $DestinationPath)) { + Write-Host "[INFO] Target directory does not exist. Attempting to create it..." -ForegroundColor Gray + try { + New-Item -ItemType Directory -Path $DestinationPath -Force | Out-Null + Write-Host "[OK] Destination folder successfully created." -ForegroundColor Green + } + catch { + Write-Host "[ERROR] Could not access or create network share: $DestinationPath" -ForegroundColor Red + Write-Host "Please make sure \\SONA is online and credentials/permissions are valid." -ForegroundColor Red + exit 1 + } +} else { + Write-Host "[OK] Target server share is accessible." -ForegroundColor Green +} + +# 2. Optional: Build images first +if ($BuildFirst) { + Write-Host "" + Write-Host "[BUILD] Building all Docker images from compose.yaml..." -ForegroundColor Cyan + docker compose -f (Join-Path $PSScriptRoot "..\compose.yaml") build + if ($LASTEXITCODE -ne 0) { + Write-Host "[ERROR] Docker build failed. Aborting export." -ForegroundColor Red + exit 1 + } +} + +Write-Host "" +Write-Host "Starting export of $($images.Count) service images..." -ForegroundColor Cyan +Write-Host "------------------------------------------------------------" -ForegroundColor Gray + +$exported = 0 +$failed = @() +$missing = @() +$stopwatch = [System.Diagnostics.Stopwatch]::StartNew() + +foreach ($img in $images) { + $ref = "$img`:latest" + $targetTar = Join-Path $DestinationPath "$img.tar" + + # Verify if image exists locally in Docker + docker image inspect $ref *> $null + if ($LASTEXITCODE -ne 0) { + Write-Host "[SKIP] Image '$ref' not found locally in Docker." -ForegroundColor Yellow + $missing += $img + continue + } + + $imgWatch = [System.Diagnostics.Stopwatch]::StartNew() + Write-Host "[EXPORT] Saving $ref -> $targetTar ... " -NoNewline -ForegroundColor White + + try { + # Export directly to network share + docker save -o $targetTar $ref + $imgWatch.Stop() + + if ($LASTEXITCODE -eq 0 -and (Test-Path $targetTar)) { + $fileSizeMB = [math]::Round((Get-Item $targetTar).Length / 1MB, 2) + Write-Host "DONE! ($fileSizeMB MB in $($imgWatch.Elapsed.ToString('mm\:ss')))" -ForegroundColor Green + $exported++ + } else { + Write-Host "FAILED!" -ForegroundColor Red + $failed += $img + } + } + catch { + Write-Host "ERROR: $_" -ForegroundColor Red + $failed += $img + } +} + +$stopwatch.Stop() + +Write-Host "------------------------------------------------------------" -ForegroundColor Gray +Write-Host "" +Write-Host "======================= SUMMARY ============================" -ForegroundColor Cyan +Write-Host "Successfully Exported : $exported / $($images.Count)" -ForegroundColor Green + +if ($missing.Count -gt 0) { + Write-Host "Missing locally : $($missing -join ', ')" -ForegroundColor Yellow + Write-Host " -> Tip: Run 'docker compose build' to build all images." -ForegroundColor DarkGray +} + +if ($failed.Count -gt 0) { + Write-Host "Failed to Export : $($failed -join ', ')" -ForegroundColor Red +} + +Write-Host "Total Duration : $($stopwatch.Elapsed.ToString('mm\:ss'))" -ForegroundColor Cyan +Write-Host "============================================================" -ForegroundColor Cyan +Write-Host "" +Write-Host "To load these images on your server, run on the server:" -ForegroundColor White +Write-Host ' for f in /pfad/zu/appdata/finlytic/images/*.tar; do docker load -i "$f"; done' -ForegroundColor Yellow +Write-Host "" diff --git a/Finlytic.sln b/Finlytic.sln index 04af16c..fd3b9a7 100644 --- a/Finlytic.sln +++ b/Finlytic.sln @@ -15,15 +15,19 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FinlyticFundamentals", "Fin 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}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FinlyticTechnicals", "FinlyticTechnicals\FinlyticTechnicals.csproj", "{A1C82F63-4482-4E99-9231-1184FA2E001F}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FinlyticBackend", "FinlyticBackend\FinlyticBackend.csproj", "{C1A924B8-904E-436D-B07E-4E621F51C1AA}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FinlyticBot", "FinlyticBot\FinlyticBot.csproj", "{B39B0AD9-FB8A-4F5F-8652-73C3BD8E75F3}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FinlyticEngine", "FinlyticEngine\FinlyticEngine.csproj", "{8112DE84-695D-489B-9568-C531B34C63F8}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FinlyticSimulation", "FinlyticSimulation\FinlyticSimulation.csproj", "{1407B23D-3B7F-4673-9548-AA2AFF2D8011}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FinlyticBot", "FinlyticBot\FinlyticBot.csproj", "{E871DD92-5450-43D8-A730-D2CA1F0B6CE3}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FinlyticEngine.Tests", "FinlyticEngine.Tests\FinlyticEngine.Tests.csproj", "{20ADD67C-EC26-4195-9DD0-0B661BE9A4AE}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FinlyticBot.Tests", "FinlyticBot.Tests\FinlyticBot.Tests.csproj", "{1E282E4D-C63E-49E6-879D-DDEEDA530E47}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -97,30 +101,80 @@ Global {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 + {A1C82F63-4482-4E99-9231-1184FA2E001F}.Debug|x64.ActiveCfg = Debug|Any CPU + {A1C82F63-4482-4E99-9231-1184FA2E001F}.Debug|x64.Build.0 = Debug|Any CPU + {A1C82F63-4482-4E99-9231-1184FA2E001F}.Debug|x86.ActiveCfg = Debug|Any CPU + {A1C82F63-4482-4E99-9231-1184FA2E001F}.Debug|x86.Build.0 = Debug|Any CPU + {A1C82F63-4482-4E99-9231-1184FA2E001F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A1C82F63-4482-4E99-9231-1184FA2E001F}.Release|Any CPU.Build.0 = Release|Any CPU + {A1C82F63-4482-4E99-9231-1184FA2E001F}.Release|x64.ActiveCfg = Release|Any CPU + {A1C82F63-4482-4E99-9231-1184FA2E001F}.Release|x64.Build.0 = Release|Any CPU + {A1C82F63-4482-4E99-9231-1184FA2E001F}.Release|x86.ActiveCfg = Release|Any CPU + {A1C82F63-4482-4E99-9231-1184FA2E001F}.Release|x86.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 - {B39B0AD9-FB8A-4F5F-8652-73C3BD8E75F3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {B39B0AD9-FB8A-4F5F-8652-73C3BD8E75F3}.Debug|Any CPU.Build.0 = Debug|Any CPU - {B39B0AD9-FB8A-4F5F-8652-73C3BD8E75F3}.Debug|x64.ActiveCfg = Debug|Any CPU - {B39B0AD9-FB8A-4F5F-8652-73C3BD8E75F3}.Debug|x64.Build.0 = Debug|Any CPU - {B39B0AD9-FB8A-4F5F-8652-73C3BD8E75F3}.Debug|x86.ActiveCfg = Debug|Any CPU - {B39B0AD9-FB8A-4F5F-8652-73C3BD8E75F3}.Debug|x86.Build.0 = Debug|Any CPU - {B39B0AD9-FB8A-4F5F-8652-73C3BD8E75F3}.Release|Any CPU.ActiveCfg = Release|Any CPU - {B39B0AD9-FB8A-4F5F-8652-73C3BD8E75F3}.Release|Any CPU.Build.0 = Release|Any CPU - {B39B0AD9-FB8A-4F5F-8652-73C3BD8E75F3}.Release|x64.ActiveCfg = Release|Any CPU - {B39B0AD9-FB8A-4F5F-8652-73C3BD8E75F3}.Release|x64.Build.0 = Release|Any CPU - {B39B0AD9-FB8A-4F5F-8652-73C3BD8E75F3}.Release|x86.ActiveCfg = Release|Any CPU - {B39B0AD9-FB8A-4F5F-8652-73C3BD8E75F3}.Release|x86.Build.0 = Release|Any CPU + {8112DE84-695D-489B-9568-C531B34C63F8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8112DE84-695D-489B-9568-C531B34C63F8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8112DE84-695D-489B-9568-C531B34C63F8}.Debug|x64.ActiveCfg = Debug|Any CPU + {8112DE84-695D-489B-9568-C531B34C63F8}.Debug|x64.Build.0 = Debug|Any CPU + {8112DE84-695D-489B-9568-C531B34C63F8}.Debug|x86.ActiveCfg = Debug|Any CPU + {8112DE84-695D-489B-9568-C531B34C63F8}.Debug|x86.Build.0 = Debug|Any CPU + {8112DE84-695D-489B-9568-C531B34C63F8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8112DE84-695D-489B-9568-C531B34C63F8}.Release|Any CPU.Build.0 = Release|Any CPU + {8112DE84-695D-489B-9568-C531B34C63F8}.Release|x64.ActiveCfg = Release|Any CPU + {8112DE84-695D-489B-9568-C531B34C63F8}.Release|x64.Build.0 = Release|Any CPU + {8112DE84-695D-489B-9568-C531B34C63F8}.Release|x86.ActiveCfg = Release|Any CPU + {8112DE84-695D-489B-9568-C531B34C63F8}.Release|x86.Build.0 = Release|Any CPU + {1407B23D-3B7F-4673-9548-AA2AFF2D8011}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1407B23D-3B7F-4673-9548-AA2AFF2D8011}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1407B23D-3B7F-4673-9548-AA2AFF2D8011}.Debug|x64.ActiveCfg = Debug|Any CPU + {1407B23D-3B7F-4673-9548-AA2AFF2D8011}.Debug|x64.Build.0 = Debug|Any CPU + {1407B23D-3B7F-4673-9548-AA2AFF2D8011}.Debug|x86.ActiveCfg = Debug|Any CPU + {1407B23D-3B7F-4673-9548-AA2AFF2D8011}.Debug|x86.Build.0 = Debug|Any CPU + {1407B23D-3B7F-4673-9548-AA2AFF2D8011}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1407B23D-3B7F-4673-9548-AA2AFF2D8011}.Release|Any CPU.Build.0 = Release|Any CPU + {1407B23D-3B7F-4673-9548-AA2AFF2D8011}.Release|x64.ActiveCfg = Release|Any CPU + {1407B23D-3B7F-4673-9548-AA2AFF2D8011}.Release|x64.Build.0 = Release|Any CPU + {1407B23D-3B7F-4673-9548-AA2AFF2D8011}.Release|x86.ActiveCfg = Release|Any CPU + {1407B23D-3B7F-4673-9548-AA2AFF2D8011}.Release|x86.Build.0 = Release|Any CPU + {E871DD92-5450-43D8-A730-D2CA1F0B6CE3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E871DD92-5450-43D8-A730-D2CA1F0B6CE3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E871DD92-5450-43D8-A730-D2CA1F0B6CE3}.Debug|x64.ActiveCfg = Debug|Any CPU + {E871DD92-5450-43D8-A730-D2CA1F0B6CE3}.Debug|x64.Build.0 = Debug|Any CPU + {E871DD92-5450-43D8-A730-D2CA1F0B6CE3}.Debug|x86.ActiveCfg = Debug|Any CPU + {E871DD92-5450-43D8-A730-D2CA1F0B6CE3}.Debug|x86.Build.0 = Debug|Any CPU + {E871DD92-5450-43D8-A730-D2CA1F0B6CE3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E871DD92-5450-43D8-A730-D2CA1F0B6CE3}.Release|Any CPU.Build.0 = Release|Any CPU + {E871DD92-5450-43D8-A730-D2CA1F0B6CE3}.Release|x64.ActiveCfg = Release|Any CPU + {E871DD92-5450-43D8-A730-D2CA1F0B6CE3}.Release|x64.Build.0 = Release|Any CPU + {E871DD92-5450-43D8-A730-D2CA1F0B6CE3}.Release|x86.ActiveCfg = Release|Any CPU + {E871DD92-5450-43D8-A730-D2CA1F0B6CE3}.Release|x86.Build.0 = Release|Any CPU + {20ADD67C-EC26-4195-9DD0-0B661BE9A4AE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {20ADD67C-EC26-4195-9DD0-0B661BE9A4AE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {20ADD67C-EC26-4195-9DD0-0B661BE9A4AE}.Debug|x64.ActiveCfg = Debug|Any CPU + {20ADD67C-EC26-4195-9DD0-0B661BE9A4AE}.Debug|x64.Build.0 = Debug|Any CPU + {20ADD67C-EC26-4195-9DD0-0B661BE9A4AE}.Debug|x86.ActiveCfg = Debug|Any CPU + {20ADD67C-EC26-4195-9DD0-0B661BE9A4AE}.Debug|x86.Build.0 = Debug|Any CPU + {20ADD67C-EC26-4195-9DD0-0B661BE9A4AE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {20ADD67C-EC26-4195-9DD0-0B661BE9A4AE}.Release|Any CPU.Build.0 = Release|Any CPU + {20ADD67C-EC26-4195-9DD0-0B661BE9A4AE}.Release|x64.ActiveCfg = Release|Any CPU + {20ADD67C-EC26-4195-9DD0-0B661BE9A4AE}.Release|x64.Build.0 = Release|Any CPU + {20ADD67C-EC26-4195-9DD0-0B661BE9A4AE}.Release|x86.ActiveCfg = Release|Any CPU + {20ADD67C-EC26-4195-9DD0-0B661BE9A4AE}.Release|x86.Build.0 = Release|Any CPU + {1E282E4D-C63E-49E6-879D-DDEEDA530E47}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1E282E4D-C63E-49E6-879D-DDEEDA530E47}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1E282E4D-C63E-49E6-879D-DDEEDA530E47}.Debug|x64.ActiveCfg = Debug|Any CPU + {1E282E4D-C63E-49E6-879D-DDEEDA530E47}.Debug|x64.Build.0 = Debug|Any CPU + {1E282E4D-C63E-49E6-879D-DDEEDA530E47}.Debug|x86.ActiveCfg = Debug|Any CPU + {1E282E4D-C63E-49E6-879D-DDEEDA530E47}.Debug|x86.Build.0 = Debug|Any CPU + {1E282E4D-C63E-49E6-879D-DDEEDA530E47}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1E282E4D-C63E-49E6-879D-DDEEDA530E47}.Release|Any CPU.Build.0 = Release|Any CPU + {1E282E4D-C63E-49E6-879D-DDEEDA530E47}.Release|x64.ActiveCfg = Release|Any CPU + {1E282E4D-C63E-49E6-879D-DDEEDA530E47}.Release|x64.Build.0 = Release|Any CPU + {1E282E4D-C63E-49E6-879D-DDEEDA530E47}.Release|x86.ActiveCfg = Release|Any CPU + {1E282E4D-C63E-49E6-879D-DDEEDA530E47}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/FinlyticAnalyzer/Controllers/ManualAnalysisController.cs b/FinlyticAnalyzer/Controllers/ManualAnalysisController.cs deleted file mode 100644 index a05ae4a..0000000 --- a/FinlyticAnalyzer/Controllers/ManualAnalysisController.cs +++ /dev/null @@ -1,211 +0,0 @@ -using System; -using System.Text.Json; -using System.Threading; -using System.Threading.Tasks; -using FinlyticAnalyzer.Database; -using FinlyticAnalyzer.Entities; -using FinlyticAnalyzer.Services; -using FinlyticAnalyzer.Util; -using FinlyticCore.Models.Analyzer; -using FinlyticCore.Models.Trades; -using FinlyticCore.Services; -using Microsoft.AspNetCore.Mvc; -using Microsoft.EntityFrameworkCore; - -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 IFinlyticLogger _finlyticLogger; - - public ManualAnalysisController( - IVixTrackerService vixTracker, - IN8nEvaluationService n8nService, - IWinRateCalculator winRateCalculator, - AnalyzerDbContext dbContext, - IFinlyticLogger finlyticLogger) - { - _vixTracker = vixTracker; - _n8nService = n8nService; - _winRateCalculator = winRateCalculator; - _dbContext = dbContext; - _finlyticLogger = finlyticLogger; - } - - /// - /// Runs a manual analysis based on the provided request. - /// - [HttpPost("manual")] - public async Task 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 = 0, - AssetWinRate = winRate, - AvgReturnPercent = 0.0, - LastTradeResult = "UNKNOWN" - } - }; - - var n8nResponse = await _n8nService.EvaluateAssetAsync(n8nRequest, cancellationToken); - bool shouldProceed = n8nResponse != null && string.Equals(n8nResponse.AiDecision, "Proceed", StringComparison.OrdinalIgnoreCase); - - double dynamicWinRate = _winRateCalculator.CalculateDynamicWinRate( - request.Sector, - request.Symbol, - regime, - n8nEvalScore: n8nResponse?.EvalScore, - signalType: n8nResponse?.SuggestedDirection ?? "BUY"); - - TradeProposalDto? proposal = null; - if (shouldProceed && n8nResponse != null) - { - proposal = new TradeProposalDto - { - TradeId = "PROP-" + Guid.NewGuid().ToString("N")[..10].ToUpperInvariant(), - 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 = dynamicWinRate, - VixRegime = regime, - VixValue = currentVix, - TtlMinutes = 60, - Reasoning = $"Manual n8n Evaluation ({n8nResponse.AiDecision}): {n8nResponse.AiReasoning}", - StopLoss = n8nResponse.ExecutionPlan?.StopLoss ?? 0, - TakeProfit = n8nResponse.ExecutionPlan?.TakeProfitTargets != null && n8nResponse.ExecutionPlan.TakeProfitTargets.Count > 0 ? n8nResponse.ExecutionPlan.TakeProfitTargets[0] : 0, - EntryZoneMin = n8nResponse.ExecutionPlan?.EntryZone?.Min, - EntryZoneMax = n8nResponse.ExecutionPlan?.EntryZone?.Max, - TakeProfitTargets = n8nResponse.ExecutionPlan?.TakeProfitTargets, - RiskRewardRatio = n8nResponse.ExecutionPlan?.RiskRewardRatio, - MaxLeverage = n8nResponse.ExecutionPlan?.MaxLeverage, - TechnicalRationale = n8nResponse.DetailedAnalysis?.TechnicalRationale ?? string.Empty, - FundamentalRationale = n8nResponse.DetailedAnalysis?.FundamentalRationale ?? string.Empty, - RiskWarning = n8nResponse.DetailedAnalysis?.RiskWarning ?? string.Empty, - CreatedAt = DateTime.UtcNow - }; - } - - var analysisEntity = new AnalysisEntity - { - AnalysisId = analysisId, - EventId = analysisId, - Sector = request.Sector, - Symbol = request.Symbol.ToUpperInvariant(), - Isin = request.Isin.ToUpperInvariant(), - VixRegime = regime, - VixValue = currentVix, - ImpactScore = 1.0, - WinRate = dynamicWinRate, - 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); - - await _finlyticLogger.LogInfoAsync(SettingKeys.AnalyzerChannel, "[ManualAnalysisController] Manual analysis completed for {Symbol} (TradeProposed: {Proposed})", request.Symbol, shouldProceed); - - 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 - }); - } -} diff --git a/FinlyticAnalyzer/Database/AnalyzerDbContext.cs b/FinlyticAnalyzer/Database/AnalyzerDbContext.cs deleted file mode 100644 index 087e2c1..0000000 --- a/FinlyticAnalyzer/Database/AnalyzerDbContext.cs +++ /dev/null @@ -1,53 +0,0 @@ -using FinlyticAnalyzer.Entities; -using FinlyticCore.Database; -using FinlyticCore.Entities.Settings; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Design; - -namespace FinlyticAnalyzer.Database; - -public class AnalyzerDbContext : DbContext, ISettingsDbContext -{ - public AnalyzerDbContext(DbContextOptions options) : base(options) { } - - public DbSet DynamicSettings => Set(); - public DbSet Analyses => Set(); - public DbSet Settings => Set(); - public DbSet TradeProposals => Set(); - - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - base.OnModelCreating(modelBuilder); - - modelBuilder.Entity(entity => - { - entity.HasKey(e => e.Id); - entity.HasIndex(e => e.Key).IsUnique(); - }); - - modelBuilder.Entity(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(entity => - { - entity.HasIndex(e => e.Isin); - entity.HasIndex(e => e.ExpiresAt); - }); - } -} - -public class AnalyzerDbContextFactory : IDesignTimeDbContextFactory -{ - public AnalyzerDbContext CreateDbContext(string[] args) - { - var optionsBuilder = new DbContextOptionsBuilder(); - optionsBuilder.UseNpgsql("Host=localhost;Database=analyzer;Username=postgres;Password=postgres"); - return new AnalyzerDbContext(optionsBuilder.Options); - } -} diff --git a/FinlyticAnalyzer/Dockerfile b/FinlyticAnalyzer/Dockerfile deleted file mode 100644 index c221872..0000000 --- a/FinlyticAnalyzer/Dockerfile +++ /dev/null @@ -1,16 +0,0 @@ -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"] diff --git a/FinlyticAnalyzer/Entities/AnalysisEntity.cs b/FinlyticAnalyzer/Entities/AnalysisEntity.cs deleted file mode 100644 index 17fa617..0000000 --- a/FinlyticAnalyzer/Entities/AnalysisEntity.cs +++ /dev/null @@ -1,60 +0,0 @@ -using System; -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; -using FinlyticCore.Models.Analyzer; - -namespace FinlyticAnalyzer.Entities; - -/// -/// Persisted raw news, market context, AI prompt payload & response in PostgreSQL. -/// -[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; -} diff --git a/FinlyticAnalyzer/Entities/AnalyzerSettingsEntity.cs b/FinlyticAnalyzer/Entities/AnalyzerSettingsEntity.cs deleted file mode 100644 index 32196dd..0000000 --- a/FinlyticAnalyzer/Entities/AnalyzerSettingsEntity.cs +++ /dev/null @@ -1,21 +0,0 @@ -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; -} diff --git a/FinlyticAnalyzer/Entities/TradeProposalEntity.cs b/FinlyticAnalyzer/Entities/TradeProposalEntity.cs deleted file mode 100644 index 657ec4d..0000000 --- a/FinlyticAnalyzer/Entities/TradeProposalEntity.cs +++ /dev/null @@ -1,95 +0,0 @@ -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; - - /// - /// KI-Entscheidung ("BUY", "SELL", "HOLD", "REJECTED") - /// - [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); -} \ No newline at end of file diff --git a/FinlyticAnalyzer/FinlyticAnalyzer.csproj b/FinlyticAnalyzer/FinlyticAnalyzer.csproj deleted file mode 100644 index fc1d158..0000000 --- a/FinlyticAnalyzer/FinlyticAnalyzer.csproj +++ /dev/null @@ -1,26 +0,0 @@ - - - - Exe - net10.0 - enable - enable - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - - - - - diff --git a/FinlyticAnalyzer/Migrations/20260801073402_Init.Designer.cs b/FinlyticAnalyzer/Migrations/20260801073402_Init.Designer.cs deleted file mode 100644 index caea4df..0000000 --- a/FinlyticAnalyzer/Migrations/20260801073402_Init.Designer.cs +++ /dev/null @@ -1,136 +0,0 @@ -// -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 - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "10.0.9") - .HasAnnotation("Relational:MaxIdentifierLength", 63); - - NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - - modelBuilder.Entity("FinlyticAnalyzer.Entities.AnalysisEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("AiOutputJson") - .IsRequired() - .HasColumnType("jsonb"); - - b.Property("AnalysisId") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("EventId") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.Property("ImpactScore") - .HasColumnType("double precision"); - - b.Property("IsTradeProposed") - .HasColumnType("boolean"); - - b.Property("Isin") - .IsRequired() - .HasMaxLength(30) - .HasColumnType("character varying(30)"); - - b.Property("N8nDecision") - .IsRequired() - .HasMaxLength(30) - .HasColumnType("character varying(30)"); - - b.Property("N8nEvalScore") - .HasColumnType("double precision"); - - b.Property("N8nResponseJson") - .IsRequired() - .HasColumnType("jsonb"); - - b.Property("RawDataJson") - .IsRequired() - .HasColumnType("jsonb"); - - b.Property("Sector") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("character varying(50)"); - - b.Property("Symbol") - .IsRequired() - .HasMaxLength(30) - .HasColumnType("character varying(30)"); - - b.Property("VixRegime") - .HasColumnType("integer"); - - b.Property("VixValue") - .HasColumnType("numeric"); - - b.Property("WinRate") - .HasColumnType("double precision"); - - b.HasKey("Id"); - - b.HasIndex("AnalysisId") - .IsUnique(); - - b.HasIndex("CreatedAt"); - - b.HasIndex("EventId"); - - b.HasIndex("Isin"); - - b.HasIndex("Sector"); - - b.ToTable("analyses"); - }); - - modelBuilder.Entity("FinlyticAnalyzer.Entities.AnalyzerSettingsEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("MinSignalScore") - .HasColumnType("double precision"); - - b.Property("ScanCronSchedule") - .IsRequired() - .HasColumnType("text"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.ToTable("Settings"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/FinlyticAnalyzer/Migrations/20260801073402_Init.cs b/FinlyticAnalyzer/Migrations/20260801073402_Init.cs deleted file mode 100644 index ebdcc08..0000000 --- a/FinlyticAnalyzer/Migrations/20260801073402_Init.cs +++ /dev/null @@ -1,92 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace FinlyticAnalyzer.Migrations -{ - /// - public partial class Init : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "analyses", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - AnalysisId = table.Column(type: "character varying(100)", maxLength: 100, nullable: false), - EventId = table.Column(type: "character varying(100)", maxLength: 100, nullable: false), - Sector = table.Column(type: "character varying(50)", maxLength: 50, nullable: false), - Symbol = table.Column(type: "character varying(30)", maxLength: 30, nullable: false), - Isin = table.Column(type: "character varying(30)", maxLength: 30, nullable: false), - VixRegime = table.Column(type: "integer", nullable: false), - VixValue = table.Column(type: "numeric", nullable: false), - ImpactScore = table.Column(type: "double precision", nullable: false), - WinRate = table.Column(type: "double precision", nullable: false), - RawDataJson = table.Column(type: "jsonb", nullable: false), - AiOutputJson = table.Column(type: "jsonb", nullable: false), - N8nResponseJson = table.Column(type: "jsonb", nullable: false), - N8nEvalScore = table.Column(type: "double precision", nullable: false), - N8nDecision = table.Column(type: "character varying(30)", maxLength: 30, nullable: false), - IsTradeProposed = table.Column(type: "boolean", nullable: false), - CreatedAt = table.Column(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(type: "uuid", nullable: false), - ScanCronSchedule = table.Column(type: "text", nullable: false), - MinSignalScore = table.Column(type: "double precision", nullable: false), - UpdatedAt = table.Column(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"); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "analyses"); - - migrationBuilder.DropTable( - name: "Settings"); - } - } -} diff --git a/FinlyticAnalyzer/Migrations/20260803185020_AddLogFilterSettings.Designer.cs b/FinlyticAnalyzer/Migrations/20260803185020_AddLogFilterSettings.Designer.cs deleted file mode 100644 index da66b74..0000000 --- a/FinlyticAnalyzer/Migrations/20260803185020_AddLogFilterSettings.Designer.cs +++ /dev/null @@ -1,151 +0,0 @@ -// -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 - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "10.0.9") - .HasAnnotation("Relational:MaxIdentifierLength", 63); - - NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - - modelBuilder.Entity("FinlyticAnalyzer.Entities.AnalysisEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("AiOutputJson") - .IsRequired() - .HasColumnType("jsonb"); - - b.Property("AnalysisId") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("EventId") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.Property("ImpactScore") - .HasColumnType("double precision"); - - b.Property("IsTradeProposed") - .HasColumnType("boolean"); - - b.Property("Isin") - .IsRequired() - .HasMaxLength(30) - .HasColumnType("character varying(30)"); - - b.Property("N8nDecision") - .IsRequired() - .HasMaxLength(30) - .HasColumnType("character varying(30)"); - - b.Property("N8nEvalScore") - .HasColumnType("double precision"); - - b.Property("N8nResponseJson") - .IsRequired() - .HasColumnType("jsonb"); - - b.Property("RawDataJson") - .IsRequired() - .HasColumnType("jsonb"); - - b.Property("Sector") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("character varying(50)"); - - b.Property("Symbol") - .IsRequired() - .HasMaxLength(30) - .HasColumnType("character varying(30)"); - - b.Property("VixRegime") - .HasColumnType("integer"); - - b.Property("VixValue") - .HasColumnType("numeric"); - - b.Property("WinRate") - .HasColumnType("double precision"); - - b.HasKey("Id"); - - b.HasIndex("AnalysisId") - .IsUnique(); - - b.HasIndex("CreatedAt"); - - b.HasIndex("EventId"); - - b.HasIndex("Isin"); - - b.HasIndex("Sector"); - - b.ToTable("analyses"); - }); - - modelBuilder.Entity("FinlyticAnalyzer.Entities.AnalyzerSettingsEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("EnableLogAnalyzerAuto") - .HasColumnType("boolean"); - - b.Property("EnableLogAnalyzerManual") - .HasColumnType("boolean"); - - b.Property("EnableLogDatabaseOps") - .HasColumnType("boolean"); - - b.Property("EnableLogMqttGeneral") - .HasColumnType("boolean"); - - b.Property("EnableLogMqttHealthPing") - .HasColumnType("boolean"); - - b.Property("MinSignalScore") - .HasColumnType("double precision"); - - b.Property("ScanCronSchedule") - .IsRequired() - .HasColumnType("text"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.ToTable("Settings"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/FinlyticAnalyzer/Migrations/20260803185020_AddLogFilterSettings.cs b/FinlyticAnalyzer/Migrations/20260803185020_AddLogFilterSettings.cs deleted file mode 100644 index 2dfe8d0..0000000 --- a/FinlyticAnalyzer/Migrations/20260803185020_AddLogFilterSettings.cs +++ /dev/null @@ -1,73 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace FinlyticAnalyzer.Migrations -{ - /// - public partial class AddLogFilterSettings : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "EnableLogAnalyzerAuto", - table: "Settings", - type: "boolean", - nullable: false, - defaultValue: false); - - migrationBuilder.AddColumn( - name: "EnableLogAnalyzerManual", - table: "Settings", - type: "boolean", - nullable: false, - defaultValue: false); - - migrationBuilder.AddColumn( - name: "EnableLogDatabaseOps", - table: "Settings", - type: "boolean", - nullable: false, - defaultValue: false); - - migrationBuilder.AddColumn( - name: "EnableLogMqttGeneral", - table: "Settings", - type: "boolean", - nullable: false, - defaultValue: false); - - migrationBuilder.AddColumn( - name: "EnableLogMqttHealthPing", - table: "Settings", - type: "boolean", - nullable: false, - defaultValue: false); - } - - /// - 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"); - } - } -} diff --git a/FinlyticAnalyzer/Migrations/20260804184350_CheckPendingMigrations.Designer.cs b/FinlyticAnalyzer/Migrations/20260804184350_CheckPendingMigrations.Designer.cs deleted file mode 100644 index 4f45749..0000000 --- a/FinlyticAnalyzer/Migrations/20260804184350_CheckPendingMigrations.Designer.cs +++ /dev/null @@ -1,151 +0,0 @@ -// -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 - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "10.0.9") - .HasAnnotation("Relational:MaxIdentifierLength", 63); - - NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - - modelBuilder.Entity("FinlyticAnalyzer.Entities.AnalysisEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("AiOutputJson") - .IsRequired() - .HasColumnType("jsonb"); - - b.Property("AnalysisId") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("EventId") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.Property("ImpactScore") - .HasColumnType("double precision"); - - b.Property("IsTradeProposed") - .HasColumnType("boolean"); - - b.Property("Isin") - .IsRequired() - .HasMaxLength(30) - .HasColumnType("character varying(30)"); - - b.Property("N8nDecision") - .IsRequired() - .HasMaxLength(30) - .HasColumnType("character varying(30)"); - - b.Property("N8nEvalScore") - .HasColumnType("double precision"); - - b.Property("N8nResponseJson") - .IsRequired() - .HasColumnType("jsonb"); - - b.Property("RawDataJson") - .IsRequired() - .HasColumnType("jsonb"); - - b.Property("Sector") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("character varying(50)"); - - b.Property("Symbol") - .IsRequired() - .HasMaxLength(30) - .HasColumnType("character varying(30)"); - - b.Property("VixRegime") - .HasColumnType("integer"); - - b.Property("VixValue") - .HasColumnType("numeric"); - - b.Property("WinRate") - .HasColumnType("double precision"); - - b.HasKey("Id"); - - b.HasIndex("AnalysisId") - .IsUnique(); - - b.HasIndex("CreatedAt"); - - b.HasIndex("EventId"); - - b.HasIndex("Isin"); - - b.HasIndex("Sector"); - - b.ToTable("analyses"); - }); - - modelBuilder.Entity("FinlyticAnalyzer.Entities.AnalyzerSettingsEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("EnableLogAnalyzerAuto") - .HasColumnType("boolean"); - - b.Property("EnableLogAnalyzerManual") - .HasColumnType("boolean"); - - b.Property("EnableLogDatabaseOps") - .HasColumnType("boolean"); - - b.Property("EnableLogMqttGeneral") - .HasColumnType("boolean"); - - b.Property("EnableLogMqttHealthPing") - .HasColumnType("boolean"); - - b.Property("MinSignalScore") - .HasColumnType("double precision"); - - b.Property("ScanCronSchedule") - .IsRequired() - .HasColumnType("text"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.ToTable("Settings"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/FinlyticAnalyzer/Migrations/20260804184350_CheckPendingMigrations.cs b/FinlyticAnalyzer/Migrations/20260804184350_CheckPendingMigrations.cs deleted file mode 100644 index 780e3ea..0000000 --- a/FinlyticAnalyzer/Migrations/20260804184350_CheckPendingMigrations.cs +++ /dev/null @@ -1,22 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace FinlyticAnalyzer.Migrations -{ - /// - public partial class CheckPendingMigrations : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - - } - } -} diff --git a/FinlyticAnalyzer/Migrations/20260805184638_AddTradeProposals.Designer.cs b/FinlyticAnalyzer/Migrations/20260805184638_AddTradeProposals.Designer.cs deleted file mode 100644 index d9cddd8..0000000 --- a/FinlyticAnalyzer/Migrations/20260805184638_AddTradeProposals.Designer.cs +++ /dev/null @@ -1,194 +0,0 @@ -// -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 - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "10.0.9") - .HasAnnotation("Relational:MaxIdentifierLength", 63); - - NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - - modelBuilder.Entity("FinlyticAnalyzer.Entities.AnalysisEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("AiOutputJson") - .IsRequired() - .HasColumnType("jsonb"); - - b.Property("AnalysisId") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("EventId") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.Property("ImpactScore") - .HasColumnType("double precision"); - - b.Property("IsTradeProposed") - .HasColumnType("boolean"); - - b.Property("Isin") - .IsRequired() - .HasMaxLength(30) - .HasColumnType("character varying(30)"); - - b.Property("N8nDecision") - .IsRequired() - .HasMaxLength(30) - .HasColumnType("character varying(30)"); - - b.Property("N8nEvalScore") - .HasColumnType("double precision"); - - b.Property("N8nResponseJson") - .IsRequired() - .HasColumnType("jsonb"); - - b.Property("RawDataJson") - .IsRequired() - .HasColumnType("jsonb"); - - b.Property("Sector") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("character varying(50)"); - - b.Property("Symbol") - .IsRequired() - .HasMaxLength(30) - .HasColumnType("character varying(30)"); - - b.Property("VixRegime") - .HasColumnType("integer"); - - b.Property("VixValue") - .HasColumnType("numeric"); - - b.Property("WinRate") - .HasColumnType("double precision"); - - b.HasKey("Id"); - - b.HasIndex("AnalysisId") - .IsUnique(); - - b.HasIndex("CreatedAt"); - - b.HasIndex("EventId"); - - b.HasIndex("Isin"); - - b.HasIndex("Sector"); - - b.ToTable("analyses"); - }); - - modelBuilder.Entity("FinlyticAnalyzer.Entities.AnalyzerSettingsEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("EnableLogAnalyzerAuto") - .HasColumnType("boolean"); - - b.Property("EnableLogAnalyzerManual") - .HasColumnType("boolean"); - - b.Property("EnableLogDatabaseOps") - .HasColumnType("boolean"); - - b.Property("EnableLogMqttGeneral") - .HasColumnType("boolean"); - - b.Property("EnableLogMqttHealthPing") - .HasColumnType("boolean"); - - b.Property("MinSignalScore") - .HasColumnType("double precision"); - - b.Property("ScanCronSchedule") - .IsRequired() - .HasColumnType("text"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.ToTable("Settings"); - }); - - modelBuilder.Entity("FinlyticAnalyzer.Entities.TradeProposalEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("ConfidenceScore") - .HasColumnType("double precision"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("ExpiresAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Isin") - .IsRequired() - .HasColumnType("text"); - - b.Property("Name") - .IsRequired() - .HasColumnType("text"); - - b.Property("ProposedAction") - .IsRequired() - .HasColumnType("text"); - - b.Property("ReasonSummary") - .IsRequired() - .HasColumnType("text"); - - b.Property("Type") - .HasColumnType("integer"); - - b.HasKey("Id"); - - b.HasIndex("ExpiresAt"); - - b.HasIndex("Isin"); - - b.ToTable("TradeProposals"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/FinlyticAnalyzer/Migrations/20260805184638_AddTradeProposals.cs b/FinlyticAnalyzer/Migrations/20260805184638_AddTradeProposals.cs deleted file mode 100644 index 9b7758a..0000000 --- a/FinlyticAnalyzer/Migrations/20260805184638_AddTradeProposals.cs +++ /dev/null @@ -1,51 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace FinlyticAnalyzer.Migrations -{ - /// - public partial class AddTradeProposals : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "TradeProposals", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - Isin = table.Column(type: "text", nullable: false), - Name = table.Column(type: "text", nullable: false), - Type = table.Column(type: "integer", nullable: false), - ProposedAction = table.Column(type: "text", nullable: false), - ConfidenceScore = table.Column(type: "double precision", nullable: false), - ReasonSummary = table.Column(type: "text", nullable: false), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - ExpiresAt = table.Column(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"); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "TradeProposals"); - } - } -} diff --git a/FinlyticAnalyzer/Migrations/20260813202556_CheckPendingAnalyzer.Designer.cs b/FinlyticAnalyzer/Migrations/20260813202556_CheckPendingAnalyzer.Designer.cs deleted file mode 100644 index b2e9f0d..0000000 --- a/FinlyticAnalyzer/Migrations/20260813202556_CheckPendingAnalyzer.Designer.cs +++ /dev/null @@ -1,277 +0,0 @@ -// -using System; -using FinlyticAnalyzer.Database; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; - -#nullable disable - -namespace FinlyticAnalyzer.Migrations -{ - [DbContext(typeof(AnalyzerDbContext))] - [Migration("20260813202556_CheckPendingAnalyzer")] - partial class CheckPendingAnalyzer - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "10.0.9") - .HasAnnotation("Relational:MaxIdentifierLength", 63); - - NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - - modelBuilder.Entity("FinlyticAnalyzer.Entities.AnalysisEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("AiOutputJson") - .IsRequired() - .HasColumnType("jsonb"); - - b.Property("AnalysisId") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("EventId") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.Property("ImpactScore") - .HasColumnType("double precision"); - - b.Property("IsTradeProposed") - .HasColumnType("boolean"); - - b.Property("Isin") - .IsRequired() - .HasMaxLength(30) - .HasColumnType("character varying(30)"); - - b.Property("N8nDecision") - .IsRequired() - .HasMaxLength(30) - .HasColumnType("character varying(30)"); - - b.Property("N8nEvalScore") - .HasColumnType("double precision"); - - b.Property("N8nResponseJson") - .IsRequired() - .HasColumnType("jsonb"); - - b.Property("RawDataJson") - .IsRequired() - .HasColumnType("jsonb"); - - b.Property("Sector") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("character varying(50)"); - - b.Property("Symbol") - .IsRequired() - .HasMaxLength(30) - .HasColumnType("character varying(30)"); - - b.Property("VixRegime") - .HasColumnType("integer"); - - b.Property("VixValue") - .HasColumnType("numeric"); - - b.Property("WinRate") - .HasColumnType("double precision"); - - b.HasKey("Id"); - - b.HasIndex("AnalysisId") - .IsUnique(); - - b.HasIndex("CreatedAt"); - - b.HasIndex("EventId"); - - b.HasIndex("Isin"); - - b.HasIndex("Sector"); - - b.ToTable("analyses"); - }); - - modelBuilder.Entity("FinlyticAnalyzer.Entities.AnalyzerSettingsEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("EnableLogAnalyzerAuto") - .HasColumnType("boolean"); - - b.Property("EnableLogAnalyzerManual") - .HasColumnType("boolean"); - - b.Property("EnableLogDatabaseOps") - .HasColumnType("boolean"); - - b.Property("EnableLogMqttGeneral") - .HasColumnType("boolean"); - - b.Property("EnableLogMqttHealthPing") - .HasColumnType("boolean"); - - b.Property("MinSignalScore") - .HasColumnType("double precision"); - - b.Property("ScanCronSchedule") - .IsRequired() - .HasColumnType("text"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.ToTable("Settings"); - }); - - modelBuilder.Entity("FinlyticAnalyzer.Entities.TradeProposalEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("AnalysisId") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.Property("ConfidenceScore") - .HasColumnType("double precision"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("EntryPrice") - .HasColumnType("decimal(18,4)"); - - b.Property("EntryZoneMax") - .HasColumnType("decimal(18,4)"); - - b.Property("EntryZoneMin") - .HasColumnType("decimal(18,4)"); - - b.Property("EventId") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.Property("ExpiresAt") - .HasColumnType("timestamp with time zone"); - - b.Property("FundamentalRationale") - .IsRequired() - .HasColumnType("text"); - - b.Property("InstrumentType") - .IsRequired() - .HasMaxLength(30) - .HasColumnType("character varying(30)"); - - b.Property("Isin") - .IsRequired() - .HasMaxLength(30) - .HasColumnType("character varying(30)"); - - b.Property("MaxLeverage") - .HasColumnType("decimal(18,4)"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(150) - .HasColumnType("character varying(150)"); - - b.Property("ProposedAction") - .IsRequired() - .HasMaxLength(20) - .HasColumnType("character varying(20)"); - - b.Property("ReasonSummary") - .IsRequired() - .HasColumnType("text"); - - b.Property("RiskRewardRatio") - .HasColumnType("decimal(18,4)"); - - b.Property("RiskTolerance") - .IsRequired() - .HasMaxLength(30) - .HasColumnType("character varying(30)"); - - b.Property("RiskWarning") - .IsRequired() - .HasColumnType("text"); - - b.Property("Sector") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("character varying(50)"); - - b.Property("StopLoss") - .HasColumnType("decimal(18,4)"); - - b.Property("Symbol") - .IsRequired() - .HasMaxLength(30) - .HasColumnType("character varying(30)"); - - b.Property("TakeProfit") - .HasColumnType("decimal(18,4)"); - - b.Property("TakeProfitTargets") - .HasColumnType("text"); - - b.Property("TechnicalRationale") - .IsRequired() - .HasColumnType("text"); - - b.Property("Timeframe") - .IsRequired() - .HasMaxLength(20) - .HasColumnType("character varying(20)"); - - b.Property("Type") - .HasColumnType("integer"); - - b.Property("VixRegime") - .HasColumnType("integer"); - - b.Property("VixValue") - .HasColumnType("decimal(18,4)"); - - b.Property("WinRate") - .HasColumnType("double precision"); - - b.HasKey("Id"); - - b.HasIndex("ExpiresAt"); - - b.HasIndex("Isin"); - - b.ToTable("trade_proposals"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/FinlyticAnalyzer/Migrations/20260813202556_CheckPendingAnalyzer.cs b/FinlyticAnalyzer/Migrations/20260813202556_CheckPendingAnalyzer.cs deleted file mode 100644 index 2fc3b74..0000000 --- a/FinlyticAnalyzer/Migrations/20260813202556_CheckPendingAnalyzer.cs +++ /dev/null @@ -1,351 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace FinlyticAnalyzer.Migrations -{ - /// - public partial class CheckPendingAnalyzer : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropPrimaryKey( - name: "PK_TradeProposals", - table: "TradeProposals"); - - migrationBuilder.RenameTable( - name: "TradeProposals", - newName: "trade_proposals"); - - migrationBuilder.RenameIndex( - name: "IX_TradeProposals_Isin", - table: "trade_proposals", - newName: "IX_trade_proposals_Isin"); - - migrationBuilder.RenameIndex( - name: "IX_TradeProposals_ExpiresAt", - table: "trade_proposals", - newName: "IX_trade_proposals_ExpiresAt"); - - migrationBuilder.AlterColumn( - name: "ProposedAction", - table: "trade_proposals", - type: "character varying(20)", - maxLength: 20, - nullable: false, - oldClrType: typeof(string), - oldType: "text"); - - migrationBuilder.AlterColumn( - name: "Name", - table: "trade_proposals", - type: "character varying(150)", - maxLength: 150, - nullable: false, - oldClrType: typeof(string), - oldType: "text"); - - migrationBuilder.AlterColumn( - name: "Isin", - table: "trade_proposals", - type: "character varying(30)", - maxLength: 30, - nullable: false, - oldClrType: typeof(string), - oldType: "text"); - - migrationBuilder.AddColumn( - name: "AnalysisId", - table: "trade_proposals", - type: "character varying(100)", - maxLength: 100, - nullable: false, - defaultValue: ""); - - migrationBuilder.AddColumn( - name: "EntryPrice", - table: "trade_proposals", - type: "numeric(18,4)", - nullable: false, - defaultValue: 0m); - - migrationBuilder.AddColumn( - name: "EntryZoneMax", - table: "trade_proposals", - type: "numeric(18,4)", - nullable: true); - - migrationBuilder.AddColumn( - name: "EntryZoneMin", - table: "trade_proposals", - type: "numeric(18,4)", - nullable: true); - - migrationBuilder.AddColumn( - name: "EventId", - table: "trade_proposals", - type: "character varying(100)", - maxLength: 100, - nullable: false, - defaultValue: ""); - - migrationBuilder.AddColumn( - name: "FundamentalRationale", - table: "trade_proposals", - type: "text", - nullable: false, - defaultValue: ""); - - migrationBuilder.AddColumn( - name: "InstrumentType", - table: "trade_proposals", - type: "character varying(30)", - maxLength: 30, - nullable: false, - defaultValue: ""); - - migrationBuilder.AddColumn( - name: "MaxLeverage", - table: "trade_proposals", - type: "numeric(18,4)", - nullable: true); - - migrationBuilder.AddColumn( - name: "RiskRewardRatio", - table: "trade_proposals", - type: "numeric(18,4)", - nullable: true); - - migrationBuilder.AddColumn( - name: "RiskTolerance", - table: "trade_proposals", - type: "character varying(30)", - maxLength: 30, - nullable: false, - defaultValue: ""); - - migrationBuilder.AddColumn( - name: "RiskWarning", - table: "trade_proposals", - type: "text", - nullable: false, - defaultValue: ""); - - migrationBuilder.AddColumn( - name: "Sector", - table: "trade_proposals", - type: "character varying(50)", - maxLength: 50, - nullable: false, - defaultValue: ""); - - migrationBuilder.AddColumn( - name: "StopLoss", - table: "trade_proposals", - type: "numeric(18,4)", - nullable: false, - defaultValue: 0m); - - migrationBuilder.AddColumn( - name: "Symbol", - table: "trade_proposals", - type: "character varying(30)", - maxLength: 30, - nullable: false, - defaultValue: ""); - - migrationBuilder.AddColumn( - name: "TakeProfit", - table: "trade_proposals", - type: "numeric(18,4)", - nullable: false, - defaultValue: 0m); - - migrationBuilder.AddColumn( - name: "TakeProfitTargets", - table: "trade_proposals", - type: "text", - nullable: true); - - migrationBuilder.AddColumn( - name: "TechnicalRationale", - table: "trade_proposals", - type: "text", - nullable: false, - defaultValue: ""); - - migrationBuilder.AddColumn( - name: "Timeframe", - table: "trade_proposals", - type: "character varying(20)", - maxLength: 20, - nullable: false, - defaultValue: ""); - - migrationBuilder.AddColumn( - name: "VixRegime", - table: "trade_proposals", - type: "integer", - nullable: false, - defaultValue: 0); - - migrationBuilder.AddColumn( - name: "VixValue", - table: "trade_proposals", - type: "numeric(18,4)", - nullable: false, - defaultValue: 0m); - - migrationBuilder.AddColumn( - name: "WinRate", - table: "trade_proposals", - type: "double precision", - nullable: false, - defaultValue: 0.0); - - migrationBuilder.AddPrimaryKey( - name: "PK_trade_proposals", - table: "trade_proposals", - column: "Id"); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropPrimaryKey( - name: "PK_trade_proposals", - table: "trade_proposals"); - - migrationBuilder.DropColumn( - name: "AnalysisId", - table: "trade_proposals"); - - migrationBuilder.DropColumn( - name: "EntryPrice", - table: "trade_proposals"); - - migrationBuilder.DropColumn( - name: "EntryZoneMax", - table: "trade_proposals"); - - migrationBuilder.DropColumn( - name: "EntryZoneMin", - table: "trade_proposals"); - - migrationBuilder.DropColumn( - name: "EventId", - table: "trade_proposals"); - - migrationBuilder.DropColumn( - name: "FundamentalRationale", - table: "trade_proposals"); - - migrationBuilder.DropColumn( - name: "InstrumentType", - table: "trade_proposals"); - - migrationBuilder.DropColumn( - name: "MaxLeverage", - table: "trade_proposals"); - - migrationBuilder.DropColumn( - name: "RiskRewardRatio", - table: "trade_proposals"); - - migrationBuilder.DropColumn( - name: "RiskTolerance", - table: "trade_proposals"); - - migrationBuilder.DropColumn( - name: "RiskWarning", - table: "trade_proposals"); - - migrationBuilder.DropColumn( - name: "Sector", - table: "trade_proposals"); - - migrationBuilder.DropColumn( - name: "StopLoss", - table: "trade_proposals"); - - migrationBuilder.DropColumn( - name: "Symbol", - table: "trade_proposals"); - - migrationBuilder.DropColumn( - name: "TakeProfit", - table: "trade_proposals"); - - migrationBuilder.DropColumn( - name: "TakeProfitTargets", - table: "trade_proposals"); - - migrationBuilder.DropColumn( - name: "TechnicalRationale", - table: "trade_proposals"); - - migrationBuilder.DropColumn( - name: "Timeframe", - table: "trade_proposals"); - - migrationBuilder.DropColumn( - name: "VixRegime", - table: "trade_proposals"); - - migrationBuilder.DropColumn( - name: "VixValue", - table: "trade_proposals"); - - migrationBuilder.DropColumn( - name: "WinRate", - table: "trade_proposals"); - - migrationBuilder.RenameTable( - name: "trade_proposals", - newName: "TradeProposals"); - - migrationBuilder.RenameIndex( - name: "IX_trade_proposals_Isin", - table: "TradeProposals", - newName: "IX_TradeProposals_Isin"); - - migrationBuilder.RenameIndex( - name: "IX_trade_proposals_ExpiresAt", - table: "TradeProposals", - newName: "IX_TradeProposals_ExpiresAt"); - - migrationBuilder.AlterColumn( - name: "ProposedAction", - table: "TradeProposals", - type: "text", - nullable: false, - oldClrType: typeof(string), - oldType: "character varying(20)", - oldMaxLength: 20); - - migrationBuilder.AlterColumn( - name: "Name", - table: "TradeProposals", - type: "text", - nullable: false, - oldClrType: typeof(string), - oldType: "character varying(150)", - oldMaxLength: 150); - - migrationBuilder.AlterColumn( - name: "Isin", - table: "TradeProposals", - type: "text", - nullable: false, - oldClrType: typeof(string), - oldType: "character varying(30)", - oldMaxLength: 30); - - migrationBuilder.AddPrimaryKey( - name: "PK_TradeProposals", - table: "TradeProposals", - column: "Id"); - } - } -} diff --git a/FinlyticAnalyzer/Migrations/20260815184017_AddDynamicSettings.Designer.cs b/FinlyticAnalyzer/Migrations/20260815184017_AddDynamicSettings.Designer.cs deleted file mode 100644 index 21f265b..0000000 --- a/FinlyticAnalyzer/Migrations/20260815184017_AddDynamicSettings.Designer.cs +++ /dev/null @@ -1,308 +0,0 @@ -// -using System; -using FinlyticAnalyzer.Database; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; - -#nullable disable - -namespace FinlyticAnalyzer.Migrations -{ - [DbContext(typeof(AnalyzerDbContext))] - [Migration("20260815184017_AddDynamicSettings")] - partial class AddDynamicSettings - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "10.0.9") - .HasAnnotation("Relational:MaxIdentifierLength", 63); - - NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - - modelBuilder.Entity("FinlyticAnalyzer.Entities.AnalysisEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("AiOutputJson") - .IsRequired() - .HasColumnType("jsonb"); - - b.Property("AnalysisId") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("EventId") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.Property("ImpactScore") - .HasColumnType("double precision"); - - b.Property("IsTradeProposed") - .HasColumnType("boolean"); - - b.Property("Isin") - .IsRequired() - .HasMaxLength(30) - .HasColumnType("character varying(30)"); - - b.Property("N8nDecision") - .IsRequired() - .HasMaxLength(30) - .HasColumnType("character varying(30)"); - - b.Property("N8nEvalScore") - .HasColumnType("double precision"); - - b.Property("N8nResponseJson") - .IsRequired() - .HasColumnType("jsonb"); - - b.Property("RawDataJson") - .IsRequired() - .HasColumnType("jsonb"); - - b.Property("Sector") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("character varying(50)"); - - b.Property("Symbol") - .IsRequired() - .HasMaxLength(30) - .HasColumnType("character varying(30)"); - - b.Property("VixRegime") - .HasColumnType("integer"); - - b.Property("VixValue") - .HasColumnType("numeric"); - - b.Property("WinRate") - .HasColumnType("double precision"); - - b.HasKey("Id"); - - b.HasIndex("AnalysisId") - .IsUnique(); - - b.HasIndex("CreatedAt"); - - b.HasIndex("EventId"); - - b.HasIndex("Isin"); - - b.HasIndex("Sector"); - - b.ToTable("analyses"); - }); - - modelBuilder.Entity("FinlyticAnalyzer.Entities.AnalyzerSettingsEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("EnableLogAnalyzerAuto") - .HasColumnType("boolean"); - - b.Property("EnableLogAnalyzerManual") - .HasColumnType("boolean"); - - b.Property("EnableLogDatabaseOps") - .HasColumnType("boolean"); - - b.Property("EnableLogMqttGeneral") - .HasColumnType("boolean"); - - b.Property("EnableLogMqttHealthPing") - .HasColumnType("boolean"); - - b.Property("MinSignalScore") - .HasColumnType("double precision"); - - b.Property("ScanCronSchedule") - .IsRequired() - .HasColumnType("text"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.ToTable("Settings"); - }); - - modelBuilder.Entity("FinlyticAnalyzer.Entities.TradeProposalEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("AnalysisId") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.Property("ConfidenceScore") - .HasColumnType("double precision"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("EntryPrice") - .HasColumnType("decimal(18,4)"); - - b.Property("EntryZoneMax") - .HasColumnType("decimal(18,4)"); - - b.Property("EntryZoneMin") - .HasColumnType("decimal(18,4)"); - - b.Property("EventId") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.Property("ExpiresAt") - .HasColumnType("timestamp with time zone"); - - b.Property("FundamentalRationale") - .IsRequired() - .HasColumnType("text"); - - b.Property("InstrumentType") - .IsRequired() - .HasMaxLength(30) - .HasColumnType("character varying(30)"); - - b.Property("Isin") - .IsRequired() - .HasMaxLength(30) - .HasColumnType("character varying(30)"); - - b.Property("MaxLeverage") - .HasColumnType("decimal(18,4)"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(150) - .HasColumnType("character varying(150)"); - - b.Property("ProposedAction") - .IsRequired() - .HasMaxLength(20) - .HasColumnType("character varying(20)"); - - b.Property("ReasonSummary") - .IsRequired() - .HasColumnType("text"); - - b.Property("RiskRewardRatio") - .HasColumnType("decimal(18,4)"); - - b.Property("RiskTolerance") - .IsRequired() - .HasMaxLength(30) - .HasColumnType("character varying(30)"); - - b.Property("RiskWarning") - .IsRequired() - .HasColumnType("text"); - - b.Property("Sector") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("character varying(50)"); - - b.Property("StopLoss") - .HasColumnType("decimal(18,4)"); - - b.Property("Symbol") - .IsRequired() - .HasMaxLength(30) - .HasColumnType("character varying(30)"); - - b.Property("TakeProfit") - .HasColumnType("decimal(18,4)"); - - b.Property("TakeProfitTargets") - .HasColumnType("text"); - - b.Property("TechnicalRationale") - .IsRequired() - .HasColumnType("text"); - - b.Property("Timeframe") - .IsRequired() - .HasMaxLength(20) - .HasColumnType("character varying(20)"); - - b.Property("Type") - .HasColumnType("integer"); - - b.Property("VixRegime") - .HasColumnType("integer"); - - b.Property("VixValue") - .HasColumnType("decimal(18,4)"); - - b.Property("WinRate") - .HasColumnType("double precision"); - - b.HasKey("Id"); - - b.HasIndex("ExpiresAt"); - - b.HasIndex("Isin"); - - b.ToTable("trade_proposals"); - }); - - modelBuilder.Entity("FinlyticCore.Entities.Settings.SettingEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Key") - .IsRequired() - .HasMaxLength(150) - .HasColumnType("character varying(150)"); - - b.Property("LastUpdatedUtc") - .HasColumnType("timestamp with time zone"); - - b.Property("ServiceIdentifier") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.Property("ValueJson") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("Id"); - - b.HasIndex("Key") - .IsUnique(); - - b.ToTable("DynamicSettings"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/FinlyticAnalyzer/Migrations/20260815184017_AddDynamicSettings.cs b/FinlyticAnalyzer/Migrations/20260815184017_AddDynamicSettings.cs deleted file mode 100644 index 539c266..0000000 --- a/FinlyticAnalyzer/Migrations/20260815184017_AddDynamicSettings.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace FinlyticAnalyzer.Migrations -{ - /// - public partial class AddDynamicSettings : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "DynamicSettings", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - Key = table.Column(type: "character varying(150)", maxLength: 150, nullable: false), - ValueJson = table.Column(type: "text", nullable: false), - ServiceIdentifier = table.Column(type: "character varying(100)", maxLength: 100, nullable: false), - LastUpdatedUtc = table.Column(type: "timestamp with time zone", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_DynamicSettings", x => x.Id); - }); - - migrationBuilder.CreateIndex( - name: "IX_DynamicSettings_Key", - table: "DynamicSettings", - column: "Key", - unique: true); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "DynamicSettings"); - } - } -} diff --git a/FinlyticAnalyzer/Migrations/AnalyzerDbContextModelSnapshot.cs b/FinlyticAnalyzer/Migrations/AnalyzerDbContextModelSnapshot.cs deleted file mode 100644 index 373d5a7..0000000 --- a/FinlyticAnalyzer/Migrations/AnalyzerDbContextModelSnapshot.cs +++ /dev/null @@ -1,305 +0,0 @@ -// -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("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("AiOutputJson") - .IsRequired() - .HasColumnType("jsonb"); - - b.Property("AnalysisId") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("EventId") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.Property("ImpactScore") - .HasColumnType("double precision"); - - b.Property("IsTradeProposed") - .HasColumnType("boolean"); - - b.Property("Isin") - .IsRequired() - .HasMaxLength(30) - .HasColumnType("character varying(30)"); - - b.Property("N8nDecision") - .IsRequired() - .HasMaxLength(30) - .HasColumnType("character varying(30)"); - - b.Property("N8nEvalScore") - .HasColumnType("double precision"); - - b.Property("N8nResponseJson") - .IsRequired() - .HasColumnType("jsonb"); - - b.Property("RawDataJson") - .IsRequired() - .HasColumnType("jsonb"); - - b.Property("Sector") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("character varying(50)"); - - b.Property("Symbol") - .IsRequired() - .HasMaxLength(30) - .HasColumnType("character varying(30)"); - - b.Property("VixRegime") - .HasColumnType("integer"); - - b.Property("VixValue") - .HasColumnType("numeric"); - - b.Property("WinRate") - .HasColumnType("double precision"); - - b.HasKey("Id"); - - b.HasIndex("AnalysisId") - .IsUnique(); - - b.HasIndex("CreatedAt"); - - b.HasIndex("EventId"); - - b.HasIndex("Isin"); - - b.HasIndex("Sector"); - - b.ToTable("analyses"); - }); - - modelBuilder.Entity("FinlyticAnalyzer.Entities.AnalyzerSettingsEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("EnableLogAnalyzerAuto") - .HasColumnType("boolean"); - - b.Property("EnableLogAnalyzerManual") - .HasColumnType("boolean"); - - b.Property("EnableLogDatabaseOps") - .HasColumnType("boolean"); - - b.Property("EnableLogMqttGeneral") - .HasColumnType("boolean"); - - b.Property("EnableLogMqttHealthPing") - .HasColumnType("boolean"); - - b.Property("MinSignalScore") - .HasColumnType("double precision"); - - b.Property("ScanCronSchedule") - .IsRequired() - .HasColumnType("text"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.ToTable("Settings"); - }); - - modelBuilder.Entity("FinlyticAnalyzer.Entities.TradeProposalEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("AnalysisId") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.Property("ConfidenceScore") - .HasColumnType("double precision"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("EntryPrice") - .HasColumnType("decimal(18,4)"); - - b.Property("EntryZoneMax") - .HasColumnType("decimal(18,4)"); - - b.Property("EntryZoneMin") - .HasColumnType("decimal(18,4)"); - - b.Property("EventId") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.Property("ExpiresAt") - .HasColumnType("timestamp with time zone"); - - b.Property("FundamentalRationale") - .IsRequired() - .HasColumnType("text"); - - b.Property("InstrumentType") - .IsRequired() - .HasMaxLength(30) - .HasColumnType("character varying(30)"); - - b.Property("Isin") - .IsRequired() - .HasMaxLength(30) - .HasColumnType("character varying(30)"); - - b.Property("MaxLeverage") - .HasColumnType("decimal(18,4)"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(150) - .HasColumnType("character varying(150)"); - - b.Property("ProposedAction") - .IsRequired() - .HasMaxLength(20) - .HasColumnType("character varying(20)"); - - b.Property("ReasonSummary") - .IsRequired() - .HasColumnType("text"); - - b.Property("RiskRewardRatio") - .HasColumnType("decimal(18,4)"); - - b.Property("RiskTolerance") - .IsRequired() - .HasMaxLength(30) - .HasColumnType("character varying(30)"); - - b.Property("RiskWarning") - .IsRequired() - .HasColumnType("text"); - - b.Property("Sector") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("character varying(50)"); - - b.Property("StopLoss") - .HasColumnType("decimal(18,4)"); - - b.Property("Symbol") - .IsRequired() - .HasMaxLength(30) - .HasColumnType("character varying(30)"); - - b.Property("TakeProfit") - .HasColumnType("decimal(18,4)"); - - b.Property("TakeProfitTargets") - .HasColumnType("text"); - - b.Property("TechnicalRationale") - .IsRequired() - .HasColumnType("text"); - - b.Property("Timeframe") - .IsRequired() - .HasMaxLength(20) - .HasColumnType("character varying(20)"); - - b.Property("Type") - .HasColumnType("integer"); - - b.Property("VixRegime") - .HasColumnType("integer"); - - b.Property("VixValue") - .HasColumnType("decimal(18,4)"); - - b.Property("WinRate") - .HasColumnType("double precision"); - - b.HasKey("Id"); - - b.HasIndex("ExpiresAt"); - - b.HasIndex("Isin"); - - b.ToTable("trade_proposals"); - }); - - modelBuilder.Entity("FinlyticCore.Entities.Settings.SettingEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Key") - .IsRequired() - .HasMaxLength(150) - .HasColumnType("character varying(150)"); - - b.Property("LastUpdatedUtc") - .HasColumnType("timestamp with time zone"); - - b.Property("ServiceIdentifier") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.Property("ValueJson") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("Id"); - - b.HasIndex("Key") - .IsUnique(); - - b.ToTable("DynamicSettings"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/FinlyticAnalyzer/Program.cs b/FinlyticAnalyzer/Program.cs deleted file mode 100644 index 1350195..0000000 --- a/FinlyticAnalyzer/Program.cs +++ /dev/null @@ -1,65 +0,0 @@ -using System; -using FinlyticAnalyzer.Database; -using FinlyticAnalyzer.Services; -using FinlyticAnalyzer.Util; -using FinlyticCore.Database; -using FinlyticCore.Services; -using FinlyticCore.Services.Yahoo; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; - -var builder = Host.CreateApplicationBuilder(args); - -// Register DB Context -builder.Services.AddDbContext(options => - options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection"))); -builder.Services.AddScoped(sp => sp.GetRequiredService()); - -// Register Core Services -builder.Services.AddSingleton(); -builder.Services.AddSingleton(typeof(IFinlyticLogger<>), typeof(FinlyticLogger<>)); - -// Register HTTP Clients for external webhooks (HttpClientFactory manages pool) -builder.Services.AddHttpClient(); - -// Register Domain Services -builder.Services.AddSingleton(); -builder.Services.AddSingleton(); -builder.Services.AddSingleton(); -builder.Services.AddScoped(); -builder.Services.AddSingleton(); - -// Unified MQTT Client (Handles both Events and RPC) -builder.Services.AddSingleton(); -builder.Services.AddHostedService(provider => provider.GetRequiredService()); - -// Register Active Trade Monitor -builder.Services.AddHostedService(); - -var host = builder.Build(); - -// Run DB Migrations -using (var scope = host.Services.CreateScope()) -{ - try - { - var context = scope.ServiceProvider.GetRequiredService(); - await context.Database.MigrateAsync(); - Console.WriteLine("Database migrations successfully executed for FinlyticAnalyzer."); - } - catch (Exception ex) - { - Console.WriteLine($"Critical error during database migration for FinlyticAnalyzer: {ex.Message}"); - } -} - -// Initial VIX Poll -using (var scope = host.Services.CreateScope()) -{ - var vixService = scope.ServiceProvider.GetRequiredService(); - await vixService.PollVixAsync(); -} - -await host.RunAsync(); \ No newline at end of file diff --git a/FinlyticAnalyzer/Project.md b/FinlyticAnalyzer/Project.md deleted file mode 100644 index 39df143..0000000 --- a/FinlyticAnalyzer/Project.md +++ /dev/null @@ -1,37 +0,0 @@ -# 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. diff --git a/FinlyticAnalyzer/Services/ActiveTradeMonitorWorker.cs b/FinlyticAnalyzer/Services/ActiveTradeMonitorWorker.cs deleted file mode 100644 index 2c3962f..0000000 --- a/FinlyticAnalyzer/Services/ActiveTradeMonitorWorker.cs +++ /dev/null @@ -1,343 +0,0 @@ -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.Services; -using FinlyticCore.Util; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; - -namespace FinlyticAnalyzer.Services; - -public class ActiveTradeMonitorWorker : BackgroundService -{ - private readonly IFinlyticLogger _finlyticLogger; - private readonly IServiceScopeFactory _scopeFactory; - private readonly AnalyzerMqttClient _mqttClient; - - public ActiveTradeMonitorWorker( - IFinlyticLogger finlyticLogger, - IServiceScopeFactory scopeFactory, - AnalyzerMqttClient mqttClient) - { - _finlyticLogger = finlyticLogger; - _scopeFactory = scopeFactory; - _mqttClient = mqttClient; - } - - protected override async Task ExecuteAsync(CancellationToken stoppingToken) - { - await _finlyticLogger.LogInfoAsync(SettingKeys.AnalyzerChannel, "[ActiveTradeMonitorWorker] ActiveTradeMonitorWorker started."); - - try - { - await Task.Delay(TimeSpan.FromSeconds(30), stoppingToken); - } - catch (OperationCanceledException) - { - return; - } - - while (!stoppingToken.IsCancellationRequested) - { - try - { - await MonitorActiveTradesAsync(stoppingToken); - } - catch (Exception ex) when (!stoppingToken.IsCancellationRequested) - { - await _finlyticLogger.LogErrorAsync(SettingKeys.AnalyzerChannel, ex, "[ActiveTradeMonitorWorker] Error in ActiveTradeMonitorWorker loop."); - } - - try - { - await Task.Delay(TimeSpan.FromMinutes(60), stoppingToken); - } - catch (OperationCanceledException) - { - break; - } - } - - await _finlyticLogger.LogInfoAsync(SettingKeys.AnalyzerChannel, "[ActiveTradeMonitorWorker] ActiveTradeMonitorWorker stopped."); - } - - private async Task MonitorActiveTradesAsync(CancellationToken cancellationToken) - { - if (!_mqttClient.IsConnected) - { - await _finlyticLogger.LogWarningAsync(SettingKeys.AnalyzerChannel, "[ActiveTradeMonitorWorker] Skipping trade monitoring. RPC client not connected."); - return; - } - - var activeTrades = await _mqttClient.SendRpcRequestAsync, GetTradesRequest>( - "trades_Get", - new GetTradesRequest(null, "Active"), - TimeSpan.FromSeconds(10)); - - var proposedTrades = await _mqttClient.SendRpcRequestAsync, GetTradesRequest>( - "trades_Get", - new GetTradesRequest(null, "Proposed"), - TimeSpan.FromSeconds(10)); - - var trades = new List(); - if (activeTrades != null) trades.AddRange(activeTrades); - if (proposedTrades != null) trades.AddRange(proposedTrades.Where(t => t.IsGlobalProposal)); - - if (trades.Count == 0) - { - await _finlyticLogger.LogInfoAsync(SettingKeys.AnalyzerChannel, "[ActiveTradeMonitorWorker] No active or proposed global trades found to monitor."); - return; - } - - await _finlyticLogger.LogInfoAsync(SettingKeys.AnalyzerChannel, "[ActiveTradeMonitorWorker] Found {Count} trades to monitor. Starting evaluation...", trades.Count); - - using var scope = _scopeFactory.CreateScope(); - var n8nService = scope.ServiceProvider.GetRequiredService(); - var vixService = scope.ServiceProvider.GetRequiredService(); - - foreach (var trade in trades) - { - if (cancellationToken.IsCancellationRequested) break; - - try - { - await ProcessTradeAsync(trade, n8nService, vixService, cancellationToken); - } - catch (Exception ex) - { - await _finlyticLogger.LogErrorAsync(SettingKeys.AnalyzerChannel, ex, "[ActiveTradeMonitorWorker] Failed to monitor trade {TradeId} ({Symbol}).", trade.TradeId, trade.Symbol); - } - } - } - - private async Task ProcessTradeAsync(TradeProposalDto trade, IN8nEvaluationService n8nService, - IVixTrackerService vixService, CancellationToken cancellationToken) - { - var livePriceReq = new IsinRequest(trade.Isin); - var livePriceDto = await _mqttClient.SendRpcRequestAsync( - "tr_GetLivePrice", livePriceReq, TimeSpan.FromSeconds(3)); - - decimal currentPrice = livePriceDto?.CurrentPrice > 0 ? livePriceDto.CurrentPrice : trade.EntryPrice; - - bool isLong = string.Equals(trade.SignalType, "BUY", StringComparison.OrdinalIgnoreCase) || - string.Equals(trade.SignalType, "LONG", StringComparison.OrdinalIgnoreCase); - - int maxHoldingDays = EstimateMaxHoldingDays(trade.Timeframe); - double daysOpen = (DateTime.UtcNow - trade.CreatedAt).TotalDays; - - 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; - } - } - - var taResult = await _mqttClient.SendRpcRequestAsync( - "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() - }; - - 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 = vixService.GetCurrentVix(), - MarketRegime = vixService.GetCurrentRegime().ToString() - }, - UserPreferences = new UserPreferencesInfo - { - InstrumentType = trade.InstrumentType, - TimeframeFormatted = trade.Timeframe - }, - TechnicalContext = taInfo - }; - - var aiResponse = await n8nService.EvaluateAssetAsync(n8nReq, cancellationToken); - if (aiResponse == null) - { - await _finlyticLogger.LogWarningAsync(SettingKeys.AnalyzerChannel, "[ActiveTradeMonitorWorker] AI evaluation returned null for {TradeId}. Skipping update.", trade.TradeId); - return; - } - - string newRecommendation = "Hold"; - string reasoning = aiResponse.AiReasoning; - decimal? newStopLoss = trade.StopLoss; - decimal? newTakeProfit = trade.TakeProfit; - - 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) - { - if (aiResponse.ExecutionPlan.StopLoss > 0) - { - var proposedSl = aiResponse.ExecutionPlan.StopLoss; - if (isLong) - { - if (trade.StopLoss <= 0 || proposedSl > trade.StopLoss) - { - newStopLoss = proposedSl; - if (proposedSl > trade.StopLoss && trade.StopLoss > 0) newRecommendation = "AdjustSL"; - } - } - else - { - 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 - }; - - string topic = $"finlytic/trades/updates/{trade.Isin}"; - await _mqttClient.PublishAsync(topic, update); - - await _finlyticLogger.LogInfoAsync(SettingKeys.AnalyzerChannel, "[ActiveTradeMonitorWorker] Published trade update for {TradeId} to topic {Topic}. Recommendation: {Rec}, Reasoning: {Reason}", - trade.TradeId, topic, recommendation, reasoning); - } - - private static int EstimateMaxHoldingDays(string timeframe) - { - if (string.IsNullOrWhiteSpace(timeframe)) return 14; - - 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(); - 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; - - return maxNum * multiplier; - } -} \ No newline at end of file diff --git a/FinlyticAnalyzer/Services/IN8nEvaluationService.cs b/FinlyticAnalyzer/Services/IN8nEvaluationService.cs deleted file mode 100644 index 19ef27f..0000000 --- a/FinlyticAnalyzer/Services/IN8nEvaluationService.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System.Threading; -using System.Threading.Tasks; -using FinlyticCore.Models.Analyzer; - -namespace FinlyticAnalyzer.Services; - -public interface IN8nEvaluationService -{ - /// - /// Evaluates an asset asynchronously using N8n. - /// - Task EvaluateAssetAsync(N8nAnalysisRequestDto request, CancellationToken cancellationToken = default); -} diff --git a/FinlyticAnalyzer/Services/IThreeLayerFilterEngine.cs b/FinlyticAnalyzer/Services/IThreeLayerFilterEngine.cs deleted file mode 100644 index 4692c78..0000000 --- a/FinlyticAnalyzer/Services/IThreeLayerFilterEngine.cs +++ /dev/null @@ -1,29 +0,0 @@ -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 -{ - /// - /// Evaluates news based on market regime and returns a filter result. - /// - FilterResult EvaluateNews(NewsArticleDto newsEvent, VixMarketRegime regime); -} diff --git a/FinlyticAnalyzer/Services/IVixTrackerService.cs b/FinlyticAnalyzer/Services/IVixTrackerService.cs deleted file mode 100644 index bea931d..0000000 --- a/FinlyticAnalyzer/Services/IVixTrackerService.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System.Threading; -using System.Threading.Tasks; -using FinlyticCore.Models.Analyzer; - -namespace FinlyticAnalyzer.Services; - -public interface IVixTrackerService -{ - /// - /// Gets the current VIX value. - /// - decimal GetCurrentVix(); - - /// - /// Gets the current market regime based on VIX. - /// - VixMarketRegime GetCurrentRegime(); - - /// - /// Updates the VIX tracker with a new tick value. - /// - void UpdateVixFromTick(decimal vixValue); - - /// - /// Polls the VIX asynchronously and returns its value. - /// - Task PollVixAsync(CancellationToken cancellationToken = default); -} diff --git a/FinlyticAnalyzer/Services/IWinRateCalculator.cs b/FinlyticAnalyzer/Services/IWinRateCalculator.cs deleted file mode 100644 index ea83ccb..0000000 --- a/FinlyticAnalyzer/Services/IWinRateCalculator.cs +++ /dev/null @@ -1,25 +0,0 @@ -using FinlyticCore.Models.Analyzer; - -namespace FinlyticAnalyzer.Services; - -public interface IWinRateCalculator -{ - /// - /// Calculates the win rate for a given sector and symbol under the specified market regime. - /// - double CalculateWinRate(string sector, string symbol, VixMarketRegime regime); - - /// - /// Calculates a multi-factor dynamic AI Win-Rate / Confidence Score using technicals, sentiment, fundamentals, AI eval score, and market regime. - /// - double CalculateDynamicWinRate( - string sector, - string symbol, - VixMarketRegime regime, - double? n8nEvalScore = null, - double? technicalScore = null, - double? sentimentScore = null, - double? fundamentalScore = null, - string signalType = "BUY"); -} - diff --git a/FinlyticAnalyzer/Services/LogCategoryFilter.cs b/FinlyticAnalyzer/Services/LogCategoryFilter.cs deleted file mode 100644 index 1ba66f1..0000000 --- a/FinlyticAnalyzer/Services/LogCategoryFilter.cs +++ /dev/null @@ -1,36 +0,0 @@ -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; - - /// - /// Checks if a given log category is enabled. - /// - 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 - }; - } -} diff --git a/FinlyticAnalyzer/Services/N8nEvaluationService.cs b/FinlyticAnalyzer/Services/N8nEvaluationService.cs deleted file mode 100644 index 8aa57eb..0000000 --- a/FinlyticAnalyzer/Services/N8nEvaluationService.cs +++ /dev/null @@ -1,126 +0,0 @@ -using System; -using System.Net.Http; -using System.Net.Http.Json; -using System.Text.Json; -using System.Threading; -using System.Threading.Tasks; -using FinlyticAnalyzer.Util; -using FinlyticCore.Models.Analyzer; -using FinlyticCore.Services; -using FinlyticCore.Util; -using Microsoft.Extensions.Configuration; - -namespace FinlyticAnalyzer.Services; - -public class N8nEvaluationService : IN8nEvaluationService -{ - private readonly HttpClient _httpClient; - private readonly ISettingsService _settingsService; - private readonly IConfiguration _configuration; - private readonly IFinlyticLogger _finlyticLogger; - - public N8nEvaluationService( - HttpClient httpClient, - ISettingsService settingsService, - IConfiguration configuration, - IFinlyticLogger finlyticLogger) - { - _httpClient = httpClient; - _settingsService = settingsService; - _configuration = configuration; - _finlyticLogger = finlyticLogger; - _httpClient.Timeout = TimeSpan.FromSeconds(45); - } - - /// - /// Evaluates an asset asynchronously using N8n / Gemini workflows. - /// - public async Task EvaluateAssetAsync(N8nAnalysisRequestDto request, CancellationToken cancellationToken = default) - { - string webhookUrl = await _settingsService.GetSettingAsync(SettingKeys.N8nWebhookUrl, cancellationToken); - if (string.IsNullOrWhiteSpace(webhookUrl)) - { - webhookUrl = _configuration["N8N:WebhookUrl"] ?? _configuration["N8N__WebhookUrl"] ?? string.Empty; - } - - if (string.IsNullOrWhiteSpace(webhookUrl)) - { - await _finlyticLogger.LogErrorAsync(SettingKeys.AnalyzerChannel, "[N8nEvaluationService] Cannot execute AI evaluation for {Symbol}: N8N:WebhookUrl is not configured in dynamic settings or environment.", request.TargetAsset.Symbol); - return null; - } - - try - { - await _finlyticLogger.LogInfoAsync(SettingKeys.AnalyzerChannel, "[N8nEvaluationService] Sending n8n AI Evaluation request {RequestId} for Asset {Symbol} (ISIN: {Isin}) to {Url}...", - request.RequestId, request.TargetAsset.Symbol, request.TargetAsset.Isin, webhookUrl); - - 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() == "[]") - { - await _finlyticLogger.LogWarningAsync(SettingKeys.AnalyzerChannel, "[N8nEvaluationService] n8n Webhook returned an EMPTY response for Request {RequestId}. Flagging as AI Rejection (Too Risky).", request.RequestId); - return CreateRejectionFallback(request, "Die KI (n8n/Gemini) stuft den Trade als zu riskant ein und empfiehlt keine Positionierung."); - } - - 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)) - { - await _finlyticLogger.LogInfoAsync(SettingKeys.AnalyzerChannel, "[N8nEvaluationService] Received n8n AI Response for Request {RequestId}: Decision={Decision}, Score={Score:F2}, Direction={Direction}, Timeframe={Timeframe}", - request.RequestId, responseDto.AiDecision, responseDto.EvalScore, responseDto.SuggestedDirection, responseDto.SuggestedTimeframe); - - return responseDto; - } - } - else - { - await _finlyticLogger.LogWarningAsync(SettingKeys.AnalyzerChannel, "[N8nEvaluationService] n8n Webhook returned HTTP {StatusCode} for Request {RequestId}", - response.StatusCode, request.RequestId); - } - } - catch (OperationCanceledException ex) when (!cancellationToken.IsCancellationRequested) - { - await _finlyticLogger.LogErrorAsync(SettingKeys.AnalyzerChannel, ex, "[N8nEvaluationService] Timeout waiting for n8n AI Evaluation Webhook (Request {RequestId}). Consider increasing timeout.", request.RequestId); - } - catch (Exception ex) - { - await _finlyticLogger.LogErrorAsync(SettingKeys.AnalyzerChannel, ex, "[N8nEvaluationService] Error calling n8n AI Evaluation Webhook for Request {RequestId}", request.RequestId); - } - - return null; - } - - 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 - }; - } -} \ No newline at end of file diff --git a/FinlyticAnalyzer/Services/SettingsDbService.cs b/FinlyticAnalyzer/Services/SettingsDbService.cs deleted file mode 100644 index faf932f..0000000 --- a/FinlyticAnalyzer/Services/SettingsDbService.cs +++ /dev/null @@ -1,124 +0,0 @@ -using FinlyticAnalyzer.Database; -using FinlyticAnalyzer.Entities; -using Microsoft.EntityFrameworkCore; - -namespace FinlyticAnalyzer.Services; - -public interface ISettingsDbService -{ - /// - /// Gets the analyzer settings asynchronously. - /// - Task GetSettingsAsync(); - - /// - /// Saves the analyzer settings asynchronously. - /// - Task SaveSettingsAsync(AnalyzerSettingsEntity settings); - - /// - /// Updates settings from a dictionary asynchronously. - /// - Task UpdateSettingsFromDictionaryAsync(Dictionary dictionary); -} - -public class SettingsDbService : ISettingsDbService -{ - private readonly AnalyzerDbContext _context; - - public SettingsDbService(AnalyzerDbContext context) - { - _context = context; - } - - /// - /// Gets the analyzer settings asynchronously. - /// - public async Task GetSettingsAsync() - { - var settings = await _context.Settings.AsNoTracking().FirstOrDefaultAsync(); - if (settings == null) - { - settings = new AnalyzerSettingsEntity { Id = Guid.NewGuid(), UpdatedAt = DateTime.UtcNow }; - _context.Settings.Add(settings); - await _context.SaveChangesAsync(); - _context.ChangeTracker.Clear(); - } - - // Synchronize in-memory static filter values on get - SyncLogFilters(settings); - - return settings; - } - - /// - /// Saves the analyzer settings asynchronously. - /// - public async Task SaveSettingsAsync(AnalyzerSettingsEntity settings) - { - var existing = await _context.Settings.FirstOrDefaultAsync(s => s.Id == settings.Id) - ?? await _context.Settings.FirstOrDefaultAsync(); - - if (existing == null) - { - if (settings.Id == Guid.Empty) settings.Id = Guid.NewGuid(); - settings.UpdatedAt = DateTime.UtcNow; - _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 = DateTime.UtcNow; - } - - await _context.SaveChangesAsync(); - - SyncLogFilters(settings); - - return settings; - } - - private static void SyncLogFilters(AnalyzerSettingsEntity settings) - { - LogCategoryFilter.EnableLogMqttHealthPing = settings.EnableLogMqttHealthPing; - LogCategoryFilter.EnableLogMqttGeneral = settings.EnableLogMqttGeneral; - LogCategoryFilter.EnableLogAnalyzerAuto = settings.EnableLogAnalyzerAuto; - LogCategoryFilter.EnableLogAnalyzerManual = settings.EnableLogAnalyzerManual; - LogCategoryFilter.EnableLogDatabaseOps = settings.EnableLogDatabaseOps; - } - - /// - /// Updates settings from a dictionary asynchronously. - /// - public async Task UpdateSettingsFromDictionaryAsync(Dictionary 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); - } -} diff --git a/FinlyticAnalyzer/Services/ThreeLayerFilterEngine.cs b/FinlyticAnalyzer/Services/ThreeLayerFilterEngine.cs deleted file mode 100644 index 7ae69e6..0000000 --- a/FinlyticAnalyzer/Services/ThreeLayerFilterEngine.cs +++ /dev/null @@ -1,132 +0,0 @@ -using System; -using System.Collections.Concurrent; -using FinlyticAnalyzer.Util; -using FinlyticCore.Dtos.News; -using FinlyticCore.Models.Analyzer; -using FinlyticCore.Services; - -namespace FinlyticAnalyzer.Services; - -public class ThreeLayerFilterEngine : IThreeLayerFilterEngine -{ - private readonly IFinlyticLogger _finlyticLogger; - private readonly ConcurrentDictionary _seenEvents = new(); - private readonly object _cleanupLock = new(); - private DateTime _lastCleanupTime = DateTime.UtcNow; - - public ThreeLayerFilterEngine(IFinlyticLogger finlyticLogger) - { - _finlyticLogger = finlyticLogger; - } - - /// - /// Evaluates news strictly based on ISIN and dynamic VIX market regime. - /// - public FilterResult EvaluateNews(NewsArticleDto newsEvent, VixMarketRegime regime) - { - var result = new FilterResult(); - - 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; - - if ((now - _lastCleanupTime).TotalMinutes > 30 || _seenEvents.Count > 10000) - { - lock (_cleanupLock) - { - if ((now - _lastCleanupTime).TotalMinutes > 30 || _seenEvents.Count > 10000) - { - CleanupSeenEvents(now); - } - } - } - - 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; - - 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; - } - - if (string.IsNullOrWhiteSpace(isin)) - { - result.Passed = false; - result.RejectReason = "Layer 1: Missing mandatory ISIN for news item"; - return result; - } - - result.Isin = isin; - result.Symbol = isin; - result.Sector = "General"; - - 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}"; - _ = _finlyticLogger.LogInfoAsync(SettingKeys.AnalyzerChannel, "[ThreeLayerFilterEngine] Event {EventId} (ISIN: {Isin}) rejected by Layer 2 filter. Impact: {Impact:F2}, Threshold: {Threshold:F2}, Regime: {Regime}", - eventId, isin, impactScore, requiredThreshold, regime); - return result; - } - - 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; - _ = _finlyticLogger.LogInfoAsync(SettingKeys.AnalyzerChannel, "[ThreeLayerFilterEngine] Event {EventId} passed 3-Layer Filter for ISIN {Isin}. Impact: {Impact:F2}, Regime: {Regime}", - eventId, result.Isin, impactScore, regime); - - return result; - } - - private void CleanupSeenEvents(DateTime now) - { - _lastCleanupTime = now; - foreach (var kv in _seenEvents) - { - if ((now - kv.Value).TotalHours > 12.0) - { - _seenEvents.TryRemove(kv.Key, out _); - } - } - } -} \ No newline at end of file diff --git a/FinlyticAnalyzer/Services/VixTrackerService.cs b/FinlyticAnalyzer/Services/VixTrackerService.cs deleted file mode 100644 index f6597a3..0000000 --- a/FinlyticAnalyzer/Services/VixTrackerService.cs +++ /dev/null @@ -1,100 +0,0 @@ -using System; -using System.Threading; -using System.Threading.Tasks; -using FinlyticAnalyzer.Util; -using FinlyticCore.Models.Analyzer; -using FinlyticCore.Services; -using FinlyticCore.Services.Yahoo; - -namespace FinlyticAnalyzer.Services; - -public class VixTrackerService : IVixTrackerService -{ - private readonly YahooFinanceClient _yahooClient; - private readonly IFinlyticLogger _finlyticLogger; - - private decimal _currentVix = 18.5m; - private VixMarketRegime _currentRegime = VixMarketRegime.Normal; - private readonly object _lock = new(); - - public VixTrackerService(YahooFinanceClient yahooClient, IFinlyticLogger finlyticLogger) - { - _yahooClient = yahooClient; - _finlyticLogger = finlyticLogger; - } - - 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) - { - _ = _finlyticLogger.LogWarningAsync(SettingKeys.AnalyzerChannel, "[VixTrackerService] [VIX REGIME SHIFT] Markt-Regime gewechselt: {OldRegime} -> {NewRegime} (VIX: {Vix:F2})", - oldRegime, _currentRegime, _currentVix); - } - else if (Math.Abs(oldVix - vixValue) >= 0.5m) - { - _ = _finlyticLogger.LogInfoAsync(SettingKeys.AnalyzerChannel, "[VixTrackerService] VIX aktualisiert: {Vix:F2} (Regime: {Regime})", - _currentVix, _currentRegime); - } - } - } - - public async Task 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) - { - } - catch (Exception ex) - { - await _finlyticLogger.LogWarningAsync(SettingKeys.AnalyzerChannel, ex, "[VixTrackerService] Fehler beim Abfragen von ^VIX über YahooFinanceClient. Nutze gecachten Wert {Vix}.", 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 - }; - } -} \ No newline at end of file diff --git a/FinlyticAnalyzer/Services/WinRateCalculator.cs b/FinlyticAnalyzer/Services/WinRateCalculator.cs deleted file mode 100644 index cdab052..0000000 --- a/FinlyticAnalyzer/Services/WinRateCalculator.cs +++ /dev/null @@ -1,166 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text.Json; -using FinlyticAnalyzer.Util; -using FinlyticCore.Models.Analyzer; -using FinlyticCore.Models.Trades; -using FinlyticCore.Services; - -namespace FinlyticAnalyzer.Services; - -public class WinRateCalculator : IWinRateCalculator -{ - private readonly IFinlyticLogger _finlyticLogger; - private readonly string _feedbackDir; - - private readonly object _cacheLock = new(); - private List? _cachedRecords; - private DateTime _lastCacheTime = DateTime.MinValue; - private static readonly TimeSpan CacheTtl = TimeSpan.FromMinutes(3); - - public WinRateCalculator(IFinlyticLogger finlyticLogger) - { - _finlyticLogger = finlyticLogger; - _feedbackDir = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "data", "feedback"); - if (!Directory.Exists(_feedbackDir)) - { - Directory.CreateDirectory(_feedbackDir); - } - } - - /// - /// Calculates the win rate for a given sector and symbol under the specified market regime. - /// - public double CalculateWinRate(string sector, string symbol, VixMarketRegime regime) - { - return CalculateDynamicWinRate(sector, symbol, regime); - } - - /// - /// Calculates a multi-factor dynamic AI Win-Rate / Confidence Score using technicals, sentiment, fundamentals, AI eval score, and market regime. - /// - public double CalculateDynamicWinRate( - string sector, - string symbol, - VixMarketRegime regime, - double? n8nEvalScore = null, - double? technicalScore = null, - double? sentimentScore = null, - double? fundamentalScore = null, - string signalType = "BUY") - { - try - { - double n8nComponent = 62.0; - if (n8nEvalScore.HasValue && n8nEvalScore.Value > 0) - { - n8nComponent = n8nEvalScore.Value <= 1.0 ? n8nEvalScore.Value * 100.0 : n8nEvalScore.Value; - } - - double taComponent = 60.0; - if (technicalScore.HasValue && technicalScore.Value > 0) - { - taComponent = technicalScore.Value <= 1.0 ? technicalScore.Value * 100.0 : technicalScore.Value; - } - - double sentComponent = 58.0; - if (sentimentScore.HasValue) - { - if (sentimentScore.Value >= -1.0 && sentimentScore.Value <= 1.0) - { - sentComponent = 50.0 + (sentimentScore.Value * 25.0); - } - else - { - sentComponent = sentimentScore.Value; - } - } - - double fundComponent = 60.0; - if (fundamentalScore.HasValue && fundamentalScore.Value > 0) - { - fundComponent = fundamentalScore.Value <= 1.0 ? fundamentalScore.Value * 100.0 : fundamentalScore.Value; - } - - double composite = (n8nComponent * 0.40) + (taComponent * 0.30) + (sentComponent * 0.15) + (fundComponent * 0.15); - - double vixAdjustment = regime switch - { - VixMarketRegime.LowVol => +4.0, - VixMarketRegime.Normal => +1.5, - VixMarketRegime.HighVol => -3.5, - VixMarketRegime.Panic => -8.0, - _ => 0.0 - }; - - composite += vixAdjustment; - - var records = GetCachedOrLoadRecords(); - if (records.Count > 0) - { - var matching = records.Where(r => - string.Equals(r.Sector, sector, StringComparison.OrdinalIgnoreCase) && - r.VixRegime == regime).ToList(); - - if (matching.Count >= 5) - { - int winningTrades = matching.Count(r => r.IsWin); - double historicalWinRate = (double)winningTrades / matching.Count * 100.0; - composite = (composite * 0.75) + (historicalWinRate * 0.25); - } - } - - double finalWinRate = Math.Clamp(Math.Round(composite, 1), 45.0, 92.0); - - _ = _finlyticLogger.LogInfoAsync(SettingKeys.AnalyzerChannel, "[WinRateCalculator] Dynamic Win-Rate for {Symbol} ({Sector}): {WinRate:F1}% [AI: {N8n:F1}%, TA: {TA:F1}%, Sent: {Sent:F1}%, Regime: {Regime}]", - symbol, sector, finalWinRate, n8nComponent, taComponent, sentComponent, regime); - - return finalWinRate; - } - catch (Exception ex) - { - _ = _finlyticLogger.LogWarningAsync(SettingKeys.AnalyzerChannel, ex, "[WinRateCalculator] Error calculating dynamic win-rate for {Symbol}. Fallback applied.", symbol); - return 65.0; - } - } - - private List GetCachedOrLoadRecords() - { - lock (_cacheLock) - { - if (_cachedRecords != null && (DateTime.UtcNow - _lastCacheTime) < CacheTtl) - { - return _cachedRecords; - } - - var loadedList = new List(); - - if (Directory.Exists(_feedbackDir)) - { - var jsonFiles = Directory.GetFiles(_feedbackDir, "*.json", SearchOption.AllDirectories); - foreach (var file in jsonFiles) - { - try - { - var content = File.ReadAllText(file); - var records = JsonSerializer.Deserialize(content); - if (records != null && records.Length > 0) - { - loadedList.AddRange(records); - } - } - catch (Exception ex) - { - _ = _finlyticLogger.LogWarningAsync(SettingKeys.AnalyzerChannel, ex, "[WinRateCalculator] Failed to read or parse feedback file '{File}'", file); - } - } - } - - _cachedRecords = loadedList; - _lastCacheTime = DateTime.UtcNow; - return _cachedRecords; - } - } -} diff --git a/FinlyticAnalyzer/Util/AnalyzerMqttClient.cs b/FinlyticAnalyzer/Util/AnalyzerMqttClient.cs deleted file mode 100644 index b7c5aa6..0000000 --- a/FinlyticAnalyzer/Util/AnalyzerMqttClient.cs +++ /dev/null @@ -1,931 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text.Json; -using System.Threading; -using System.Threading.Tasks; -using FinlyticAnalyzer.Database; -using FinlyticAnalyzer.Entities; -using FinlyticAnalyzer.Services; -using FinlyticCore.Dtos; -using FinlyticCore.Dtos.Settings; -using FinlyticCore.Models; -using FinlyticCore.Models.Analyzer; -using FinlyticCore.Models.Trades; -using FinlyticCore.Services; -using FinlyticCore.Util; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; - -namespace FinlyticAnalyzer.Util; - -/// -/// Unified Managed MQTT Client for FinlyticAnalyzer. -/// Handles event subscriptions, market screening, manual AI evaluation triggers, -/// and dispatches trade proposals via MQTT. -/// -public class AnalyzerMqttClient : ManagedMqttClient, IHostedService -{ - private readonly IConfiguration _configuration; - private readonly IServiceScopeFactory _scopeFactory; - private readonly IVixTrackerService _vixTracker; - private readonly IThreeLayerFilterEngine _filterEngine; - private readonly IWinRateCalculator _winRateCalculator; - private readonly IN8nEvaluationService _n8nService; - private readonly ILogger _logger; - - public AnalyzerMqttClient( - IConfiguration configuration, - IServiceScopeFactory scopeFactory, - IVixTrackerService vixTracker, - IThreeLayerFilterEngine filterEngine, - IWinRateCalculator winRateCalculator, - IN8nEvaluationService n8nService, - ILogger logger) : base(logger) - { - _configuration = configuration; - _scopeFactory = scopeFactory; - _vixTracker = vixTracker; - _filterEngine = filterEngine; - _winRateCalculator = winRateCalculator; - _n8nService = n8nService; - _logger = logger; - } - - public async Task StartAsync(CancellationToken cancellationToken) - { - var config = new MqttConfiguration - { - Host = _configuration["MQTT:Host"] ?? _configuration["MQTT__Host"] ?? "localhost", - Port = Convert.ToInt32(_configuration["MQTT:Port"] ?? _configuration["MQTT__Port"] ?? "1883"), - Username = _configuration["MQTT:Username"] ?? _configuration["MQTT__Username"], - Password = _configuration["MQTT:Password"] ?? _configuration["MQTT__Password"], - ClientId = $"{(_configuration["MQTT:ClientId"] ?? _configuration["MQTT__ClientId"] ?? "finlytic_analyzer")}_{Guid.NewGuid():N}" - }; - - _logger.LogInformation("Starting Unified Analyzer MQTT Client. Host: {Host}, ClientId: {ClientId}", config.Host, config.ClientId); - await ConnectAsync(config); - } - - public async Task StopAsync(CancellationToken cancellationToken) - { - _logger.LogInformation("Stopping Unified Analyzer MQTT Client."); - await DisconnectAsync(); - } - - protected override async Task OnConnectedAsync() - { - _logger.LogInformation("Analyzer MQTT Client connected. Subscribing to topics and RPC response channels..."); - - // Incoming Event Topics - await SubscribeAsync("services/news/#"); - await SubscribeAsync("finlytic/news/raw/#"); - await SubscribeAsync("finlytic/market/ticks/#"); - await SubscribeAsync("services/config/updated/#"); - await SubscribeAsync("services/request/health_Ping/#"); - await SubscribeAsync("services/request/analyzer_TriggerManual/#"); - await SubscribeAsync("services/request/analyzer_settings_GetAll/#"); - await SubscribeAsync("services/request/analyzer_settings_Update/#"); - await SubscribeAsync("finlytic/trades/closed/#"); - - // RPC Response Channels - await SubscribeAsync("services/response/ta_GetAnalysis/#"); - await SubscribeAsync("services/response/fundamentals_Get/#"); - await SubscribeAsync("services/response/sentiment_GetIsin/#"); - await SubscribeAsync("services/response/sentiment_Analyze/#"); - await SubscribeAsync("services/response/trades_Get/#"); - await SubscribeAsync("services/response/tr_GetLivePrice/#"); - await SubscribeAsync("services/response/events_GetByMonth/#"); - await SubscribeAsync("services/response/events_GetAll/#"); - - FinlyticCore.Services.FinlyticLogBroadcaster.OnLogPublished = async (logDto) => - { - if (IsConnected && string.Equals(logDto.ServiceName, "FinlyticAnalyzer", StringComparison.OrdinalIgnoreCase)) - { - await PublishAsync("finlytic/logs/FinlyticAnalyzer", logDto); - } - }; - - _logger.LogInformation("Successfully subscribed to all event and RPC channels."); - } - - protected override async Task OnMessageReceivedAsync(string topic, string payloadStr) - { - try - { - if (topic.Contains("health_Ping", StringComparison.OrdinalIgnoreCase)) - { - var segments = topic.Split('/'); - bool isForMe = segments.Length >= 5 - ? segments[3].Equals("FinlyticAnalyzer", StringComparison.OrdinalIgnoreCase) - : topic.Contains("FinlyticAnalyzer", StringComparison.OrdinalIgnoreCase); - - if (isForMe) - { - var correlationId = segments[^1]; - string respTopic = $"services/response/health_Ping/{correlationId}"; - var healthResp = new ServiceHealthResponse("FinlyticAnalyzer", "Online", DateTime.UtcNow, "Connected"); - await PublishAsync(respTopic, healthResp); - using var scope = _scopeFactory.CreateScope(); - var finlyticLogger = scope.ServiceProvider.GetRequiredService>(); - await finlyticLogger.LogInfoAsync(SettingKeys.HealthPingChannel, "[AnalyzerMqttClient] Responded to live health_Ping RPC request [CorrelationId: {CorrelationId}].", correlationId); - } - return; - } - - if (topic.StartsWith("services/config/updated", StringComparison.OrdinalIgnoreCase)) - { - if (topic.EndsWith("FinlyticAnalyzer", StringComparison.OrdinalIgnoreCase)) - { - try - { - var configUpdate = JsonSerializer.Deserialize(payloadStr, FinlyticJsonSerializerContext.Default.ServiceConfigUpdatePayload); - if (configUpdate?.Settings != null && configUpdate.Settings.Count > 0) - { - using var scope = _scopeFactory.CreateScope(); - var settings = scope.ServiceProvider.GetRequiredService(); - var dict = configUpdate.Settings.ToDictionary(k => k.Key, v => (object?)v.Value); - await settings.UpdateSettingsAsync(dict); - } - } - catch (Exception ex) - { - using var scope = _scopeFactory.CreateScope(); - var finlyticLogger = scope.ServiceProvider.GetRequiredService>(); - await finlyticLogger.LogErrorAsync(SettingKeys.AnalyzerChannel, ex, "[AnalyzerMqttClient] Error processing MQTT config update event."); - } - } - return; - } - - if (topic.StartsWith("finlytic/market/ticks/")) - { - ProcessTickMessage(topic, payloadStr); - } - else if (topic.StartsWith("finlytic/news/raw/", StringComparison.OrdinalIgnoreCase) || - topic.StartsWith("services/news/", StringComparison.OrdinalIgnoreCase)) - { - await ProcessNewsMessageAsync(payloadStr, CancellationToken.None); - } - else if (topic.StartsWith("services/request/analyzer_TriggerManual/")) - { - var correlationId = topic.Split('/').Last(); - await HandleManualTriggerAsync(correlationId, payloadStr, CancellationToken.None); - } - else if (topic.StartsWith("services/request/analyzer_settings_GetAll", StringComparison.OrdinalIgnoreCase)) - { - var correlationId = topic.Split('/').Last(); - await HandleSettingsGetAllAsync(correlationId); - } - else if (topic.StartsWith("services/request/analyzer_settings_Update", StringComparison.OrdinalIgnoreCase)) - { - var correlationId = topic.Split('/').Last(); - await HandleSettingsUpdateAsync(payloadStr, correlationId); - } - else if (topic.StartsWith("finlytic/trades/closed/")) - { - await HandleClosedTradeFeedbackAsync(payloadStr); - } - } - catch (Exception ex) - { - using var scope = _scopeFactory.CreateScope(); - var finlyticLogger = scope.ServiceProvider.GetRequiredService>(); - await finlyticLogger.LogErrorAsync(SettingKeys.AnalyzerChannel, ex, "[AnalyzerMqttClient] Error processing incoming MQTT message on topic {Topic}", topic); - } - } - - private async Task HandleSettingsGetAllAsync(string correlationId) - { - using var scope = _scopeFactory.CreateScope(); - var finlyticLogger = scope.ServiceProvider.GetRequiredService>(); - var settingsService = scope.ServiceProvider.GetRequiredService(); - - await finlyticLogger.LogInfoAsync(SettingKeys.MqttChannel, "[FinlyticAnalyzer] [Settings_GetAll] Retrieving all dynamic settings via reflection [CorrelationId: {CorrelationId}]", correlationId); - try - { - var settings = await settingsService.GetAllRegisteredSettingsAsync(new[] { typeof(SettingKeys) }); - var responseTopic = $"services/response/analyzer_settings_GetAll/{correlationId}"; - - await PublishAsync(responseTopic, settings); - await finlyticLogger.LogInfoAsync(SettingKeys.MqttChannel, "[FinlyticAnalyzer] [Settings_GetAll] Published {Count} settings to '{ResponseTopic}'", settings.Count, responseTopic); - } - catch (Exception ex) - { - await finlyticLogger.LogErrorAsync(SettingKeys.MqttChannel, ex, "[FinlyticAnalyzer] [Settings_GetAll] Failed to retrieve settings."); - } - } - - private async Task HandleSettingsUpdateAsync(string payload, string correlationId) - { - if (string.IsNullOrWhiteSpace(payload)) return; - - using var scope = _scopeFactory.CreateScope(); - var finlyticLogger = scope.ServiceProvider.GetRequiredService>(); - var settingsService = scope.ServiceProvider.GetRequiredService(); - - await finlyticLogger.LogInfoAsync(SettingKeys.MqttChannel, "[FinlyticAnalyzer] [Settings_Update] Processing settings update RPC [CorrelationId: {CorrelationId}]", correlationId); - try - { - Dictionary? updates = null; - try - { - updates = JsonSerializer.Deserialize>(payload); - } - catch - { - var list = JsonSerializer.Deserialize>(payload); - if (list != null) - { - updates = new Dictionary(); - foreach (var item in list) updates[item.Key] = item.Value; - } - } - - if (updates != null && updates.Count > 0) - { - await settingsService.UpdateSettingsAsync(updates); - await finlyticLogger.LogInfoAsync(SettingKeys.MqttChannel, "[FinlyticAnalyzer] [Settings_Update] Successfully updated {Count} settings in database and cache.", updates.Count); - } - - var currentSettings = await settingsService.GetAllRegisteredSettingsAsync(new[] { typeof(SettingKeys) }); - var responseTopic = $"services/response/analyzer_settings_Update/{correlationId}"; - await PublishAsync(responseTopic, currentSettings); - } - catch (Exception ex) - { - await finlyticLogger.LogErrorAsync(SettingKeys.MqttChannel, ex, "[FinlyticAnalyzer] [Settings_Update] Failed to update settings."); - } - } - - private async Task HandleClosedTradeFeedbackAsync(string payloadStr) - { - using var scope = _scopeFactory.CreateScope(); - var finlyticLogger = scope.ServiceProvider.GetRequiredService>(); - - try - { - var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; - var closedDto = JsonSerializer.Deserialize(payloadStr, options); - - if (closedDto != null && !string.IsNullOrWhiteSpace(closedDto.TradeId)) - { - bool isWin = closedDto.Status?.Contains("Profit", StringComparison.OrdinalIgnoreCase) == true || - closedDto.Status?.Contains("Win", StringComparison.OrdinalIgnoreCase) == true; - - var feedback = new TradeFeedbackRecord - { - TradeId = closedDto.TradeId, - AnalysisId = closedDto.AnalysisId, - Sector = closedDto.Sector, - Symbol = closedDto.Symbol, - Isin = closedDto.Isin, - EntryPrice = closedDto.EntryPrice, - StopLoss = closedDto.StopLoss, - TakeProfit = closedDto.TakeProfit, - IsWin = isWin, - VixRegime = closedDto.VixRegime, - VixValue = closedDto.VixValue, - CreatedAt = closedDto.CreatedAt, - ClosedAt = DateTime.UtcNow - }; - - string feedbackDir = System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "data", "feedback"); - if (!System.IO.Directory.Exists(feedbackDir)) - { - System.IO.Directory.CreateDirectory(feedbackDir); - } - - string filePath = System.IO.Path.Combine(feedbackDir, $"{closedDto.TradeId}.json"); - await System.IO.File.WriteAllTextAsync(filePath, JsonSerializer.Serialize(new[] { feedback }, options)); - - await finlyticLogger.LogInfoAsync(SettingKeys.AnalyzerChannel, "[AnalyzerMqttClient] Processed closed trade feedback for {TradeId}. Saved to {FilePath}", closedDto.TradeId, filePath); - } - } - catch (Exception ex) - { - await finlyticLogger.LogErrorAsync(SettingKeys.AnalyzerChannel, ex, "[AnalyzerMqttClient] Error processing closed trade feedback."); - } - } - - private async Task HandleManualTriggerAsync(string correlationId, string payloadStr, CancellationToken cancellationToken) - { - using var scope = _scopeFactory.CreateScope(); - var finlyticLogger = scope.ServiceProvider.GetRequiredService>(); - - try - { - var manualReq = JsonSerializer.Deserialize(payloadStr, FinlyticJsonSerializerContext.Default.ManualAnalysisRpcRequest); - if (manualReq == null || string.IsNullOrWhiteSpace(manualReq.Isin)) - { - await finlyticLogger.LogWarningAsync(SettingKeys.AnalyzerChannel, "[AnalyzerMqttClient] Manual trigger received without valid request or ISIN."); - return; - } - - await finlyticLogger.LogInfoAsync(SettingKeys.AnalyzerChannel, "[ManualAnalyzer] [TRIGGERED] Processing rich manual trigger for ISIN '{Isin}' (Symbol: {Symbol}). CorrelationId: {CorrelationId}", manualReq.Isin, manualReq.Symbol, correlationId); - - var dbContext = scope.ServiceProvider.GetRequiredService(); - - var regime = _vixTracker.GetCurrentRegime(); - var currentVix = _vixTracker.GetCurrentVix(); - string analysisId = Guid.NewGuid().ToString("N"); - double winRate = _winRateCalculator.CalculateWinRate(manualReq.Sector, manualReq.Symbol, regime); - - string riskLabel = manualReq.RiskScore > 70 ? $"Aggressiv ({manualReq.RiskScore}/100)" : (manualReq.RiskScore > 30 ? $"Balanced ({manualReq.RiskScore}/100)" : $"Konservativ ({manualReq.RiskScore}/100)"); - string timeframeFormatted = $"{manualReq.MinTimeframeValue}-{manualReq.MaxTimeframeValue} {manualReq.TimeframeUnit}"; - - var n8nRequest = new N8nAnalysisRequestDto - { - RequestId = analysisId, - Timestamp = DateTime.UtcNow, - TriggerType = "Manual", - TargetAsset = new TargetAssetInfo - { - Symbol = manualReq.FundamentalsData?.Fundamentals?.Ticker?.Ticker ?? manualReq.FundamentalsData?.Asset?.PrimaryTicker?.Ticker ?? manualReq.Symbol.ToUpperInvariant(), - Name = !string.IsNullOrWhiteSpace(manualReq.FundamentalsData?.Asset?.Name) ? manualReq.FundamentalsData.Asset.Name : manualReq.Isin.ToUpperInvariant(), - Isin = manualReq.Isin.ToUpperInvariant(), - Sector = manualReq.Sector - }, - MarketContext = new MarketContextInfo - { - Vix = currentVix, - MarketRegime = regime.ToString() - }, - FilterContext = new FilterContextInfo - { - ImpactScore = 1.0, - RawNewsHeadline = string.IsNullOrWhiteSpace(manualReq.Headline) ? "Manual User Trigger" : manualReq.Headline - }, - UserPreferences = new UserPreferencesInfo - { - RiskScore = manualReq.RiskScore, - RiskTolerance = riskLabel, - MinTimeframeValue = manualReq.MinTimeframeValue, - MaxTimeframeValue = manualReq.MaxTimeframeValue, - TimeframeUnit = manualReq.TimeframeUnit, - TimeframeFormatted = timeframeFormatted, - InstrumentType = manualReq.InstrumentType, - UserNotes = manualReq.UserNotes - }, - TradeFeedback = new TradeFeedbackInfo - { - TotalAssetTrades = 0, - AssetWinRate = winRate, - AvgReturnPercent = 0.0, - LastTradeResult = "UNKNOWN" - }, - TechnicalContext = new TechnicalContextInfo - { - Rsi = manualReq.TaData?.Indicators?.LastOrDefault()?.Rsi14?.ToString("F1") ?? "N/A", - SupertrendStatus = manualReq.TaData?.Indicators?.LastOrDefault()?.SupertrendDirection ?? "NEUTRAL", - Atr = manualReq.TaData?.Indicators?.LastOrDefault()?.Atr14?.ToString("F2") ?? "N/A", - Sma50 = (double?)manualReq.TaData?.Indicators?.LastOrDefault()?.Sma50, - Sma200 = (double?)manualReq.TaData?.Indicators?.LastOrDefault()?.Sma200, - DetectedPatterns = manualReq.TaData?.Patterns?.Select(p => new PatternContextInfo - { - PatternName = p.Type, - BreakoutDirection = p.BreakoutSignal?.Direction, - TargetPrice = (double?)p.BreakoutSignal?.TargetPrice, - PotentialPercent = (double?)p.BreakoutSignal?.PotentialPercent - }).ToList() ?? new List() - }, - SentimentContext = new SentimentContextInfo - { - AssetSentimentScore = manualReq.SentimentData?.CurrentSummary?.CompoundScore ?? 0.0, - SectorSentimentScore = 0.0, - NewsSentimentSummary = manualReq.SentimentData?.CurrentSummary?.SentimentLabel ?? "Neutral" - }, - FundamentalContext = new FundamentalContextInfo - { - PeRatio = (double?)manualReq.FundamentalsData?.Fundamentals?.TrailingPe, - ForwardPeRatio = (double?)manualReq.FundamentalsData?.Fundamentals?.ForwardPe, - PegRatio = (double?)manualReq.FundamentalsData?.Fundamentals?.PegRatio, - MarketCap = (double?)manualReq.FundamentalsData?.Fundamentals?.MarketCap, - DebtToEquity = (double?)manualReq.FundamentalsData?.Fundamentals?.DebtToEquity, - GrossMargin = (double?)manualReq.FundamentalsData?.Fundamentals?.GrossProfit, - NetProfitMargin = (double?)manualReq.FundamentalsData?.Fundamentals?.NetIncome, - ReturnOnEquity = (double?)manualReq.FundamentalsData?.Fundamentals?.ReturnOnEquity, - DividendYield = (double?)manualReq.FundamentalsData?.Fundamentals?.ForwardDividendYield, - ShortPercentOfFloat = null, - AnalystTargetMedian = null, - EvToEbitda = (double?)manualReq.FundamentalsData?.Fundamentals?.EvToEbitda - } - }; - - var n8nResponse = await _n8nService.EvaluateAssetAsync(n8nRequest, cancellationToken); - - var settingsService = scope.ServiceProvider.GetRequiredService(); - double minSignalScore = await settingsService.GetSettingAsync(SettingKeys.MinWinRateThreshold, cancellationToken); - - double dynamicWinRate = _winRateCalculator.CalculateDynamicWinRate( - manualReq.Sector, - manualReq.Symbol, - regime, - n8nEvalScore: n8nResponse?.EvalScore, - sentimentScore: manualReq.SentimentData?.CurrentSummary?.CompoundScore, - signalType: n8nResponse?.SuggestedDirection ?? "BUY"); - - double confidenceScore = n8nResponse?.EvalScore > 0 ? n8nResponse.EvalScore : (dynamicWinRate / 100.0); - bool shouldProceed = n8nResponse != null && - string.Equals(n8nResponse.AiDecision, "Proceed", StringComparison.OrdinalIgnoreCase) && - (confidenceScore * 100.0) >= minSignalScore && - dynamicWinRate >= minSignalScore; - - TradeProposalDto? proposalDto = null; - if (n8nResponse != null) - { - proposalDto = new TradeProposalDto - { - TradeId = "PROP-" + Guid.NewGuid().ToString("N")[..10].ToUpperInvariant(), - AnalysisId = analysisId, - EventId = analysisId, - Sector = manualReq.Sector, - Symbol = manualReq.Symbol.ToUpperInvariant(), - Isin = manualReq.Isin.ToUpperInvariant(), - CompanyName = !string.IsNullOrWhiteSpace(manualReq.FundamentalsData?.Asset?.Name) ? manualReq.FundamentalsData.Asset.Name : manualReq.Symbol, - EntryPrice = manualReq.CurrentPrice, - SignalType = string.Equals(n8nResponse.SuggestedDirection, "Short", StringComparison.OrdinalIgnoreCase) ? "SELL" : "BUY", - Status = shouldProceed ? "Proposed" : "Rejected", - RiskTolerance = n8nResponse.SuggestedRisk, - Timeframe = timeframeFormatted, - InstrumentType = manualReq.InstrumentType, - WinRate = dynamicWinRate, - VixRegime = regime, - VixValue = currentVix, - TtlMinutes = 60, - Reasoning = $"Manual n8n Evaluation ({n8nResponse.AiDecision}): {n8nResponse.AiReasoning}", - - StopLoss = n8nResponse.ExecutionPlan?.StopLoss ?? 0, - TakeProfit = n8nResponse.ExecutionPlan?.TakeProfitTargets != null && n8nResponse.ExecutionPlan.TakeProfitTargets.Count > 0 ? n8nResponse.ExecutionPlan.TakeProfitTargets[0] : 0, - EntryZoneMin = n8nResponse.ExecutionPlan?.EntryZone?.Min, - EntryZoneMax = n8nResponse.ExecutionPlan?.EntryZone?.Max, - TakeProfitTargets = n8nResponse.ExecutionPlan?.TakeProfitTargets, - RiskRewardRatio = n8nResponse.ExecutionPlan?.RiskRewardRatio, - MaxLeverage = n8nResponse.ExecutionPlan?.MaxLeverage, - TechnicalRationale = n8nResponse.DetailedAnalysis?.TechnicalRationale ?? string.Empty, - FundamentalRationale = n8nResponse.DetailedAnalysis?.FundamentalRationale ?? string.Empty, - RiskWarning = n8nResponse.DetailedAnalysis?.RiskWarning ?? string.Empty, - - CreatedAt = DateTime.UtcNow - }; - } - - var analysisEntity = new AnalysisEntity - { - AnalysisId = analysisId, - EventId = analysisId, - Sector = manualReq.Sector, - Symbol = manualReq.Symbol.ToUpperInvariant(), - Isin = manualReq.Isin.ToUpperInvariant(), - VixRegime = regime, - VixValue = currentVix, - ImpactScore = 1.0, - WinRate = dynamicWinRate, - RawDataJson = JsonSerializer.Serialize(manualReq), - AiOutputJson = proposalDto != null ? JsonSerializer.Serialize(proposalDto) : "{}", - N8nResponseJson = n8nResponse != null ? JsonSerializer.Serialize(n8nResponse) : "{}", - N8nEvalScore = n8nResponse?.EvalScore ?? 0, - N8nDecision = n8nResponse?.AiDecision ?? "Rejected", - IsTradeProposed = shouldProceed, - CreatedAt = DateTime.UtcNow - }; - - dbContext.Analyses.Add(analysisEntity); - await dbContext.SaveChangesAsync(cancellationToken); - - var responseTopic = $"services/response/analyzer_TriggerManual/{correlationId}"; - var responsePayload = new ManualAnalysisResponseDto - { - AnalysisId = analysisId, - IsTradeProposed = shouldProceed, - Status = shouldProceed ? "Success" : "Rejected", - Recommendation = shouldProceed ? "RECOMMENDED" : "NOT_RECOMMENDED", - N8nResponse = n8nResponse, - Proposal = proposalDto - }; - - await PublishAsync(responseTopic, responsePayload); - - if (proposalDto != null && shouldProceed) - { - string propTopic = $"finlytic/trades/proposed/{(string.IsNullOrWhiteSpace(manualReq.Sector) ? "general" : manualReq.Sector.ToLowerInvariant())}/{manualReq.Symbol.ToLowerInvariant()}"; - await PublishAsync(propTopic, proposalDto); - await finlyticLogger.LogInfoAsync(SettingKeys.AnalyzerChannel, "[ManualAnalyzer] [DISPATCHED] Dispatched Manual Trade Proposal {AnalysisId} to topic {Topic}", analysisId, propTopic); - } - } - catch (Exception ex) - { - await finlyticLogger.LogErrorAsync(SettingKeys.AnalyzerChannel, ex, "[AnalyzerMqttClient] Failed to handle manual trigger for correlation {CorrelationId}.", correlationId); - try - { - var errorResponse = new ManualAnalysisResponseDto - { - Status = "ERROR", - Message = $"Analysis failed: {ex.Message}" - }; - await PublishAsync($"services/response/analyzer_TriggerManual/{correlationId}", errorResponse); - } - catch (Exception pubEx) - { - await finlyticLogger.LogErrorAsync(SettingKeys.AnalyzerChannel, pubEx, "[AnalyzerMqttClient] Failed to publish error response for correlation {CorrelationId}.", correlationId); - } - } - } - - private void ProcessTickMessage(string topic, string payloadStr) - { - if (topic.EndsWith("VIX", StringComparison.OrdinalIgnoreCase) || topic.EndsWith("^VIX", StringComparison.OrdinalIgnoreCase)) - { - try - { - var tick = JsonSerializer.Deserialize(payloadStr, FinlyticJsonSerializerContext.Default.TickMessageDto); - if (tick != null && tick.Price > 0) - { - _vixTracker.UpdateVixFromTick(tick.Price); - } - } - catch (Exception ex) - { - _logger.LogWarning(ex, "Failed to parse VIX tick message."); - } - } - } - - private async Task ProcessNewsMessageAsync(string payloadStr, CancellationToken cancellationToken) - { - using var scope = _scopeFactory.CreateScope(); - var finlyticLogger = scope.ServiceProvider.GetRequiredService>(); - - var newsArticle = JsonSerializer.Deserialize(payloadStr, FinlyticJsonSerializerContext.Default.NewsArticleDto); - if (newsArticle == null) return; - - var regime = _vixTracker.GetCurrentRegime(); - var currentVix = _vixTracker.GetCurrentVix(); - - var filterResult = _filterEngine.EvaluateNews(newsArticle, regime); - if (!filterResult.Passed) - { - await finlyticLogger.LogInfoAsync(SettingKeys.AnalyzerChannel, "[AutoScreener] [SKIPPED] News message skipped for ISIN '{Isin}'. Reason: {Reason}", filterResult.Isin, filterResult.RejectReason); - return; - } - - await finlyticLogger.LogInfoAsync(SettingKeys.AnalyzerChannel, "[AutoScreener] [TRIGGERED] Screening market opportunity for ISIN '{Isin}'...", filterResult.Isin); - - string analysisId = Guid.NewGuid().ToString("N"); - string eventId = newsArticle.Id != Guid.Empty ? newsArticle.Id.ToString() : analysisId; - string rawHeadline = newsArticle.Title ?? string.Empty; - - double winRate = _winRateCalculator.CalculateWinRate(filterResult.Sector, filterResult.Symbol, regime); - - int riskScore = 50; - string riskTolerance = "Balanced (50/100)"; - int minTf = 4; - int maxTf = 7; - - if (winRate < 45.0) - { - riskScore = 30; - riskTolerance = "Konservativ (30/100)"; - minTf = 7; - maxTf = 14; - } - else if (winRate >= 65.0) - { - riskScore = 75; - riskTolerance = "Aggressiv (75/100)"; - minTf = 1; - maxTf = 4; - } - - TechnicalContextInfo taInfo = new(); - FundamentalContextInfo fundInfo = new(); - SentimentContextInfo sentInfo = new(); - - string resolvedSymbol = filterResult.Symbol; - string resolvedName = filterResult.Symbol; - - if (newsArticle.MatchedAssets != null && newsArticle.MatchedAssets.Count > 0) - { - var firstAsset = newsArticle.MatchedAssets[0]; - if (!string.IsNullOrWhiteSpace(firstAsset.Name)) - { - resolvedName = firstAsset.Name; - if (resolvedSymbol == "UNKNOWN" || resolvedSymbol == filterResult.Isin) - { - resolvedSymbol = resolvedName; - } - } - } - - FinlyticCore.Dtos.TechnicalAnalysis.TechnicalAnalysisDto? taResp = null; - FinlyticCore.Dtos.Fundamentals.AssetFundamentalsDto? fundResp = null; - FinlyticCore.Dtos.TechnicalAnalysis.LivePriceDto? livePriceResp = null; - FinlyticCore.Dtos.Sentiment.IsinSentimentSummaryDto? sentResp = null; - - try - { - if (IsConnected) - { - var isinReq = new IsinRequest(filterResult.Isin); - - var livePriceTask = SendRpcRequestAsync( - "tr_GetLivePrice", isinReq, TimeSpan.FromSeconds(5)); - var taTask = SendRpcRequestAsync( - "ta_GetAnalysis", isinReq, TimeSpan.FromSeconds(5)); - var fundTask = SendRpcRequestAsync( - "fundamentals_Get", isinReq, TimeSpan.FromSeconds(5)); - var sentTask = SendRpcRequestAsync( - "sentiment_GetIsin", isinReq, TimeSpan.FromSeconds(5)); - - await Task.WhenAll(livePriceTask, taTask, fundTask, sentTask); - - livePriceResp = livePriceTask.Result; - taResp = taTask.Result; - fundResp = fundTask.Result; - sentResp = sentTask.Result; - - if (taResp?.Indicators != null) - { - var latestIndicator = taResp.Indicators.LastOrDefault(); - taInfo = new TechnicalContextInfo - { - Rsi = latestIndicator?.Rsi14?.ToString("F1") ?? "50.0", - SupertrendStatus = latestIndicator?.SupertrendDirection ?? "NEUTRAL", - Atr = latestIndicator?.Atr14?.ToString("F2") ?? "0.0", - Sma50 = (double?)latestIndicator?.Sma50, - Sma200 = (double?)latestIndicator?.Sma200, - DetectedPatterns = taResp.Patterns?.Select(p => new PatternContextInfo - { - PatternName = p.Type, - BreakoutDirection = p.BreakoutSignal?.Direction, - TargetPrice = (double?)p.BreakoutSignal?.TargetPrice, - PotentialPercent = (double?)p.BreakoutSignal?.PotentialPercent - }).ToList() ?? new List() - }; - } - - if (fundResp != null) - { - string? fundTicker = fundResp.Fundamentals?.Ticker?.Ticker ?? fundResp.Asset?.PrimaryTicker?.Ticker; - resolvedSymbol = !string.IsNullOrWhiteSpace(fundTicker) ? fundTicker : resolvedSymbol; - resolvedName = !string.IsNullOrWhiteSpace(fundResp.Asset?.Name) ? fundResp.Asset.Name : resolvedName; - - fundInfo = new FundamentalContextInfo - { - PeRatio = (double?)fundResp.Fundamentals?.TrailingPe, - ForwardPeRatio = (double?)fundResp.Fundamentals?.ForwardPe, - PegRatio = (double?)fundResp.Fundamentals?.PegRatio, - MarketCap = (double?)fundResp.Fundamentals?.MarketCap, - DebtToEquity = (double?)fundResp.Fundamentals?.DebtToEquity, - GrossMargin = (double?)fundResp.Fundamentals?.GrossProfit, - NetProfitMargin = (double?)fundResp.Fundamentals?.NetIncome, - ReturnOnEquity = (double?)fundResp.Fundamentals?.ReturnOnEquity, - DividendYield = (double?)fundResp.Fundamentals?.ForwardDividendYield, - ShortPercentOfFloat = null, - AnalystTargetMedian = null, - EvToEbitda = (double?)fundResp.Fundamentals?.EvToEbitda - }; - } - - if (sentResp != null) - { - double compound = sentResp.CurrentSummary?.CompoundScore ?? 0.0; - double normalizedScore = Math.Clamp((compound + 1.0) / 2.0, 0.0, 1.0); - - sentInfo = new SentimentContextInfo - { - AssetSentimentScore = Math.Round(normalizedScore, 2), - SectorSentimentScore = Math.Round(normalizedScore, 2), - NewsSentimentSummary = string.IsNullOrWhiteSpace(sentResp.CurrentSummary?.SentimentLabel) ? "Neutral" : sentResp.CurrentSummary.SentimentLabel - }; - } - } - } - catch (Exception ex) - { - await finlyticLogger.LogWarningAsync(SettingKeys.AnalyzerChannel, ex, "[AnalyzerMqttClient] Failed to fetch context data for auto screener analysis."); - } - - var n8nRequest = new N8nAnalysisRequestDto - { - RequestId = analysisId, - Timestamp = DateTime.UtcNow, - TriggerType = "AutoScreener", - TargetAsset = new TargetAssetInfo - { - Symbol = resolvedSymbol.ToUpperInvariant(), - Name = resolvedName, - Isin = filterResult.Isin.ToUpperInvariant(), - Sector = filterResult.Sector - }, - MarketContext = new MarketContextInfo - { - Vix = currentVix, - MarketRegime = regime.ToString() - }, - FilterContext = new FilterContextInfo - { - ImpactScore = filterResult.ImpactScore, - RawNewsHeadline = rawHeadline - }, - UserPreferences = new UserPreferencesInfo - { - RiskScore = riskScore, - RiskTolerance = riskTolerance, - MinTimeframeValue = minTf, - MaxTimeframeValue = maxTf, - TimeframeUnit = "Tage", - TimeframeFormatted = $"{minTf}-{maxTf} Tage", - InstrumentType = "KnockOut", - UserNotes = "High-Conviction Screener Mode: Evaluate underlying data for strong reliable chart moves." - }, - TradeFeedback = new TradeFeedbackInfo - { - TotalAssetTrades = 0, - AssetWinRate = winRate, - AvgReturnPercent = 0.0, - LastTradeResult = "UNKNOWN" - }, - TechnicalContext = taInfo, - SentimentContext = sentInfo, - FundamentalContext = fundInfo - }; - - var n8nResponse = await _n8nService.EvaluateAssetAsync(n8nRequest, cancellationToken); - - var settingsService = scope.ServiceProvider.GetRequiredService(); - double minSignalScore = await settingsService.GetSettingAsync(SettingKeys.MinWinRateThreshold, cancellationToken); - - double confidenceScore = n8nResponse?.EvalScore > 0 ? n8nResponse.EvalScore : 0.75; - bool isHighConviction = n8nResponse != null && - string.Equals(n8nResponse.AiDecision, "Proceed", StringComparison.OrdinalIgnoreCase) && - (confidenceScore * 100.0) >= minSignalScore && - winRate >= minSignalScore; - - string finalSymbol = !string.IsNullOrWhiteSpace(resolvedSymbol) && resolvedSymbol != "UNKNOWN" - ? resolvedSymbol - : (!string.IsNullOrWhiteSpace(filterResult.Symbol) && filterResult.Symbol != "UNKNOWN" ? filterResult.Symbol : filterResult.Isin); - - string finalName = !string.IsNullOrWhiteSpace(resolvedName) && resolvedName != "UNKNOWN" - ? resolvedName - : finalSymbol; - - string marketRegion = filterResult.Isin.StartsWith("DE", StringComparison.OrdinalIgnoreCase) ? "GERMAN_EQUITIES" : "US_EQUITIES"; - - var supportLevels = new List(); - var resistanceLevels = new List(); - - double currentPrice = (double)(livePriceResp?.CurrentPrice > 0 ? livePriceResp.CurrentPrice : 0.0m); - if (currentPrice > 0) - { - supportLevels.Add(Math.Round(currentPrice * 0.98, 2)); - supportLevels.Add(Math.Round(currentPrice * 0.95, 2)); - resistanceLevels.Add(Math.Round(currentPrice * 1.03, 2)); - resistanceLevels.Add(Math.Round(currentPrice * 1.06, 2)); - } - - if (n8nResponse?.ExecutionPlan?.EntryZone != null) - { - if (n8nResponse.ExecutionPlan.EntryZone.Min > 0) supportLevels.Insert(0, (double)n8nResponse.ExecutionPlan.EntryZone.Min); - if (n8nResponse.ExecutionPlan.EntryZone.Max > 0) resistanceLevels.Insert(0, (double)n8nResponse.ExecutionPlan.EntryZone.Max); - } - - var recommendation = new AssetRecommendationDto - { - Mode = "AUTO_SCREENER", - Timestamp = DateTime.UtcNow, - RecommendedAsset = new RecommendedAssetInfo - { - Symbol = finalSymbol, - CompanyName = finalName, - Isin = filterResult.Isin, - Market = marketRegion, - Bias = string.Equals(n8nResponse?.SuggestedDirection, "Short", StringComparison.OrdinalIgnoreCase) ? "BEARISH" : "BULLISH", - ConfidenceScore = Math.Round(confidenceScore, 2), - Timeframe = !string.IsNullOrWhiteSpace(n8nResponse?.SuggestedTimeframe) ? n8nResponse.SuggestedTimeframe : "1D" - }, - Rationale = new RecommendationRationaleInfo - { - PatternDetected = taInfo.DetectedPatterns?.Count > 0 - ? string.Join(", ", taInfo.DetectedPatterns.Select(p => p.PatternName)) - : (!string.IsNullOrWhiteSpace(n8nResponse?.DetailedAnalysis?.TechnicalRationale) ? n8nResponse.DetailedAnalysis.TechnicalRationale : "Multi-Timeframe Trend & Volume Confluence"), - VixContext = $"VIX at {currentVix:F1} ({regime} volatility environment)", - KeyTechnicalLevels = new KeyTechnicalLevelsInfo - { - Support = supportLevels.Distinct().ToList(), - Resistance = resistanceLevels.Distinct().ToList() - }, - Summary = !string.IsNullOrWhiteSpace(n8nReasoning(n8nResponse)) - ? n8nResponse!.AiReasoning - : "High conviction setup based on multi-timeframe technical confluence, sentiment, and fundamental data." - }, - ActionRequired = isHighConviction ? "PROMPT_USER_FOR_MANUAL_TRADE" : "NO_ACTION" - }; - - double dynamicWinRate = _winRateCalculator.CalculateDynamicWinRate( - filterResult.Sector, - finalSymbol, - regime, - n8nEvalScore: n8nResponse?.EvalScore, - sentimentScore: sentResp?.CurrentSummary?.CompoundScore, - signalType: n8nResponse?.SuggestedDirection ?? "BUY"); - - var dbContext = scope.ServiceProvider.GetRequiredService(); - - bool hasRecentProposal = await dbContext.Analyses.AnyAsync(a => - a.Isin == filterResult.Isin && - a.IsTradeProposed && - a.CreatedAt >= DateTime.UtcNow.AddHours(-4), - cancellationToken); - - if (hasRecentProposal && isHighConviction) - { - await finlyticLogger.LogInfoAsync(SettingKeys.AnalyzerChannel, "[AutoScreener] Asset {Symbol} ({Isin}) already has an active trade proposal in the last 4 hours. Skipping duplicate trade proposal generation.", - finalSymbol, filterResult.Isin); - isHighConviction = false; - } - - var analysisEntity = new AnalysisEntity - { - AnalysisId = analysisId, - EventId = eventId, - Sector = filterResult.Sector, - Symbol = finalSymbol, - Isin = filterResult.Isin, - VixRegime = regime, - VixValue = currentVix, - ImpactScore = filterResult.ImpactScore, - WinRate = dynamicWinRate, - RawDataJson = payloadStr, - AiOutputJson = JsonSerializer.Serialize(recommendation), - N8nResponseJson = n8nResponse != null ? JsonSerializer.Serialize(n8nResponse) : "{}", - N8nEvalScore = n8nResponse?.EvalScore ?? 0, - N8nDecision = n8nResponse?.AiDecision ?? "None", - IsTradeProposed = isHighConviction, - CreatedAt = DateTime.UtcNow - }; - - dbContext.Analyses.Add(analysisEntity); - await dbContext.SaveChangesAsync(cancellationToken); - - if (isHighConviction && n8nResponse != null) - { - var autoProposalDto = new TradeProposalDto - { - TradeId = "PROP-" + Guid.NewGuid().ToString("N")[..10].ToUpperInvariant(), - AnalysisId = analysisId, - EventId = eventId, - Sector = filterResult.Sector, - Symbol = finalSymbol, - Isin = filterResult.Isin, - CompanyName = finalName, - EntryPrice = (decimal)currentPrice, - SignalType = string.Equals(n8nResponse.SuggestedDirection, "Short", StringComparison.OrdinalIgnoreCase) ? "SELL" : "BUY", - Status = "Proposed", - RiskTolerance = n8nResponse.SuggestedRisk ?? "Balanced", - Timeframe = $"{minTf}-{maxTf} Tage", - InstrumentType = "KnockOut", - WinRate = dynamicWinRate, - VixRegime = regime, - VixValue = currentVix, - TtlMinutes = 180, - Reasoning = n8nResponse.AiReasoning ?? "Auto-Screener High Conviction Trade", - StopLoss = n8nResponse.ExecutionPlan?.StopLoss ?? 0, - TakeProfit = n8nResponse.ExecutionPlan?.TakeProfitTargets != null && n8nResponse.ExecutionPlan.TakeProfitTargets.Count > 0 ? n8nResponse.ExecutionPlan.TakeProfitTargets[0] : 0, - EntryZoneMin = n8nResponse.ExecutionPlan?.EntryZone?.Min, - EntryZoneMax = n8nResponse.ExecutionPlan?.EntryZone?.Max, - TakeProfitTargets = n8nResponse.ExecutionPlan?.TakeProfitTargets, - RiskRewardRatio = n8nResponse.ExecutionPlan?.RiskRewardRatio, - MaxLeverage = n8nResponse.ExecutionPlan?.MaxLeverage, - TechnicalRationale = n8nResponse.DetailedAnalysis?.TechnicalRationale ?? string.Empty, - FundamentalRationale = n8nResponse.DetailedAnalysis?.FundamentalRationale ?? string.Empty, - RiskWarning = n8nResponse.DetailedAnalysis?.RiskWarning ?? string.Empty, - CreatedAt = DateTime.UtcNow - }; - - string propTopic = $"finlytic/trades/proposed/{(string.IsNullOrWhiteSpace(filterResult.Sector) ? "general" : filterResult.Sector.ToLowerInvariant())}/{finalSymbol.ToLowerInvariant()}"; - await PublishAsync(propTopic, autoProposalDto); - await finlyticLogger.LogInfoAsync(SettingKeys.AnalyzerChannel, "[AutoScreener] Dispatched High-Conviction Proposal {TradeId} to topic {Topic}", autoProposalDto.TradeId, propTopic); - } - - if (isHighConviction) - { - string recTopic = $"finlytic/recommendations/auto/{(string.IsNullOrWhiteSpace(filterResult.Sector) ? "general" : filterResult.Sector.ToLowerInvariant())}/{finalSymbol.ToLowerInvariant()}"; - await PublishAsync(recTopic, recommendation); - await PublishAsync("finlytic/recommendations/auto", recommendation); - - await finlyticLogger.LogInfoAsync(SettingKeys.AnalyzerChannel, "[AutoScreener] [RECOMMENDED] High-Conviction Opportunity found for {Symbol} (Bias: {Bias}, Confidence: {Score:F2}). Published to {Topic}", - finalSymbol, recommendation.RecommendedAsset.Bias, recommendation.RecommendedAsset.ConfidenceScore, recTopic); - } - else - { - await finlyticLogger.LogInfoAsync(SettingKeys.AnalyzerChannel, "[AutoScreener] [DROPPED] Low-conviction signal for {Symbol} dropped (Confidence: {Score:F2}, Action: NO_ACTION)", - finalSymbol, recommendation.RecommendedAsset.ConfidenceScore); - } - } - - private static string n8nReasoning(N8nAnalysisResponseDto? resp) => resp?.AiReasoning ?? string.Empty; -} \ No newline at end of file diff --git a/FinlyticAnalyzer/Util/SettingKeys.cs b/FinlyticAnalyzer/Util/SettingKeys.cs deleted file mode 100644 index de70a65..0000000 --- a/FinlyticAnalyzer/Util/SettingKeys.cs +++ /dev/null @@ -1,33 +0,0 @@ -using FinlyticCore.Models.Settings; - -namespace FinlyticAnalyzer.Util; - -public static class SettingKeys -{ - // --- Logging-Kanäle --- - public static readonly SettingKey AnalyzerChannel = new("Logging.Channel.Analyzer", true); - public static readonly SettingKey MqttChannel = new("Logging.Channel.MQTT", true); - public static readonly SettingKey HealthPingChannel = new("Logging.Channel.Health", true); - - // --- Makro & VIX Schwellenwerte --- - public static readonly SettingKey VixPanicThreshold = new("Macro.VixPanicThreshold", 28.0); - public static readonly SettingKey VixElevatedThreshold = new("Macro.VixElevatedThreshold", 20.0); - public static readonly SettingKey VixPollIntervalSeconds = new("Macro.VixPollIntervalSeconds", 60); - - // --- Filter & Winrate-Logik --- - public static readonly SettingKey MinWinRateThreshold = new("Filter.MinWinRateThreshold", 60.0); - public static readonly SettingKey WeightMacro = new("Filter.WeightMacro", 0.30); - public static readonly SettingKey WeightFundamental = new("Filter.WeightFundamental", 0.30); - public static readonly SettingKey WeightSentiment = new("Filter.WeightSentiment", 0.20); - public static readonly SettingKey WeightTechnical = new("Filter.WeightTechnical", 0.20); - - // --- Trade & Risiko-Parameter --- - public static readonly SettingKey DefaultTakeProfitPercent = new("Trade.DefaultTakeProfitPercent", 15.0); - public static readonly SettingKey DefaultStopLossPercent = new("Trade.DefaultStopLossPercent", 5.0); - public static readonly SettingKey MaxAllowedLeverage = new("Trade.MaxAllowedLeverage", 10); - public static readonly SettingKey MaxRiskPerTradePercent = new("Trade.MaxRiskPerTradePercent", 2.0); - public static readonly SettingKey ProposalValidityHours = new("Trade.ProposalValidityHours", 24); - - // --- N8N / Webhook-Konfiguration --- - public static readonly SettingKey N8nWebhookUrl = new("N8N.WebhookUrl", "https://n8n.kleidukos.me/webhook/gemini/analysis/auto"); -} diff --git a/FinlyticAnalyzer/appsettings.json b/FinlyticAnalyzer/appsettings.json deleted file mode 100644 index 4f007b9..0000000 --- a/FinlyticAnalyzer/appsettings.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "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" - } -} diff --git a/FinlyticTechnicalAnalysis/Database/TechnicalAnalysisDbContext.cs b/FinlyticTechnicalAnalysis/Database/TechnicalAnalysisDbContext.cs deleted file mode 100644 index 6a396e5..0000000 --- a/FinlyticTechnicalAnalysis/Database/TechnicalAnalysisDbContext.cs +++ /dev/null @@ -1,48 +0,0 @@ -using FinlyticCore.Database; -using FinlyticCore.Entities.Settings; -using FinlyticTechnicalAnalysis.Entities; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Design; - -namespace FinlyticTechnicalAnalysis.Database; - -public class TechnicalAnalysisDbContext : DbContext, ISettingsDbContext -{ - public TechnicalAnalysisDbContext(DbContextOptions options) : base(options) - { - } - - public DbSet DynamicSettings => Set(); - public DbSet MarketCandles => Set(); - public DbSet MacroData => Set(); - public DbSet CachedAnalyses => Set(); - public DbSet Settings => Set(); - - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - base.OnModelCreating(modelBuilder); - - modelBuilder.Entity(entity => - { - entity.HasKey(e => e.Id); - entity.HasIndex(e => e.Key).IsUnique(); - }); - - modelBuilder.Entity() - .HasIndex(c => new { c.Symbol, c.Interval, c.Timestamp }) - .IsUnique(); - - modelBuilder.Entity() - .HasIndex(c => c.Isin); - } -} - -public class TechnicalAnalysisDbContextFactory : IDesignTimeDbContextFactory -{ - public TechnicalAnalysisDbContext CreateDbContext(string[] args) - { - var optionsBuilder = new DbContextOptionsBuilder(); - optionsBuilder.UseNpgsql("Host=localhost;Database=ta;Username=postgres;Password=postgres"); - return new TechnicalAnalysisDbContext(optionsBuilder.Options); - } -} diff --git a/FinlyticTechnicalAnalysis/Dockerfile b/FinlyticTechnicalAnalysis/Dockerfile deleted file mode 100644 index 5e82bc0..0000000 --- a/FinlyticTechnicalAnalysis/Dockerfile +++ /dev/null @@ -1,22 +0,0 @@ -FROM mcr.microsoft.com/dotnet/runtime:10.0 AS base -USER $APP_UID -WORKDIR /app - -FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build -ARG BUILD_CONFIGURATION=Release -WORKDIR /src -COPY ["FinlyticTechnicalAnalysis/FinlyticTechnicalAnalysis.csproj", "FinlyticTechnicalAnalysis/"] -COPY ["FinlyticCore/FinlyticCore.csproj", "FinlyticCore/"] -RUN dotnet restore "FinlyticTechnicalAnalysis/FinlyticTechnicalAnalysis.csproj" -COPY . . -WORKDIR "/src/FinlyticTechnicalAnalysis" -RUN dotnet build "./FinlyticTechnicalAnalysis.csproj" -c $BUILD_CONFIGURATION -o /app/build - -FROM build AS publish -ARG BUILD_CONFIGURATION=Release -RUN dotnet publish "./FinlyticTechnicalAnalysis.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false - -FROM base AS final -WORKDIR /app -COPY --from=publish /app/publish . -ENTRYPOINT ["dotnet", "FinlyticTechnicalAnalysis.dll"] diff --git a/FinlyticTechnicalAnalysis/Entities/CachedAnalysisEntity.cs b/FinlyticTechnicalAnalysis/Entities/CachedAnalysisEntity.cs deleted file mode 100644 index 4745ff7..0000000 --- a/FinlyticTechnicalAnalysis/Entities/CachedAnalysisEntity.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System; -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; - -namespace FinlyticTechnicalAnalysis.Entities; - -[Table("CachedAnalyses")] -public class CachedAnalysisEntity -{ - [Key] - [MaxLength(20)] - public string Isin { get; set; } = string.Empty; - - [MaxLength(20)] - public string Ticker { get; set; } = string.Empty; - - [Column(TypeName = "jsonb")] - public string AnalysisJson { get; set; } = "{}"; - - public DateTime CalculatedAt { get; set; } = DateTime.UtcNow; -} diff --git a/FinlyticTechnicalAnalysis/Entities/MacroDataEntity.cs b/FinlyticTechnicalAnalysis/Entities/MacroDataEntity.cs deleted file mode 100644 index 3d8809e..0000000 --- a/FinlyticTechnicalAnalysis/Entities/MacroDataEntity.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System; -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; - -namespace FinlyticTechnicalAnalysis.Entities; - -[Table("MacroData")] -public class MacroDataEntity -{ - [Key] - [MaxLength(20)] - public string Symbol { get; set; } = string.Empty; // "^VIX", "^GSPC", "DX-Y.NY" - - [Column(TypeName = "decimal(18, 6)")] - public decimal Value { get; set; } - - [Column(TypeName = "decimal(18, 6)")] - public decimal PreviousClose { get; set; } - - [MaxLength(50)] - public string TrendState { get; set; } = "Neutral"; - - public DateTime LastUpdatedAt { get; set; } = DateTime.UtcNow; -} diff --git a/FinlyticTechnicalAnalysis/Entities/MarketCandleEntity.cs b/FinlyticTechnicalAnalysis/Entities/MarketCandleEntity.cs deleted file mode 100644 index c69168a..0000000 --- a/FinlyticTechnicalAnalysis/Entities/MarketCandleEntity.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System; -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; - -namespace FinlyticTechnicalAnalysis.Entities; - -[Table("MarketCandles")] -public class MarketCandleEntity -{ - [Key] - public long Id { get; set; } - - [Required] - [MaxLength(20)] - public string Symbol { get; set; } = string.Empty; // e.g. "US5398301094" or "AAPL" or "^VIX" - - [Required] - [MaxLength(10)] - public string Interval { get; set; } = "1d"; // "1h", "1d" - - [Required] - public DateTime Timestamp { get; set; } - - [Column(TypeName = "decimal(18, 6)")] - public decimal Open { get; set; } - - [Column(TypeName = "decimal(18, 6)")] - public decimal High { get; set; } - - [Column(TypeName = "decimal(18, 6)")] - public decimal Low { get; set; } - - [Column(TypeName = "decimal(18, 6)")] - public decimal Close { get; set; } - - public long Volume { get; set; } - - [Column(TypeName = "decimal(18, 6)")] - public decimal? Bid { get; set; } - - [Column(TypeName = "decimal(18, 6)")] - public decimal? Ask { get; set; } -} diff --git a/FinlyticTechnicalAnalysis/Entities/TaSettingsEntity.cs b/FinlyticTechnicalAnalysis/Entities/TaSettingsEntity.cs deleted file mode 100644 index 25713a2..0000000 --- a/FinlyticTechnicalAnalysis/Entities/TaSettingsEntity.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System; -using System.ComponentModel.DataAnnotations; - -namespace FinlyticTechnicalAnalysis.Entities; - -/// -/// Entity representing global indicator and strategy settings for FinlyticTechnicalAnalysis. -/// Persisted in PostgreSQL and updated dynamically via Admin Panel MQTT events. -/// -public class TaSettingsEntity -{ - [Key] - public Guid Id { get; set; } - - public int EmaShortPeriod { get; set; } = 20; - public int SmaMediumPeriod { get; set; } = 50; - public int SmaLongPeriod { get; set; } = 200; - public double RsiOverboughtLimit { get; set; } = 70.0; - public double RsiOversoldLimit { get; set; } = 30.0; - public double SupertrendMultiplier { get; set; } = 3.0; - public DateTime UpdatedAt { get; set; } = DateTime.UtcNow; -} diff --git a/FinlyticTechnicalAnalysis/FinlyticTechnicalAnalysis.csproj b/FinlyticTechnicalAnalysis/FinlyticTechnicalAnalysis.csproj deleted file mode 100644 index 4c7f8d9..0000000 --- a/FinlyticTechnicalAnalysis/FinlyticTechnicalAnalysis.csproj +++ /dev/null @@ -1,31 +0,0 @@ - - - - net10.0 - enable - enable - Linux - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - - - - - - diff --git a/FinlyticTechnicalAnalysis/Migrations/20260801073352_Init.Designer.cs b/FinlyticTechnicalAnalysis/Migrations/20260801073352_Init.Designer.cs deleted file mode 100644 index 7d129c4..0000000 --- a/FinlyticTechnicalAnalysis/Migrations/20260801073352_Init.Designer.cs +++ /dev/null @@ -1,162 +0,0 @@ -// -using System; -using FinlyticTechnicalAnalysis.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 FinlyticTechnicalAnalysis.Migrations -{ - [DbContext(typeof(TechnicalAnalysisDbContext))] - [Migration("20260801073352_Init")] - partial class Init - { - /// - 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("FinlyticTechnicalAnalysis.Entities.CachedAnalysisEntity", b => - { - b.Property("Isin") - .HasMaxLength(20) - .HasColumnType("character varying(20)"); - - b.Property("AnalysisJson") - .IsRequired() - .HasColumnType("jsonb"); - - b.Property("CalculatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Ticker") - .IsRequired() - .HasMaxLength(20) - .HasColumnType("character varying(20)"); - - b.HasKey("Isin"); - - b.HasIndex("Isin"); - - b.ToTable("CachedAnalyses"); - }); - - modelBuilder.Entity("FinlyticTechnicalAnalysis.Entities.MacroDataEntity", b => - { - b.Property("Symbol") - .HasMaxLength(20) - .HasColumnType("character varying(20)"); - - b.Property("LastUpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("PreviousClose") - .HasColumnType("decimal(18, 6)"); - - b.Property("TrendState") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("character varying(50)"); - - b.Property("Value") - .HasColumnType("decimal(18, 6)"); - - b.HasKey("Symbol"); - - b.ToTable("MacroData"); - }); - - modelBuilder.Entity("FinlyticTechnicalAnalysis.Entities.MarketCandleEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("Ask") - .HasColumnType("decimal(18, 6)"); - - b.Property("Bid") - .HasColumnType("decimal(18, 6)"); - - b.Property("Close") - .HasColumnType("decimal(18, 6)"); - - b.Property("High") - .HasColumnType("decimal(18, 6)"); - - b.Property("Interval") - .IsRequired() - .HasMaxLength(10) - .HasColumnType("character varying(10)"); - - b.Property("Low") - .HasColumnType("decimal(18, 6)"); - - b.Property("Open") - .HasColumnType("decimal(18, 6)"); - - b.Property("Symbol") - .IsRequired() - .HasMaxLength(20) - .HasColumnType("character varying(20)"); - - b.Property("Timestamp") - .HasColumnType("timestamp with time zone"); - - b.Property("Volume") - .HasColumnType("bigint"); - - b.HasKey("Id"); - - b.HasIndex("Symbol", "Interval", "Timestamp") - .IsUnique(); - - b.ToTable("MarketCandles"); - }); - - modelBuilder.Entity("FinlyticTechnicalAnalysis.Entities.TaSettingsEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("EmaShortPeriod") - .HasColumnType("integer"); - - b.Property("RsiOverboughtLimit") - .HasColumnType("double precision"); - - b.Property("RsiOversoldLimit") - .HasColumnType("double precision"); - - b.Property("SmaLongPeriod") - .HasColumnType("integer"); - - b.Property("SmaMediumPeriod") - .HasColumnType("integer"); - - b.Property("SupertrendMultiplier") - .HasColumnType("double precision"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.ToTable("Settings"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/FinlyticTechnicalAnalysis/Migrations/20260801073352_Init.cs b/FinlyticTechnicalAnalysis/Migrations/20260801073352_Init.cs deleted file mode 100644 index 228583d..0000000 --- a/FinlyticTechnicalAnalysis/Migrations/20260801073352_Init.cs +++ /dev/null @@ -1,112 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; - -#nullable disable - -namespace FinlyticTechnicalAnalysis.Migrations -{ - /// - public partial class Init : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "CachedAnalyses", - columns: table => new - { - Isin = table.Column(type: "character varying(20)", maxLength: 20, nullable: false), - Ticker = table.Column(type: "character varying(20)", maxLength: 20, nullable: false), - AnalysisJson = table.Column(type: "jsonb", nullable: false), - CalculatedAt = table.Column(type: "timestamp with time zone", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_CachedAnalyses", x => x.Isin); - }); - - migrationBuilder.CreateTable( - name: "MacroData", - columns: table => new - { - Symbol = table.Column(type: "character varying(20)", maxLength: 20, nullable: false), - Value = table.Column(type: "numeric(18,6)", nullable: false), - PreviousClose = table.Column(type: "numeric(18,6)", nullable: false), - TrendState = table.Column(type: "character varying(50)", maxLength: 50, nullable: false), - LastUpdatedAt = table.Column(type: "timestamp with time zone", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_MacroData", x => x.Symbol); - }); - - migrationBuilder.CreateTable( - name: "MarketCandles", - columns: table => new - { - Id = table.Column(type: "bigint", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - Symbol = table.Column(type: "character varying(20)", maxLength: 20, nullable: false), - Interval = table.Column(type: "character varying(10)", maxLength: 10, nullable: false), - Timestamp = table.Column(type: "timestamp with time zone", nullable: false), - Open = table.Column(type: "numeric(18,6)", nullable: false), - High = table.Column(type: "numeric(18,6)", nullable: false), - Low = table.Column(type: "numeric(18,6)", nullable: false), - Close = table.Column(type: "numeric(18,6)", nullable: false), - Volume = table.Column(type: "bigint", nullable: false), - Bid = table.Column(type: "numeric(18,6)", nullable: true), - Ask = table.Column(type: "numeric(18,6)", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_MarketCandles", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "Settings", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - EmaShortPeriod = table.Column(type: "integer", nullable: false), - SmaMediumPeriod = table.Column(type: "integer", nullable: false), - SmaLongPeriod = table.Column(type: "integer", nullable: false), - RsiOverboughtLimit = table.Column(type: "double precision", nullable: false), - RsiOversoldLimit = table.Column(type: "double precision", nullable: false), - SupertrendMultiplier = table.Column(type: "double precision", nullable: false), - UpdatedAt = table.Column(type: "timestamp with time zone", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_Settings", x => x.Id); - }); - - migrationBuilder.CreateIndex( - name: "IX_CachedAnalyses_Isin", - table: "CachedAnalyses", - column: "Isin"); - - migrationBuilder.CreateIndex( - name: "IX_MarketCandles_Symbol_Interval_Timestamp", - table: "MarketCandles", - columns: new[] { "Symbol", "Interval", "Timestamp" }, - unique: true); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "CachedAnalyses"); - - migrationBuilder.DropTable( - name: "MacroData"); - - migrationBuilder.DropTable( - name: "MarketCandles"); - - migrationBuilder.DropTable( - name: "Settings"); - } - } -} diff --git a/FinlyticTechnicalAnalysis/Migrations/20260813202624_CheckPendingTechnicalAnalysis.Designer.cs b/FinlyticTechnicalAnalysis/Migrations/20260813202624_CheckPendingTechnicalAnalysis.Designer.cs deleted file mode 100644 index 0c6fc46..0000000 --- a/FinlyticTechnicalAnalysis/Migrations/20260813202624_CheckPendingTechnicalAnalysis.Designer.cs +++ /dev/null @@ -1,162 +0,0 @@ -// -using System; -using FinlyticTechnicalAnalysis.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 FinlyticTechnicalAnalysis.Migrations -{ - [DbContext(typeof(TechnicalAnalysisDbContext))] - [Migration("20260813202624_CheckPendingTechnicalAnalysis")] - partial class CheckPendingTechnicalAnalysis - { - /// - 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("FinlyticTechnicalAnalysis.Entities.CachedAnalysisEntity", b => - { - b.Property("Isin") - .HasMaxLength(20) - .HasColumnType("character varying(20)"); - - b.Property("AnalysisJson") - .IsRequired() - .HasColumnType("jsonb"); - - b.Property("CalculatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Ticker") - .IsRequired() - .HasMaxLength(20) - .HasColumnType("character varying(20)"); - - b.HasKey("Isin"); - - b.HasIndex("Isin"); - - b.ToTable("CachedAnalyses"); - }); - - modelBuilder.Entity("FinlyticTechnicalAnalysis.Entities.MacroDataEntity", b => - { - b.Property("Symbol") - .HasMaxLength(20) - .HasColumnType("character varying(20)"); - - b.Property("LastUpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("PreviousClose") - .HasColumnType("decimal(18, 6)"); - - b.Property("TrendState") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("character varying(50)"); - - b.Property("Value") - .HasColumnType("decimal(18, 6)"); - - b.HasKey("Symbol"); - - b.ToTable("MacroData"); - }); - - modelBuilder.Entity("FinlyticTechnicalAnalysis.Entities.MarketCandleEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("Ask") - .HasColumnType("decimal(18, 6)"); - - b.Property("Bid") - .HasColumnType("decimal(18, 6)"); - - b.Property("Close") - .HasColumnType("decimal(18, 6)"); - - b.Property("High") - .HasColumnType("decimal(18, 6)"); - - b.Property("Interval") - .IsRequired() - .HasMaxLength(10) - .HasColumnType("character varying(10)"); - - b.Property("Low") - .HasColumnType("decimal(18, 6)"); - - b.Property("Open") - .HasColumnType("decimal(18, 6)"); - - b.Property("Symbol") - .IsRequired() - .HasMaxLength(20) - .HasColumnType("character varying(20)"); - - b.Property("Timestamp") - .HasColumnType("timestamp with time zone"); - - b.Property("Volume") - .HasColumnType("bigint"); - - b.HasKey("Id"); - - b.HasIndex("Symbol", "Interval", "Timestamp") - .IsUnique(); - - b.ToTable("MarketCandles"); - }); - - modelBuilder.Entity("FinlyticTechnicalAnalysis.Entities.TaSettingsEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("EmaShortPeriod") - .HasColumnType("integer"); - - b.Property("RsiOverboughtLimit") - .HasColumnType("double precision"); - - b.Property("RsiOversoldLimit") - .HasColumnType("double precision"); - - b.Property("SmaLongPeriod") - .HasColumnType("integer"); - - b.Property("SmaMediumPeriod") - .HasColumnType("integer"); - - b.Property("SupertrendMultiplier") - .HasColumnType("double precision"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.ToTable("Settings"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/FinlyticTechnicalAnalysis/Migrations/20260813202624_CheckPendingTechnicalAnalysis.cs b/FinlyticTechnicalAnalysis/Migrations/20260813202624_CheckPendingTechnicalAnalysis.cs deleted file mode 100644 index a540ee0..0000000 --- a/FinlyticTechnicalAnalysis/Migrations/20260813202624_CheckPendingTechnicalAnalysis.cs +++ /dev/null @@ -1,22 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace FinlyticTechnicalAnalysis.Migrations -{ - /// - public partial class CheckPendingTechnicalAnalysis : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - - } - } -} diff --git a/FinlyticTechnicalAnalysis/Migrations/20260815183955_AddDynamicSettings.Designer.cs b/FinlyticTechnicalAnalysis/Migrations/20260815183955_AddDynamicSettings.Designer.cs deleted file mode 100644 index 985c812..0000000 --- a/FinlyticTechnicalAnalysis/Migrations/20260815183955_AddDynamicSettings.Designer.cs +++ /dev/null @@ -1,193 +0,0 @@ -// -using System; -using FinlyticTechnicalAnalysis.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 FinlyticTechnicalAnalysis.Migrations -{ - [DbContext(typeof(TechnicalAnalysisDbContext))] - [Migration("20260815183955_AddDynamicSettings")] - partial class AddDynamicSettings - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "10.0.9") - .HasAnnotation("Relational:MaxIdentifierLength", 63); - - NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - - modelBuilder.Entity("FinlyticCore.Entities.Settings.SettingEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Key") - .IsRequired() - .HasMaxLength(150) - .HasColumnType("character varying(150)"); - - b.Property("LastUpdatedUtc") - .HasColumnType("timestamp with time zone"); - - b.Property("ServiceIdentifier") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.Property("ValueJson") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("Id"); - - b.HasIndex("Key") - .IsUnique(); - - b.ToTable("DynamicSettings"); - }); - - modelBuilder.Entity("FinlyticTechnicalAnalysis.Entities.CachedAnalysisEntity", b => - { - b.Property("Isin") - .HasMaxLength(20) - .HasColumnType("character varying(20)"); - - b.Property("AnalysisJson") - .IsRequired() - .HasColumnType("jsonb"); - - b.Property("CalculatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Ticker") - .IsRequired() - .HasMaxLength(20) - .HasColumnType("character varying(20)"); - - b.HasKey("Isin"); - - b.HasIndex("Isin"); - - b.ToTable("CachedAnalyses"); - }); - - modelBuilder.Entity("FinlyticTechnicalAnalysis.Entities.MacroDataEntity", b => - { - b.Property("Symbol") - .HasMaxLength(20) - .HasColumnType("character varying(20)"); - - b.Property("LastUpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("PreviousClose") - .HasColumnType("decimal(18, 6)"); - - b.Property("TrendState") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("character varying(50)"); - - b.Property("Value") - .HasColumnType("decimal(18, 6)"); - - b.HasKey("Symbol"); - - b.ToTable("MacroData"); - }); - - modelBuilder.Entity("FinlyticTechnicalAnalysis.Entities.MarketCandleEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("Ask") - .HasColumnType("decimal(18, 6)"); - - b.Property("Bid") - .HasColumnType("decimal(18, 6)"); - - b.Property("Close") - .HasColumnType("decimal(18, 6)"); - - b.Property("High") - .HasColumnType("decimal(18, 6)"); - - b.Property("Interval") - .IsRequired() - .HasMaxLength(10) - .HasColumnType("character varying(10)"); - - b.Property("Low") - .HasColumnType("decimal(18, 6)"); - - b.Property("Open") - .HasColumnType("decimal(18, 6)"); - - b.Property("Symbol") - .IsRequired() - .HasMaxLength(20) - .HasColumnType("character varying(20)"); - - b.Property("Timestamp") - .HasColumnType("timestamp with time zone"); - - b.Property("Volume") - .HasColumnType("bigint"); - - b.HasKey("Id"); - - b.HasIndex("Symbol", "Interval", "Timestamp") - .IsUnique(); - - b.ToTable("MarketCandles"); - }); - - modelBuilder.Entity("FinlyticTechnicalAnalysis.Entities.TaSettingsEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("EmaShortPeriod") - .HasColumnType("integer"); - - b.Property("RsiOverboughtLimit") - .HasColumnType("double precision"); - - b.Property("RsiOversoldLimit") - .HasColumnType("double precision"); - - b.Property("SmaLongPeriod") - .HasColumnType("integer"); - - b.Property("SmaMediumPeriod") - .HasColumnType("integer"); - - b.Property("SupertrendMultiplier") - .HasColumnType("double precision"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.ToTable("Settings"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/FinlyticTechnicalAnalysis/Migrations/20260815183955_AddDynamicSettings.cs b/FinlyticTechnicalAnalysis/Migrations/20260815183955_AddDynamicSettings.cs deleted file mode 100644 index 0bdaea2..0000000 --- a/FinlyticTechnicalAnalysis/Migrations/20260815183955_AddDynamicSettings.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace FinlyticTechnicalAnalysis.Migrations -{ - /// - public partial class AddDynamicSettings : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "DynamicSettings", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - Key = table.Column(type: "character varying(150)", maxLength: 150, nullable: false), - ValueJson = table.Column(type: "text", nullable: false), - ServiceIdentifier = table.Column(type: "character varying(100)", maxLength: 100, nullable: false), - LastUpdatedUtc = table.Column(type: "timestamp with time zone", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_DynamicSettings", x => x.Id); - }); - - migrationBuilder.CreateIndex( - name: "IX_DynamicSettings_Key", - table: "DynamicSettings", - column: "Key", - unique: true); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "DynamicSettings"); - } - } -} diff --git a/FinlyticTechnicalAnalysis/Migrations/TechnicalAnalysisDbContextModelSnapshot.cs b/FinlyticTechnicalAnalysis/Migrations/TechnicalAnalysisDbContextModelSnapshot.cs deleted file mode 100644 index 4d26447..0000000 --- a/FinlyticTechnicalAnalysis/Migrations/TechnicalAnalysisDbContextModelSnapshot.cs +++ /dev/null @@ -1,190 +0,0 @@ -// -using System; -using FinlyticTechnicalAnalysis.Database; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; - -#nullable disable - -namespace FinlyticTechnicalAnalysis.Migrations -{ - [DbContext(typeof(TechnicalAnalysisDbContext))] - partial class TechnicalAnalysisDbContextModelSnapshot : 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("FinlyticCore.Entities.Settings.SettingEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Key") - .IsRequired() - .HasMaxLength(150) - .HasColumnType("character varying(150)"); - - b.Property("LastUpdatedUtc") - .HasColumnType("timestamp with time zone"); - - b.Property("ServiceIdentifier") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.Property("ValueJson") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("Id"); - - b.HasIndex("Key") - .IsUnique(); - - b.ToTable("DynamicSettings"); - }); - - modelBuilder.Entity("FinlyticTechnicalAnalysis.Entities.CachedAnalysisEntity", b => - { - b.Property("Isin") - .HasMaxLength(20) - .HasColumnType("character varying(20)"); - - b.Property("AnalysisJson") - .IsRequired() - .HasColumnType("jsonb"); - - b.Property("CalculatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Ticker") - .IsRequired() - .HasMaxLength(20) - .HasColumnType("character varying(20)"); - - b.HasKey("Isin"); - - b.HasIndex("Isin"); - - b.ToTable("CachedAnalyses"); - }); - - modelBuilder.Entity("FinlyticTechnicalAnalysis.Entities.MacroDataEntity", b => - { - b.Property("Symbol") - .HasMaxLength(20) - .HasColumnType("character varying(20)"); - - b.Property("LastUpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("PreviousClose") - .HasColumnType("decimal(18, 6)"); - - b.Property("TrendState") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("character varying(50)"); - - b.Property("Value") - .HasColumnType("decimal(18, 6)"); - - b.HasKey("Symbol"); - - b.ToTable("MacroData"); - }); - - modelBuilder.Entity("FinlyticTechnicalAnalysis.Entities.MarketCandleEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("Ask") - .HasColumnType("decimal(18, 6)"); - - b.Property("Bid") - .HasColumnType("decimal(18, 6)"); - - b.Property("Close") - .HasColumnType("decimal(18, 6)"); - - b.Property("High") - .HasColumnType("decimal(18, 6)"); - - b.Property("Interval") - .IsRequired() - .HasMaxLength(10) - .HasColumnType("character varying(10)"); - - b.Property("Low") - .HasColumnType("decimal(18, 6)"); - - b.Property("Open") - .HasColumnType("decimal(18, 6)"); - - b.Property("Symbol") - .IsRequired() - .HasMaxLength(20) - .HasColumnType("character varying(20)"); - - b.Property("Timestamp") - .HasColumnType("timestamp with time zone"); - - b.Property("Volume") - .HasColumnType("bigint"); - - b.HasKey("Id"); - - b.HasIndex("Symbol", "Interval", "Timestamp") - .IsUnique(); - - b.ToTable("MarketCandles"); - }); - - modelBuilder.Entity("FinlyticTechnicalAnalysis.Entities.TaSettingsEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("EmaShortPeriod") - .HasColumnType("integer"); - - b.Property("RsiOverboughtLimit") - .HasColumnType("double precision"); - - b.Property("RsiOversoldLimit") - .HasColumnType("double precision"); - - b.Property("SmaLongPeriod") - .HasColumnType("integer"); - - b.Property("SmaMediumPeriod") - .HasColumnType("integer"); - - b.Property("SupertrendMultiplier") - .HasColumnType("double precision"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.ToTable("Settings"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/FinlyticTechnicalAnalysis/Program.cs b/FinlyticTechnicalAnalysis/Program.cs deleted file mode 100644 index 5ad6f1c..0000000 --- a/FinlyticTechnicalAnalysis/Program.cs +++ /dev/null @@ -1,67 +0,0 @@ -using System; -using FinlyticCore.Database; -using FinlyticCore.Services; -using FinlyticCore.Services.TradeRepublic; -using FinlyticCore.Services.Yahoo; -using FinlyticTechnicalAnalysis.Database; -using FinlyticTechnicalAnalysis.Services; -using FinlyticTechnicalAnalysis.Util; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; - -var builder = Host.CreateApplicationBuilder(args); - -// Register DB Context -builder.Services.AddDbContext(options => - options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection"))); -builder.Services.AddScoped(sp => sp.GetRequiredService()); - -// Register Core Services & Logger -builder.Services.AddSingleton(); -builder.Services.AddSingleton(typeof(IFinlyticLogger<>), typeof(FinlyticLogger<>)); - -// Register HTTP Clients -builder.Services.AddHttpClient() - .ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler - { - UseCookies = true, - CookieContainer = new System.Net.CookieContainer() - }); - -// Register Trade Republic WebSocket Client & Services -builder.Services.AddSingleton(); -builder.Services.AddSingleton(); - -// Register Technical Analysis Services -builder.Services.AddSingleton(); -builder.Services.AddTransient(); -builder.Services.AddScoped(); -builder.Services.AddTransient(); -builder.Services.AddSingleton(); - -// Register MQTT Client (as a Hosted Service) -builder.Services.AddHostedService(); - -var host = builder.Build(); - -// Run startup database migrations -using (var scope = host.Services.CreateScope()) -{ - try - { - var context = scope.ServiceProvider.GetRequiredService(); - await context.Database.MigrateAsync(); - Console.WriteLine("Database migrations successfully executed for FinlyticTechnicalAnalysis."); - - var settingsService = scope.ServiceProvider.GetRequiredService(); - await settingsService.GetSettingsAsync(); - } - catch (Exception ex) - { - Console.WriteLine($"Critical error during database migration: {ex.Message}"); - } -} - -await host.RunAsync(); diff --git a/FinlyticTechnicalAnalysis/Project.md b/FinlyticTechnicalAnalysis/Project.md deleted file mode 100644 index 78b5346..0000000 --- a/FinlyticTechnicalAnalysis/Project.md +++ /dev/null @@ -1,34 +0,0 @@ -# Finlytic Technical Analysis Service - -Finlytic Technical Analysis is a C# microservice providing real-time technical indicator calculations, candle pattern recognition, and trend regime evaluations for traded assets. - ---- - -## Core Features & Architecture - -1. **Indicator Calculations**: - - Calculates Exponential Moving Averages (`EMA 20`), Simple Moving Averages (`SMA 50`, `SMA 200`), Relative Strength Index (`RSI 14`), Moving Average Convergence Divergence (`MACD`), and `Supertrend`. - -2. **Chart Pattern Detection**: - - Detects technical chart patterns (`ChartPatternDto`) including Double Bottoms, Head & Shoulders, Bull Flags, and Trendline breakouts. - -3. **Macro Market Regime Mapping**: - - Evaluates overall technical signals (`BUY`, `STRONG BUY`, `NEUTRAL`, `SELL`, `STRONG SELL`). - -4. **MQTT RPC & Event Messaging**: - - Publishes technical analysis updates to `finlytic/technicalanalysis/{symbol}` and `finlytic/ta/{symbol}`. - - Answers RPC queries on `services/request/ta_GetAnalysis/#`. - ---- - -## Feature Status - -### Implemented Features -- [x] Technical Indicator Calculations (`IndicatorValuesDto`, `TechnicalAnalysisDto`). -- [x] Chart Pattern Detection Service (`IChartPatternDetector`). -- [x] Zero-Allocation MQTT serialization via `FinlyticJsonSerializerContext`. -- [x] Pure Worker Service architecture (no Kestrel HTTP webserver). - -### Planned Features -- [ ] Auto-tuned indicator parameters based on asset volatility regime (Adaptive EMA/RSI). -- [ ] Multi-timeframe indicator alignment matrix (5m, 1h, 1D, 1W sync). diff --git a/FinlyticTechnicalAnalysis/Services/SettingsDbService.cs b/FinlyticTechnicalAnalysis/Services/SettingsDbService.cs deleted file mode 100644 index c55cad4..0000000 --- a/FinlyticTechnicalAnalysis/Services/SettingsDbService.cs +++ /dev/null @@ -1,100 +0,0 @@ -using FinlyticTechnicalAnalysis.Database; -using FinlyticTechnicalAnalysis.Entities; -using Microsoft.EntityFrameworkCore; - -namespace FinlyticTechnicalAnalysis.Services; - -public interface ISettingsDbService -{ - /// - /// Gets the settings. - /// - Task GetSettingsAsync(); - /// - /// Saves the settings. - /// - Task SaveSettingsAsync(TaSettingsEntity settings); - /// - /// Updates settings from a dictionary. - /// - Task UpdateSettingsFromDictionaryAsync(Dictionary dictionary); -} - -public class SettingsDbService : ISettingsDbService -{ - private readonly TechnicalAnalysisDbContext _context; - - public SettingsDbService(TechnicalAnalysisDbContext context) - { - _context = context; - } - - /// - /// Gets the settings. - /// - public async Task GetSettingsAsync() - { - var settings = await _context.Settings.AsNoTracking().FirstOrDefaultAsync(); - if (settings == null) - { - settings = new TaSettingsEntity { Id = Guid.NewGuid() }; - _context.Settings.Add(settings); - await _context.SaveChangesAsync(); - _context.ChangeTracker.Clear(); - } - return settings; - } - - /// - /// Saves the settings. - /// - public async Task SaveSettingsAsync(TaSettingsEntity 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.EmaShortPeriod = settings.EmaShortPeriod; - existing.SmaMediumPeriod = settings.SmaMediumPeriod; - existing.SmaLongPeriod = settings.SmaLongPeriod; - existing.RsiOverboughtLimit = settings.RsiOverboughtLimit; - existing.RsiOversoldLimit = settings.RsiOversoldLimit; - existing.SupertrendMultiplier = settings.SupertrendMultiplier; - existing.UpdatedAt = settings.UpdatedAt; - _context.Settings.Update(existing); - } - await _context.SaveChangesAsync(); - return settings; - } - - /// - /// Updates settings from a dictionary. - /// - public async Task UpdateSettingsFromDictionaryAsync(Dictionary dictionary) - { - var settings = await GetSettingsAsync(); - - foreach (var (key, value) in dictionary) - { - if (string.Equals(key, "EmaShortPeriod", StringComparison.OrdinalIgnoreCase) && int.TryParse(value, out var esp)) - settings.EmaShortPeriod = esp; - else if (string.Equals(key, "SmaMediumPeriod", StringComparison.OrdinalIgnoreCase) && int.TryParse(value, out var smp)) - settings.SmaMediumPeriod = smp; - else if (string.Equals(key, "SmaLongPeriod", StringComparison.OrdinalIgnoreCase) && int.TryParse(value, out var slp)) - settings.SmaLongPeriod = slp; - else if (string.Equals(key, "RsiOverboughtLimit", StringComparison.OrdinalIgnoreCase) && double.TryParse(value, out var rsiOb)) - settings.RsiOverboughtLimit = rsiOb; - else if (string.Equals(key, "RsiOversoldLimit", StringComparison.OrdinalIgnoreCase) && double.TryParse(value, out var rsiOs)) - settings.RsiOversoldLimit = rsiOs; - else if (string.Equals(key, "SupertrendMultiplier", StringComparison.OrdinalIgnoreCase) && double.TryParse(value, out var stm)) - settings.SupertrendMultiplier = stm; - } - - settings.UpdatedAt = DateTime.UtcNow; - await SaveSettingsAsync(settings); - } -} diff --git a/FinlyticTechnicalAnalysis/Services/TechnicalAnalysisCalculator.cs b/FinlyticTechnicalAnalysis/Services/TechnicalAnalysisCalculator.cs deleted file mode 100644 index 9eab27d..0000000 --- a/FinlyticTechnicalAnalysis/Services/TechnicalAnalysisCalculator.cs +++ /dev/null @@ -1,671 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using FinlyticCore.Dtos.TechnicalAnalysis; -using FinlyticTechnicalAnalysis.Entities; -using Skender.Stock.Indicators; - -namespace FinlyticTechnicalAnalysis.Services; - -public interface ITechnicalAnalysisCalculator -{ - /// - /// Calculates the technical analysis using Skender.StockIndicators for math and custom algorithms for pattern detection. - /// - (List Indicators, List Patterns, List Signals) CalculateAnalysis(List candles, string currency = "EUR"); -} - -public class TechnicalAnalysisCalculator : ITechnicalAnalysisCalculator -{ - public (List Indicators, List Patterns, List Signals) CalculateAnalysis(List candles, string currency = "EUR") - { - var indicators = new List(); - var patterns = new List(); - var signals = new List(); - - if (candles == null || candles.Count == 0) - return (indicators, patterns, signals); - - var curSym = GetCurrencySymbol(currency); - var sortedCandles = candles.OrderBy(c => c.Timestamp).ToList(); - - // 1. Convert domain candles to Skender Quotes - var quotes = sortedCandles.Select(c => new Quote - { - Date = c.Timestamp, - Open = c.Open, - High = c.High, - Low = c.Low, - Close = c.Close, - Volume = c.Volume - }).ToList(); - - // 2. Compute Indicators via Skender.StockIndicators - var ema20List = quotes.GetEma(20).ToList(); - var sma50List = quotes.GetSma(50).ToList(); - var sma200List = quotes.GetSma(200).ToList(); - var rsi14List = quotes.GetRsi(14).ToList(); - var macdList = quotes.GetMacd(12, 26, 9).ToList(); - var atr14List = quotes.GetAtr(14).ToList(); - var vwapList = quotes.GetVwap().ToList(); - var supertrendList = quotes.GetSuperTrend(10, 3.0).ToList(); - - // Build IndicatorValuesDto list per candle - for (int i = 0; i < sortedCandles.Count; i++) - { - var candle = sortedCandles[i]; - var closeVal = candle.Close; - - var atr = atr14List[i].Atr.HasValue ? (decimal)atr14List[i].Atr!.Value : 0m; - var stopLoss = atr > 0m ? closeVal - (1.5m * atr) : (decimal?)null; - - // Map Supertrend direction string - string? superDir = null; - if (supertrendList[i].LowerBand.HasValue) superDir = "Bullish"; - else if (supertrendList[i].UpperBand.HasValue) superDir = "Bearish"; - - indicators.Add(new IndicatorValuesDto( - Timestamp: candle.Timestamp, - Ema20: ema20List[i].Ema.HasValue ? (decimal)ema20List[i].Ema!.Value : null, - Sma50: sma50List[i].Sma.HasValue ? (decimal)sma50List[i].Sma!.Value : null, - Sma200: sma200List[i].Sma.HasValue ? (decimal)sma200List[i].Sma!.Value : null, - Rsi14: rsi14List[i].Rsi.HasValue ? (decimal)rsi14List[i].Rsi!.Value : null, - MacdLine: macdList[i].Macd.HasValue ? (decimal)macdList[i].Macd!.Value : null, - MacdSignal: macdList[i].Signal.HasValue ? (decimal)macdList[i].Signal!.Value : null, - MacdHistogram: macdList[i].Histogram.HasValue ? (decimal)macdList[i].Histogram!.Value : null, - Atr14: atr > 0m ? atr : null, - Vwap: vwapList[i].Vwap.HasValue ? (decimal)vwapList[i].Vwap!.Value : null, - SupertrendUpper: supertrendList[i].UpperBand.HasValue ? (decimal)supertrendList[i].UpperBand!.Value : null, - SupertrendLower: supertrendList[i].LowerBand.HasValue ? (decimal)supertrendList[i].LowerBand!.Value : null, - SupertrendDirection: superDir, - RecommendedStopLoss: stopLoss - )); - } - - // 3. Detect Strategy Signals using computed indicator lists - var sma50Values = sma50List.Select(x => x.Sma).ToList(); - var sma200Values = sma200List.Select(x => x.Sma).ToList(); - var rsiValues = rsi14List.Select(x => x.Rsi).ToList(); - - DetectStrategySignals(sortedCandles, sma50Values, sma200Values, rsiValues, signals); - - // 4. Detect Geometric Chart Patterns - DetectChartPatterns(sortedCandles, patterns, curSym); - - return (indicators, patterns, signals); - } - - private static string GetCurrencySymbol(string currency) - { - if (string.IsNullOrWhiteSpace(currency)) return "€"; - return currency.ToUpperInvariant() switch - { - "USD" => "$", - "GBP" => "£", - "CHF" => "CHF ", - "JPY" => "¥", - _ => "€" - }; - } - - private static void DetectStrategySignals(List candles, List sma50, List sma200, List rsi14, List signals) - { - for (int i = 1; i < candles.Count; i++) - { - var candle = candles[i]; - - if (sma50[i - 1].HasValue && sma200[i - 1].HasValue && sma50[i].HasValue && sma200[i].HasValue) - { - if (sma50[i - 1]!.Value <= sma200[i - 1]!.Value && sma50[i]!.Value > sma200[i]!.Value) - { - signals.Add(new StrategySignalDto( - Type: "GoldenCross", - Timestamp: candle.Timestamp, - Direction: "BUY", - Price: candle.Close, - Description: "Golden Cross: SMA 50 hat den SMA 200 von unten nach oben gekreuzt (Bullisches Signal)." - )); - } - else if (sma50[i - 1]!.Value >= sma200[i - 1]!.Value && sma50[i]!.Value < sma200[i]!.Value) - { - signals.Add(new StrategySignalDto( - Type: "DeathCross", - Timestamp: candle.Timestamp, - Direction: "SELL", - Price: candle.Close, - Description: "Death Cross: SMA 50 hat den SMA 200 von oben nach unten gekreuzt (Bearisches Signal)." - )); - } - } - - if (rsi14[i].HasValue && rsi14[i - 1].HasValue) - { - if (rsi14[i - 1]!.Value < 30 && rsi14[i]!.Value >= 30) - { - signals.Add(new StrategySignalDto( - Type: "RsiOversoldRebound", - Timestamp: candle.Timestamp, - Direction: "BUY", - Price: candle.Close, - Description: "RSI (14) steigt aus überverkauftem Bereich (<30) wieder an." - )); - } - else if (rsi14[i - 1]!.Value > 70 && rsi14[i]!.Value <= 70) - { - signals.Add(new StrategySignalDto( - Type: "RsiOverboughtCorrection", - Timestamp: candle.Timestamp, - Direction: "SELL", - Price: candle.Close, - Description: "RSI (14) fällt aus überkauftem Bereich (>70) zurück." - )); - } - } - } - } - - private static void DetectChartPatterns(List sortedCandles, List patterns, string curSym) - { - if (sortedCandles.Count < 20) return; - - int[] windowSizes = { 20, 30, 45, 60, 90, 120 }; - var candidatePatterns = new List(); - - foreach (var window in windowSizes) - { - if (sortedCandles.Count < window) continue; - var slice = sortedCandles.TakeLast(window).ToList(); - - DetectDoubleBottomInSlice(slice, candidatePatterns, curSym); - DetectDoubleTopInSlice(slice, candidatePatterns, curSym); - DetectHeadAndShouldersInSlice(slice, candidatePatterns, curSym); - DetectTrianglesInSlice(slice, candidatePatterns, curSym); - } - - if (candidatePatterns.Count == 0) return; - - var currentClose = sortedCandles.Last().Close; - - bool activeSellBreakdown = candidatePatterns.Any(p => - p.BreakoutSignal?.Direction == "SELL" && - currentClose < p.BreakoutSignal.TriggerPrice); - - bool activeBuyBreakout = candidatePatterns.Any(p => - p.BreakoutSignal?.Direction == "BUY" && - currentClose > p.BreakoutSignal.TriggerPrice); - - var filteredPatterns = candidatePatterns.Where(p => - { - var isBuy = p.BreakoutSignal?.Direction == "BUY"; - var trigger = p.BreakoutSignal?.TriggerPrice ?? 0m; - - if (activeSellBreakdown && isBuy && currentClose < trigger) - return false; - - if (activeBuyBreakout && !isBuy && currentClose > trigger) - return false; - - return true; - }).ToList(); - - // Gruppierung nach Typ & Auswahl des Musters mit der höchsten Confidence - var distinctPatterns = filteredPatterns - .GroupBy(p => p.Type) - .Select(g => g.OrderByDescending(p => p.ConfidencePercent ?? 0m).First()) - .OrderByDescending(p => p.ConfidencePercent ?? 0m) - .ToList(); - - // Wenn ein starkes Reversal-Muster (z.B. DoubleTop mit 90%+ Confidence) existiert, - // entfeuern wir konkurrierende generische Dreiecks-Formationen im selben Zeitfenster. - if (distinctPatterns.Any(p => p.Type == "DoubleTop" && (p.ConfidencePercent ?? 0) > 90m)) - { - distinctPatterns.RemoveAll(p => p.Type == "SymmetricalTriangle"); - } - - patterns.Clear(); - patterns.AddRange(distinctPatterns); - } - - private static List FindPivotLows(List candles, int lookback = 3) - { - var result = new List(); - for (int i = lookback; i < candles.Count - lookback; i++) - { - var low = candles[i].Low; - bool isPivot = true; - for (int j = i - lookback; j <= i + lookback; j++) - { - if (j == i) continue; - if (candles[j].Low <= low) { isPivot = false; break; } - } - if (isPivot) result.Add(i); - } - return result; - } - - private static List FindPivotHighs(List candles, int lookback = 3) - { - var result = new List(); - for (int i = lookback; i < candles.Count - lookback; i++) - { - var high = candles[i].High; - bool isPivot = true; - for (int j = i - lookback; j <= i + lookback; j++) - { - if (j == i) continue; - if (candles[j].High >= high) { isPivot = false; break; } - } - if (isPivot) result.Add(i); - } - return result; - } - - private static void DetectDoubleBottomInSlice(List slice, List patterns, string curSym) - { - if (slice.Count < 15) return; - var currentClose = slice.Last().Close; - var maxRecentHigh = slice.Max(c => c.High); - - int lookback = slice.Count >= 45 ? 3 : 2; - var pivotLows = FindPivotLows(slice, lookback); - if (pivotLows.Count < 2) return; - - for (int a = 0; a < pivotLows.Count - 1; a++) - { - for (int b = a + 1; b < pivotLows.Count; b++) - { - int idx1 = pivotLows[a]; - int idx2 = pivotLows[b]; - if (idx2 - idx1 < 5) continue; - - decimal low1 = slice[idx1].Low; - decimal low2 = slice[idx2].Low; - - if (Math.Abs(low1 - low2) / Math.Max(low1, low2) > 0.05m) continue; - - decimal neckline = 0m; - for (int k = idx1; k <= idx2; k++) - if (slice[k].High > neckline) neckline = slice[k].High; - - decimal avgLow = (low1 + low2) / 2m; - if (neckline < avgLow * 1.02m) continue; - - var targetPrice = neckline + (neckline - avgLow); - - if (maxRecentHigh >= targetPrice) continue; - if (currentClose < avgLow * 0.97m) continue; - - bool breakoutConfirmed = maxRecentHigh >= neckline * 1.01m; - if (breakoutConfirmed && currentClose < neckline) continue; - if (!breakoutConfirmed && currentClose < neckline * 0.90m) continue; - - DateTime breakoutTime = slice.Last().Timestamp; - for (int k = idx2 + 1; k < slice.Count; k++) - { - if (slice[k].High >= neckline || slice[k].Close >= neckline) - { - breakoutTime = slice[k].Timestamp; - break; - } - } - - var diffRatio = Math.Abs(low1 - low2) / Math.Max(low1, low2); - var neckDistRatio = (neckline - avgLow) / avgLow; - var conf = Math.Round(Math.Max(70m, 98m - (diffRatio * 600m) + (neckDistRatio * 200m)), 1); - conf = Math.Min(conf, 99m); - - var pct = currentClose > 0m ? ((targetPrice - currentClose) / currentClose) * 100m : 0m; - - string status = breakoutConfirmed - ? $"Ausbruch über {neckline:F2} {curSym} erfolgt (Kurs bei {currentClose:F2} {curSym}). Signal aktiv." - : $"Warten auf Ausbruch über Nackenlinie {neckline:F2} {curSym} (Trigger)."; - - DateTime futureTime = slice.Last().Timestamp.AddDays(14); - - patterns.Add(new ChartPatternDto( - Type: "DoubleBottom", - Description: $"Doppel-Tief (W-Muster): Bullische Bodenformation. Zwei Tiefs bei ~{avgLow:F2} {curSym} getestet. {status}", - UpperLine: new List - { - new(slice[idx1].Timestamp, neckline), - new(futureTime, neckline) - }, - LowerLine: new List - { - new(slice[idx1].Timestamp, low1), - new(slice[idx2].Timestamp, low2) - }, - ApexTime: null, - BreakoutSignal: new BreakoutSignalDto( - Time: breakoutTime, - Direction: "BUY", - TriggerPrice: neckline, - TargetPrice: targetPrice, - PotentialPercent: pct), - ConfidencePercent: conf)); - return; - } - } - } - - private static void DetectDoubleTopInSlice(List slice, List patterns, string curSym) - { - if (slice.Count < 15) return; - var currentClose = slice.Last().Close; - var minRecentLow = slice.Min(c => c.Low); - - int lookback = slice.Count >= 45 ? 3 : 2; - var pivotHighs = FindPivotHighs(slice, lookback); - if (pivotHighs.Count < 2) return; - - for (int a = 0; a < pivotHighs.Count - 1; a++) - { - for (int b = a + 1; b < pivotHighs.Count; b++) - { - int idx1 = pivotHighs[a]; - int idx2 = pivotHighs[b]; - if (idx2 - idx1 < 5) continue; - - decimal high1 = slice[idx1].High; - decimal high2 = slice[idx2].High; - - if (Math.Abs(high1 - high2) / Math.Max(high1, high2) > 0.05m) continue; - - decimal neckline = decimal.MaxValue; - for (int k = idx1; k <= idx2; k++) - if (slice[k].Low < neckline) neckline = slice[k].Low; - - decimal avgHigh = (high1 + high2) / 2m; - if (neckline > avgHigh * 0.98m) continue; - - var targetPrice = neckline - (avgHigh - neckline); - - if (minRecentLow <= targetPrice) continue; - if (currentClose > avgHigh * 1.03m) continue; - - bool breakdownConfirmed = minRecentLow <= neckline * 0.99m; - if (breakdownConfirmed && currentClose > neckline) continue; - if (!breakdownConfirmed && currentClose > neckline * 1.10m) continue; - - DateTime breakdownTime = slice.Last().Timestamp; - for (int k = idx2 + 1; k < slice.Count; k++) - { - if (slice[k].Low <= neckline || slice[k].Close <= neckline) - { - breakdownTime = slice[k].Timestamp; - break; - } - } - - var diffRatio = Math.Abs(high1 - high2) / Math.Max(high1, high2); - var neckDistRatio = (avgHigh - neckline) / avgHigh; - var conf = Math.Round(Math.Max(70m, 97m - (diffRatio * 600m) + (neckDistRatio * 200m)), 1); - conf = Math.Min(conf, 99m); - - var pct = currentClose > 0m ? ((currentClose - targetPrice) / currentClose) * 100m : 0m; - - string status = breakdownConfirmed - ? $"Breakdown unter {neckline:F2} {curSym} erfolgt (Kurs bei {currentClose:F2} {curSym}). Signal aktiv." - : $"Warten auf Breakdown unter Nackenlinie {neckline:F2} {curSym} (Trigger)."; - - DateTime futureTime = slice.Last().Timestamp.AddDays(14); - - patterns.Add(new ChartPatternDto( - Type: "DoubleTop", - Description: $"Doppel-Top (M-Muster): Bearische Umkehrformation. Widerstand bei ~{avgHigh:F2} {curSym} zweimal abgeprallt. {status}", - UpperLine: new List - { - new(slice[idx1].Timestamp, high1), - new(slice[idx2].Timestamp, high2) - }, - LowerLine: new List - { - new(slice[idx1].Timestamp, neckline), - new(futureTime, neckline) - }, - ApexTime: null, - BreakoutSignal: new BreakoutSignalDto( - Time: breakdownTime, - Direction: "SELL", - TriggerPrice: neckline, - TargetPrice: targetPrice, - PotentialPercent: pct), - ConfidencePercent: conf)); - return; - } - } - } - - private static void DetectHeadAndShouldersInSlice(List slice, List patterns, string curSym) - { - if (slice.Count < 20) return; - var currentClose = slice.Last().Close; - var minRecentLow = slice.Min(c => c.Low); - - int lookback = slice.Count >= 60 ? 4 : 3; - var pivotHighs = FindPivotHighs(slice, lookback); - if (pivotHighs.Count < 3) return; - - for (int a = 0; a < pivotHighs.Count - 2; a++) - { - int lsIdx = pivotHighs[a]; - int headIdx = pivotHighs[a + 1]; - int rsIdx = pivotHighs[a + 2]; - - decimal ls = slice[lsIdx].High; - decimal head = slice[headIdx].High; - decimal rs = slice[rsIdx].High; - - if (head <= ls * 1.01m || head <= rs * 1.01m) continue; - if (Math.Abs(ls - rs) / Math.Max(ls, rs) > 0.06m) continue; - - decimal neckline = decimal.MaxValue; - for (int k = lsIdx; k <= rsIdx; k++) - if (slice[k].Low < neckline) neckline = slice[k].Low; - - var targetPrice = neckline - (head - neckline); - - if (minRecentLow <= targetPrice) continue; - if (currentClose > head * 1.03m) continue; - - bool breakdownConfirmed = minRecentLow <= neckline * 0.99m; - if (breakdownConfirmed && currentClose > neckline) continue; - if (!breakdownConfirmed && currentClose > neckline * 1.10m) continue; - - DateTime breakdownTime = slice.Last().Timestamp; - for (int k = rsIdx + 1; k < slice.Count; k++) - { - if (slice[k].Low <= neckline || slice[k].Close <= neckline) - { - breakdownTime = slice[k].Timestamp; - break; - } - } - - var diffRatio = Math.Abs(ls - rs) / Math.Max(ls, rs); - var conf = Math.Round(Math.Max(72m, 96m - (diffRatio * 500m)), 1); - conf = Math.Min(conf, 99m); - - var pct = currentClose > 0m ? ((currentClose - targetPrice) / currentClose) * 100m : 0m; - - string status = breakdownConfirmed - ? $"Breakdown unter {neckline:F2} {curSym} erfolgt (Kurs bei {currentClose:F2} {curSym}). Signal aktiv." - : $"Warten auf Breakdown unter Nackenlinie {neckline:F2} {curSym} (Trigger)."; - - DateTime futureTime = slice.Last().Timestamp.AddDays(14); - - patterns.Add(new ChartPatternDto( - Type: "HeadAndShoulders", - Description: $"Kopf-Schulter-Formation: Bearische Trendumkehr. Kopf bei {head:F2} {curSym}, Nackenlinie bei {neckline:F2} {curSym} (Trigger). {status}", - UpperLine: new List - { - new(slice[lsIdx].Timestamp, ls), - new(slice[headIdx].Timestamp, head), - new(slice[rsIdx].Timestamp, rs) - }, - LowerLine: new List - { - new(slice[lsIdx].Timestamp, neckline), - new(futureTime, neckline) - }, - ApexTime: null, - BreakoutSignal: new BreakoutSignalDto( - Time: breakdownTime, - Direction: "SELL", - TriggerPrice: neckline, - TargetPrice: targetPrice, - PotentialPercent: pct), - ConfidencePercent: conf)); - return; - } - } - - private static void DetectTrianglesInSlice(List slice, List patterns, string curSym) - { - if (slice.Count < 15) return; - - int lookback = 2; - var pHighs = FindPivotHighs(slice, lookback); - var pLows = FindPivotLows(slice, lookback); - - if (pHighs.Count < 2 || pLows.Count < 2) return; - - // Nutze die letzten beiden Pivot-Highs und Pivot-Lows für exakte Geradengleichungen - int hIdx1 = pHighs[^2]; - int hIdx2 = pHighs[^1]; - int lIdx1 = pLows[^2]; - int lIdx2 = pLows[^1]; - - // Verhindere zu nahe beieinander liegende Pivots - if (hIdx2 - hIdx1 < 3 || lIdx2 - lIdx1 < 3) return; - - DateTime tH1 = slice[hIdx1].Timestamp; - DateTime tH2 = slice[hIdx2].Timestamp; - DateTime tL1 = slice[lIdx1].Timestamp; - DateTime tL2 = slice[lIdx2].Timestamp; - - decimal yH1 = slice[hIdx1].High; - decimal yH2 = slice[hIdx2].High; - decimal yL1 = slice[lIdx1].Low; - decimal yL2 = slice[lIdx2].Low; - - double daysH = (tH2 - tH1).TotalDays; - double daysL = (tL2 - tL1).TotalDays; - - if (daysH <= 0 || daysL <= 0) return; - - // Steigungen in €/Tag - double mUpper = (double)(yH2 - yH1) / daysH; - double mLower = (double)(yL2 - yL1) / daysL; - - var lastCandle = slice.Last(); - var lastClose = lastCandle.Close; - - // --- 1. Steigendes Dreieck (Ascending Triangle) --- - // Obere Linie ist nahezu flach (Widerstand), Untere Linie steigt - if (Math.Abs(mUpper) < 0.05 && mLower > 0.01) - { - if (!patterns.Any(p => p.Type == "AscendingTriangle")) - { - decimal resistance = (yH1 + yH2) / 2m; - decimal baseHeight = resistance - yL1; - decimal targetPrice = resistance + baseHeight; - - // Schnittpunkt (Apex) berechnen: y = mLower * x + yL1 - double daysToApex = (double)(resistance - yL1) / mLower; - DateTime apexTime = tL1.AddDays(daysToApex); - - if (apexTime > lastCandle.Timestamp) - { - var pct = lastClose > 0m ? ((targetPrice - lastClose) / lastClose) * 100m : 0m; - var conf = Math.Round(Math.Max(70m, 93m - (Math.Abs(yH1 - yH2) / yH1) * 600m), 1); - - patterns.Add(new ChartPatternDto( - Type: "AscendingTriangle", - Description: $"Steigendes Dreieck: Flacher Widerstand bei {resistance:F2} {curSym} (Trigger) mit steigenden Tiefs — bullisches Konsolidierungsmuster.", - UpperLine: new List { new(tH1, resistance), new(apexTime, resistance) }, - LowerLine: new List { new(tL1, yL1), new(tL2, yL2), new(apexTime, resistance) }, - ApexTime: apexTime, - BreakoutSignal: new BreakoutSignalDto(Time: lastCandle.Timestamp, Direction: "BUY", TriggerPrice: resistance, TargetPrice: targetPrice, PotentialPercent: pct), - ConfidencePercent: conf)); - } - } - } - - // --- 2. Fallendes Dreieck (Descending Triangle) --- - // Untere Linie ist nahezu flach (Unterstützung), Obere Linie fällt - if (Math.Abs(mLower) < 0.05 && mUpper < -0.01) - { - if (!patterns.Any(p => p.Type == "DescendingTriangle")) - { - decimal support = (yL1 + yL2) / 2m; - decimal baseHeight = yH1 - support; - decimal targetPrice = Math.Max(0.01m, support - baseHeight); - - // Schnittpunkt (Apex) berechnen: y = mUpper * x + yH1 - double daysToApex = (double)(support - yH1) / mUpper; - DateTime apexTime = tH1.AddDays(daysToApex); - - if (apexTime > lastCandle.Timestamp) - { - var pct = lastClose > 0m ? ((lastClose - targetPrice) / lastClose) * 100m : 0m; - var conf = Math.Round(Math.Max(70m, 93m - (Math.Abs(yL1 - yL2) / yL1) * 600m), 1); - - patterns.Add(new ChartPatternDto( - Type: "DescendingTriangle", - Description: $"Fallendes Dreieck: Flache Unterstützung bei {support:F2} {curSym} (Trigger) mit fallenden Hochs — bearisches Konsolidierungsmuster.", - UpperLine: new List { new(tH1, yH1), new(tH2, yH2), new(apexTime, support) }, - LowerLine: new List { new(tL1, support), new(apexTime, support) }, - ApexTime: apexTime, - BreakoutSignal: new BreakoutSignalDto(Time: lastCandle.Timestamp, Direction: "SELL", TriggerPrice: support, TargetPrice: targetPrice, PotentialPercent: pct), - ConfidencePercent: conf)); - } - } - } - - // --- 3. Symmetrisches Dreieck (Symmetrical Triangle) --- - // Obere Linie fällt (mUpper < 0) UND Untere Linie steigt (mLower > 0) -> Konvergieren! - if (mUpper < -0.005 && mLower > 0.01) - { - if (!patterns.Any(p => p.Type == "SymmetricalTriangle")) - { - // Präzise Berechnung des Schnittpunkts zweier Geraden in der Ebene (t, y) - // y = mUpper * (t - tH1) + yH1 - // y = mLower * (t - tL1) + yL1 - double deltaDaysT1 = (tH1 - tL1).TotalDays; - double denominator = mUpper - mLower; - - if (Math.Abs(denominator) > 0.0001) - { - double daysFromT1ToApex = ((double)(yL1 - yH1) + (mLower * deltaDaysT1)) / denominator; - DateTime apexTime = tH1.AddDays(daysFromT1ToApex); - - // Apex muss in der Zukunft liegen! - if (apexTime > lastCandle.Timestamp) - { - decimal apexPrice = yH1 + (decimal)(mUpper * daysFromT1ToApex); - decimal baseHeight = Math.Abs(yH1 - yL1); - - var direction = lastClose >= (yH1 + yL1) / 2m ? "BUY" : "SELL"; - var targetPrice = direction == "BUY" - ? lastClose + baseHeight - : Math.Max(0.01m, lastClose - baseHeight); - - var pct = lastClose > 0m - ? (direction == "BUY" ? ((targetPrice - lastClose) / lastClose) : ((lastClose - targetPrice) / lastClose)) * 100m - : 0m; - - patterns.Add(new ChartPatternDto( - Type: "SymmetricalTriangle", - Description: $"Symmetrisches Dreieck: Konvergierende Hochs und Tiefs — dynamischer Ausbruch in Trendrichtung erwartet.", - UpperLine: new List { new(tH1, yH1), new(tH2, yH2), new(apexTime, apexPrice) }, - LowerLine: new List { new(tL1, yL1), new(tL2, yL2), new(apexTime, apexPrice) }, - ApexTime: apexTime, - BreakoutSignal: new BreakoutSignalDto(Time: lastCandle.Timestamp, Direction: direction, TriggerPrice: lastClose, TargetPrice: targetPrice, PotentialPercent: pct), - ConfidencePercent: 85m)); - } - } - } - } - } -} \ No newline at end of file diff --git a/FinlyticTechnicalAnalysis/Services/TechnicalAnalysisDbService.cs b/FinlyticTechnicalAnalysis/Services/TechnicalAnalysisDbService.cs deleted file mode 100644 index a65b11e..0000000 --- a/FinlyticTechnicalAnalysis/Services/TechnicalAnalysisDbService.cs +++ /dev/null @@ -1,382 +0,0 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; -using System.Text.Json; -using System.Threading; -using System.Threading.Tasks; -using FinlyticCore.Dtos.TechnicalAnalysis; -using FinlyticCore.Services; -using FinlyticCore.Services.TradeRepublic; -using FinlyticTechnicalAnalysis.Database; -using FinlyticTechnicalAnalysis.Entities; -using FinlyticTechnicalAnalysis.Util; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.DependencyInjection; - -namespace FinlyticTechnicalAnalysis.Services; - -public interface ITechnicalAnalysisDbService -{ - Task GetAnalysisAsync(string isin, bool forceRefresh = false, string? ticker = null, - CancellationToken cancellationToken = default); - - Task GetLivePriceAsync(string isin, CancellationToken cancellationToken = default); -} - -public class TechnicalAnalysisDbService : ITechnicalAnalysisDbService -{ - private readonly IServiceScopeFactory _scopeFactory; - private readonly IYahooMarketDataScraper _yahooScraper; - private readonly ITradeRepublicService _trService; - private readonly ITechnicalAnalysisCalculator _calculator; - private readonly IFinlyticLogger _finlyticLogger; - - private static readonly ConcurrentDictionary Candles, string Symbol, string Currency, DateTime FetchedAt)> _candleCache = new(); - private static readonly ConcurrentDictionary _perIsinLocks = new(); - private static readonly TimeSpan CandleCacheTtl = TimeSpan.FromMinutes(15); - private static readonly TimeSpan DbCacheTtl = TimeSpan.FromHours(1); - - public TechnicalAnalysisDbService( - IServiceScopeFactory scopeFactory, - IYahooMarketDataScraper yahooScraper, - ITradeRepublicService trService, - ITechnicalAnalysisCalculator calculator, - IFinlyticLogger finlyticLogger) - { - _scopeFactory = scopeFactory; - _yahooScraper = yahooScraper; - _trService = trService; - _calculator = calculator; - _finlyticLogger = finlyticLogger; - } - - public async Task GetAnalysisAsync(string isin, bool forceRefresh = false, string? ticker = null, - CancellationToken cancellationToken = default) - { - if (string.IsNullOrWhiteSpace(isin)) return null; - var cleanIsin = isin.Trim().ToUpperInvariant(); - - if (!forceRefresh && _candleCache.TryGetValue(cleanIsin, out var ramEntry) && - DateTime.UtcNow - ramEntry.FetchedAt < CandleCacheTtl && - (string.IsNullOrWhiteSpace(ticker) || string.Equals(ramEntry.Symbol, ticker, StringComparison.OrdinalIgnoreCase))) - { - await _finlyticLogger.LogDebugAsync(SettingKeys.TechnicalAnalysisChannel, "[TechnicalAnalysisDbService] RAM-Cache Hit for ISIN {Isin}. Merging live price...", cleanIsin); - return await BuildAnalysisWithLivePriceAsync(cleanIsin, ramEntry.Candles, ramEntry.Symbol, ramEntry.Currency, cancellationToken); - } - - var semaphore = _perIsinLocks.GetOrAdd(cleanIsin, _ => new SemaphoreSlim(1, 1)); - await semaphore.WaitAsync(cancellationToken); - - try - { - if (!forceRefresh && _candleCache.TryGetValue(cleanIsin, out ramEntry) && - DateTime.UtcNow - ramEntry.FetchedAt < CandleCacheTtl && - (string.IsNullOrWhiteSpace(ticker) || string.Equals(ramEntry.Symbol, ticker, StringComparison.OrdinalIgnoreCase))) - { - return await BuildAnalysisWithLivePriceAsync(cleanIsin, ramEntry.Candles, ramEntry.Symbol, ramEntry.Currency, cancellationToken); - } - - if (!forceRefresh) - { - var dbDto = await GetFromDbCacheAsync(cleanIsin, ticker, cancellationToken); - if (dbDto != null) - { - await _finlyticLogger.LogDebugAsync(SettingKeys.TechnicalAnalysisChannel, "[TechnicalAnalysisDbService] DB-Cache Hit for ISIN {Isin}.", cleanIsin); - return dbDto; - } - } - - return await FullRefreshAsync(cleanIsin, ticker, cancellationToken); - } - finally - { - semaphore.Release(); - if (semaphore.CurrentCount == 1) - { - _perIsinLocks.TryRemove(cleanIsin, out _); - } - } - } - - public async Task GetLivePriceAsync(string isin, CancellationToken cancellationToken = default) - { - if (string.IsNullOrWhiteSpace(isin)) return null; - var cleanIsin = isin.Trim().ToUpperInvariant(); - - var (livePrice, liveBid, liveAsk, preChange) = await FetchLivePriceAsync(cleanIsin, cancellationToken); - if (!livePrice.HasValue) return null; - - return new LivePriceDto( - cleanIsin, - Math.Round(livePrice.Value, 2), - preChange ?? 0m, - liveBid.HasValue ? Math.Round(liveBid.Value, 2) : null, - liveAsk.HasValue ? Math.Round(liveAsk.Value, 2) : null - ); - } - - private async Task FullRefreshAsync(string cleanIsin, string? requestedTicker, CancellationToken cancellationToken) - { - await _finlyticLogger.LogInfoAsync(SettingKeys.TechnicalAnalysisChannel, "[TechnicalAnalysisDbService] Full refresh for ISIN {Isin} (RequestedTicker: {Ticker})", cleanIsin, requestedTicker ?? "None"); - - var macroTask = FetchMacroDataAsync(cancellationToken); - - string? ticker = requestedTicker; - if (string.IsNullOrWhiteSpace(ticker) || string.Equals(ticker.Trim(), cleanIsin, StringComparison.OrdinalIgnoreCase)) - { - ticker = await _yahooScraper.ResolveTickerFromIsinAsync(cleanIsin, cancellationToken); - } - - var querySymbol = !string.IsNullOrEmpty(ticker) ? ticker : cleanIsin; - var (vix, gspc, dxy) = await macroTask; - - var yahooResult = await _yahooScraper.FetchHistoricalCandlesWithCurrencyAsync(querySymbol, "2y", "1d", cancellationToken); - var candles = yahooResult.Candles; - var currency = yahooResult.Currency; - - if (candles.Count == 0 && querySymbol != cleanIsin) - { - yahooResult = await _yahooScraper.FetchHistoricalCandlesWithCurrencyAsync(cleanIsin, "2y", "1d", cancellationToken); - candles = yahooResult.Candles; - currency = yahooResult.Currency; - } - - if (candles.Count == 0) - { - await _finlyticLogger.LogWarningAsync(SettingKeys.TechnicalAnalysisChannel, "[TechnicalAnalysisDbService] No candles retrieved for {Symbol}", querySymbol); - return null; - } - - _candleCache[cleanIsin] = (candles.Select(CloneCandle).ToList(), querySymbol, currency, DateTime.UtcNow); - - await MergeLivePriceAsync(cleanIsin, candles, querySymbol, currency, cancellationToken); - - var resultDto = BuildDto(cleanIsin, querySymbol, currency, candles, vix, gspc, dxy); - - await PersistToDbCacheAsync(cleanIsin, querySymbol, resultDto, cancellationToken); - - return resultDto; - } - - private async Task BuildAnalysisWithLivePriceAsync( - string cleanIsin, List cachedCandles, string querySymbol, string currency, - CancellationToken cancellationToken) - { - var candles = cachedCandles.Select(CloneCandle).ToList(); - - var livePriceTask = FetchLivePriceAsync(cleanIsin, cancellationToken); - var macroTask = FetchMacroDataAsync(cancellationToken); - - await Task.WhenAll(livePriceTask, macroTask); - - var (livePrice, liveBid, liveAsk, preChange) = await livePriceTask; - var (vix, gspc, dxy) = await macroTask; - - ApplyLivePriceToCandles(cleanIsin, candles, querySymbol, currency, livePrice, liveBid, liveAsk); - - return BuildDto(cleanIsin, querySymbol, currency, candles, vix, gspc, dxy); - } - - private async Task MergeLivePriceAsync(string cleanIsin, List candles, string querySymbol, string currency, - CancellationToken cancellationToken) - { - var (livePrice, liveBid, liveAsk, _) = await FetchLivePriceAsync(cleanIsin, cancellationToken); - ApplyLivePriceToCandles(cleanIsin, candles, querySymbol, currency, livePrice, liveBid, liveAsk); - } - - private void ApplyLivePriceToCandles( - string cleanIsin, List candles, string querySymbol, string candleCurrency, - decimal? livePrice, decimal? liveBid, decimal? liveAsk) - { - if (!livePrice.HasValue || livePrice.Value <= 0m) return; - - if (candleCurrency.Equals("USD", StringComparison.OrdinalIgnoreCase) && !cleanIsin.StartsWith("DE") && !cleanIsin.StartsWith("AT")) - { - _ = _finlyticLogger.LogDebugAsync(SettingKeys.TechnicalAnalysisChannel, "[TechnicalAnalysisDbService] Skipping direct EUR live price injection for USD asset {Isin}", cleanIsin); - return; - } - - var today = DateTime.UtcNow.Date; - var lastCandle = candles.LastOrDefault(c => c.Timestamp.Date == today) ?? candles.LastOrDefault(); - - if (lastCandle != null) - { - lastCandle.Close = livePrice.Value; - lastCandle.High = Math.Max(lastCandle.High, livePrice.Value); - lastCandle.Low = Math.Min(lastCandle.Low, livePrice.Value); - if (liveBid.HasValue) lastCandle.Bid = liveBid.Value; - if (liveAsk.HasValue) lastCandle.Ask = liveAsk.Value; - } - } - - private async Task<(decimal? livePrice, decimal? liveBid, decimal? liveAsk, decimal? preChange)> FetchLivePriceAsync( - string cleanIsin, CancellationToken cancellationToken) - { - decimal? livePrice = null; - decimal? liveBid = null; - decimal? liveAsk = null; - decimal? preChange = null; - - try - { - using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); - cts.CancelAfter(1500); - - var trTask = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - - int? subId = await _trService.SubscribeRealtimeTickerAsync(cleanIsin, tick => - { - decimal? effectivePrice = tick.Bid?.PriceValue > 0m - ? tick.Bid.PriceValue - : (tick.Last?.PriceValue > 0m ? tick.Last.PriceValue : null); - - if (effectivePrice.HasValue) - { - livePrice = tick.Last?.PriceValue ?? effectivePrice.Value; - liveBid = tick.Bid?.PriceValue; - liveAsk = tick.Ask?.PriceValue; - - decimal prePrice = tick.Pre?.PriceValue ?? 0m; - - if (prePrice > 0m) - { - preChange = Math.Round(((effectivePrice.Value - prePrice) / prePrice) * 100m, 2); - } - - trTask.TrySetResult(true); - } - }, cts.Token); - - if (subId.HasValue) - { - try - { - await trTask.Task.WaitAsync(cts.Token); - } - catch (OperationCanceledException) { } - - await _trService.UnsubscribeRealtimeTickerAsync(subId.Value); - } - } - catch (Exception ex) - { - await _finlyticLogger.LogWarningAsync(SettingKeys.TechnicalAnalysisChannel, ex, "[TechnicalAnalysisDbService] Real-time price fetch skipped for ISIN {Isin}", cleanIsin); - } - - return (livePrice, liveBid, liveAsk, preChange); - } - - private async Task<(MacroDataEntity vix, MacroDataEntity gspc, MacroDataEntity dxy)> FetchMacroDataAsync( - CancellationToken cancellationToken) - { - var vixTask = _yahooScraper.FetchMacroTickerAsync("^VIX", cancellationToken); - var gspcTask = _yahooScraper.FetchMacroTickerAsync("^GSPC", cancellationToken); - var dxyTask = _yahooScraper.FetchMacroTickerAsync("DX-Y.NY", cancellationToken); - - await Task.WhenAll(vixTask, gspcTask, dxyTask); - - var vix = await vixTask ?? new MacroDataEntity { Symbol = "^VIX", Value = 18.5m, TrendState = "Moderate" }; - var gspc = await gspcTask ?? new MacroDataEntity { Symbol = "^GSPC", Value = 5500m, TrendState = "Bullish" }; - var dxy = await dxyTask ?? new MacroDataEntity { Symbol = "DX-Y.NY", Value = 104.2m, TrendState = "Neutral" }; - - return (vix, gspc, dxy); - } - - private TechnicalAnalysisDto BuildDto(string cleanIsin, string querySymbol, string currency, - List candles, MacroDataEntity vix, MacroDataEntity gspc, MacroDataEntity dxy) - { - var vixRegime = vix.Value > 25m ? "HighVolatility" : (vix.Value > 18m ? "Moderate" : "LowVolatility"); - var summaryText = $"Markt-Vola (VIX: {vix.Value:F1}) ist {vixRegime}. S&P 500 Trend ist {gspc.TrendState}. DXY: {dxy.Value:F1}."; - - var marketRegime = new MarketRegimeDto( - VixValue: vix.Value, VixRegime: vixRegime, - MarketTrend: gspc.TrendState, DxyValue: dxy.Value, - DxyState: dxy.TrendState == "Bullish" ? "DollarStrengthening" : "DollarWeakening", - SummaryText: summaryText); - - var (indicators, patterns, signals) = _calculator.CalculateAnalysis(candles, currency); - - var candleDtos = candles.Select(c => new CandleDto( - Timestamp: c.Timestamp, Open: c.Open, High: c.High, - Low: c.Low, Close: c.Close, Volume: c.Volume, - Bid: c.Bid, Ask: c.Ask)).ToList(); - - return new TechnicalAnalysisDto( - Isin: cleanIsin, Ticker: querySymbol, CompanyName: querySymbol, - LastUpdated: DateTime.UtcNow, Candles: candleDtos, - Indicators: indicators, Patterns: patterns, Signals: signals, - MarketRegime: marketRegime, Currency: currency); - } - - private async Task GetFromDbCacheAsync(string cleanIsin, string? requestedTicker, CancellationToken cancellationToken) - { - try - { - using var scope = _scopeFactory.CreateScope(); - var db = scope.ServiceProvider.GetRequiredService(); - var cached = await db.CachedAnalyses - .AsNoTracking() - .FirstOrDefaultAsync(c => c.Isin == cleanIsin, cancellationToken); - - if (cached != null && DateTime.UtcNow - cached.CalculatedAt < DbCacheTtl) - { - if (!string.IsNullOrWhiteSpace(requestedTicker) && - !string.Equals(requestedTicker.Trim(), cleanIsin, StringComparison.OrdinalIgnoreCase) && - !string.Equals(cached.Ticker, requestedTicker, StringComparison.OrdinalIgnoreCase)) - { - return null; - } - return JsonSerializer.Deserialize(cached.AnalysisJson); - } - } - catch (Exception ex) - { - await _finlyticLogger.LogWarningAsync(SettingKeys.TechnicalAnalysisChannel, ex, "[TechnicalAnalysisDbService] Failed to read DB cache for ISIN {Isin}", cleanIsin); - } - - return null; - } - - private async Task PersistToDbCacheAsync(string cleanIsin, string querySymbol, TechnicalAnalysisDto dto, - CancellationToken cancellationToken) - { - try - { - using var scope = _scopeFactory.CreateScope(); - var db = scope.ServiceProvider.GetRequiredService(); - var json = JsonSerializer.Serialize(dto); - - var existing = await db.CachedAnalyses.FirstOrDefaultAsync(c => c.Isin == cleanIsin, cancellationToken); - if (existing != null) - { - existing.Ticker = querySymbol; - existing.AnalysisJson = json; - existing.CalculatedAt = DateTime.UtcNow; - } - else - { - db.CachedAnalyses.Add(new CachedAnalysisEntity - { - Isin = cleanIsin, - Ticker = querySymbol, - AnalysisJson = json, - CalculatedAt = DateTime.UtcNow - }); - } - - await db.SaveChangesAsync(cancellationToken); - } - catch (Exception ex) - { - await _finlyticLogger.LogErrorAsync(SettingKeys.TechnicalAnalysisChannel, ex, "[TechnicalAnalysisDbService] Failed to persist TA DB cache for ISIN {Isin}", cleanIsin); - } - } - - private static MarketCandleEntity CloneCandle(MarketCandleEntity c) => new() - { - Symbol = c.Symbol, Interval = c.Interval, Timestamp = c.Timestamp, - Open = c.Open, High = c.High, Low = c.Low, Close = c.Close, - Volume = c.Volume, Bid = c.Bid, Ask = c.Ask - }; -} \ No newline at end of file diff --git a/FinlyticTechnicalAnalysis/Services/YahooMarketDataScraper.cs b/FinlyticTechnicalAnalysis/Services/YahooMarketDataScraper.cs deleted file mode 100644 index d06db83..0000000 --- a/FinlyticTechnicalAnalysis/Services/YahooMarketDataScraper.cs +++ /dev/null @@ -1,258 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using FinlyticCore.Services; -using FinlyticCore.Services.Yahoo; -using FinlyticTechnicalAnalysis.Entities; -using FinlyticTechnicalAnalysis.Util; -using Microsoft.Extensions.Configuration; - -namespace FinlyticTechnicalAnalysis.Services; - -public record YahooCandlesResult( - List Candles, - string Currency -); - -public interface IYahooMarketDataScraper -{ - /// - /// Resolves ticker from ISIN. - /// - Task ResolveTickerFromIsinAsync(string isin, CancellationToken cancellationToken = default); - - /// - /// Fetches historical candles. - /// - Task> FetchHistoricalCandlesAsync(string symbol, string range = "1y", string interval = "1d", CancellationToken cancellationToken = default); - - /// - /// Fetches historical candles with currency. - /// - Task FetchHistoricalCandlesWithCurrencyAsync(string symbol, string range = "1y", string interval = "1d", CancellationToken cancellationToken = default); - - /// - /// Fetches macro ticker. - /// - Task FetchMacroTickerAsync(string symbol, CancellationToken cancellationToken = default); -} - -public class YahooMarketDataScraper : IYahooMarketDataScraper -{ - private readonly YahooFinanceClient _yahooClient; - private readonly IConfiguration _configuration; - private readonly IFinlyticLogger _finlyticLogger; - - public YahooMarketDataScraper( - YahooFinanceClient yahooClient, - IConfiguration configuration, - IFinlyticLogger finlyticLogger) - { - _yahooClient = yahooClient; - _configuration = configuration; - _finlyticLogger = finlyticLogger; - } - - /// - /// Resolves ticker from ISIN using Yahoo Search API or Crypto Subtitle resolution for internal ISINs. - /// - public async Task ResolveTickerFromIsinAsync(string isin, CancellationToken cancellationToken = default) - { - if (string.IsNullOrWhiteSpace(isin)) return null; - - var cleanIsin = isin.Trim().ToUpperInvariant(); - if (cleanIsin.Contains('.')) - { - return cleanIsin; - } - - if (cleanIsin.StartsWith("X", StringComparison.OrdinalIgnoreCase)) - { - var (cryptoSubtitle, cryptoName) = await FinlyticCore.Utils.CryptoSubtitleResolver.ResolveCryptoInfoAsync( - cleanIsin, _configuration.GetConnectionString("DefaultConnection"), cancellationToken); - - if (!string.IsNullOrWhiteSpace(cryptoSubtitle)) - { - var candidates = new[] { $"{cryptoSubtitle}-EUR", $"{cryptoSubtitle}-USD", cryptoSubtitle }; - foreach (var candidate in candidates) - { - try - { - var res = await FetchHistoricalCandlesWithCurrencyAsync(candidate, "5d", "1d", cancellationToken); - if (res.Candles.Count > 0) - { - await _finlyticLogger.LogInfoAsync(SettingKeys.TechnicalAnalysisChannel, "[YahooMarketDataScraper] Resolved Crypto ISIN {Isin} to {Symbol} using Subtitle {Sub}", cleanIsin, candidate, cryptoSubtitle); - return candidate; - } - } - catch { } - } - - return $"{cryptoSubtitle}-EUR"; - } - } - - try - { - var searchResult = await _yahooClient.SearchAsync(cleanIsin, quotesCount: 10, newsCount: 0, cancellationToken); - if (searchResult?.Quotes != null && searchResult.Quotes.Count > 0) - { - var symbolList = searchResult.Quotes - .Select(q => q.Symbol) - .Where(s => !string.IsNullOrEmpty(s)) - .Select(s => s!) - .ToList(); - - if (symbolList.Count > 0) - { - if (cleanIsin.StartsWith("US", StringComparison.OrdinalIgnoreCase)) - { - var noDotSymbol = symbolList.FirstOrDefault(s => !s.Contains('.')); - if (noDotSymbol != null) return noDotSymbol; - } - return symbolList[0]; - } - } - } - catch (Exception ex) - { - await _finlyticLogger.LogWarningAsync(SettingKeys.TechnicalAnalysisChannel, ex, "[YahooMarketDataScraper] Failed to resolve Yahoo ticker for ISIN {Isin}", cleanIsin); - } - - return null; - } - - /// - /// Fetches historical candles. - /// - public async Task> FetchHistoricalCandlesAsync(string symbol, string range = "1y", string interval = "1d", CancellationToken cancellationToken = default) - { - var result = await FetchHistoricalCandlesWithCurrencyAsync(symbol, range, interval, cancellationToken); - return result.Candles; - } - - /// - /// Fetches historical candles with currency metadata using authenticated Crumb/Cookie flow. - /// - public async Task FetchHistoricalCandlesWithCurrencyAsync(string symbol, string range = "1y", string interval = "1d", CancellationToken cancellationToken = default) - { - var results = new List(); - string detectedCurrency = FallbackCurrencyBySymbol(symbol); - - if (string.IsNullOrWhiteSpace(symbol)) return new YahooCandlesResult(results, detectedCurrency); - - try - { - var chartDto = await _yahooClient.GetChartAsync(symbol, range, interval, cancellationToken); - var resultObj = chartDto?.Chart?.Result?.FirstOrDefault(); - - if (resultObj == null) - { - await _finlyticLogger.LogWarningAsync(SettingKeys.TechnicalAnalysisChannel, "[YahooMarketDataScraper] No chart data returned from Yahoo Client for symbol {Symbol}", symbol); - return new YahooCandlesResult(results, detectedCurrency); - } - - if (!string.IsNullOrWhiteSpace(resultObj.Meta?.Currency)) - { - detectedCurrency = resultObj.Meta.Currency.ToUpperInvariant(); - } - - var timestamps = resultObj.Timestamp; - var quote = resultObj.Indicators?.Quote?.FirstOrDefault(); - - if (timestamps == null || quote == null || timestamps.Count == 0) - { - return new YahooCandlesResult(results, detectedCurrency); - } - - var opens = quote.Open ?? []; - var highs = quote.High ?? []; - var lows = quote.Low ?? []; - var closes = quote.Close ?? []; - var volumes = quote.Volume ?? []; - - for (int i = 0; i < timestamps.Count; i++) - { - var dt = DateTimeOffset.FromUnixTimeSeconds(timestamps[i]).UtcDateTime; - - var open = i < opens.Count && opens[i].HasValue ? (decimal)opens[i]!.Value : 0m; - var high = i < highs.Count && highs[i].HasValue ? (decimal)highs[i]!.Value : open; - var low = i < lows.Count && lows[i].HasValue ? (decimal)lows[i]!.Value : open; - var close = i < closes.Count && closes[i].HasValue ? (decimal)closes[i]!.Value : open; - var vol = i < volumes.Count && volumes[i].HasValue ? (long)volumes[i]!.Value : 0L; - - if (close <= 0m && open <= 0m) continue; - - results.Add(new MarketCandleEntity - { - Symbol = symbol.ToUpperInvariant(), - Interval = interval, - Timestamp = dt, - Open = open, - High = Math.Max(high, Math.Max(open, close)), - Low = Math.Min(low, Math.Min(open, close)), - Close = close, - Volume = vol - }); - } - - await _finlyticLogger.LogInfoAsync(SettingKeys.TechnicalAnalysisChannel, "[YahooMarketDataScraper] Successfully fetched {Count} candles for {Symbol} ({Range}, {Interval}, Currency: {Currency})", - results.Count, symbol, range, interval, detectedCurrency); - } - catch (Exception ex) - { - await _finlyticLogger.LogErrorAsync(SettingKeys.TechnicalAnalysisChannel, ex, "[YahooMarketDataScraper] Error fetching historical candles for {Symbol}", symbol); - } - - return new YahooCandlesResult(results, detectedCurrency); - } - - /// - /// Fetches macro ticker data (e.g., ^VIX, ^GSPC, DX-Y.NY). - /// - public async Task FetchMacroTickerAsync(string symbol, CancellationToken cancellationToken = default) - { - var candles = await FetchHistoricalCandlesAsync(symbol, "5d", "1d", cancellationToken); - if (candles.Count == 0) return null; - - var lastCandle = candles.Last(); - var prevCandle = candles.Count > 1 ? candles[^2] : lastCandle; - - var trendState = lastCandle.Close >= prevCandle.Close ? "Bullish" : "Bearish"; - if (symbol == "^VIX") - { - trendState = lastCandle.Close > 25m ? "HighVolatility" : (lastCandle.Close > 18m ? "Moderate" : "LowVolatility"); - } - - return new MacroDataEntity - { - Symbol = symbol, - Value = lastCandle.Close, - PreviousClose = prevCandle.Close, - TrendState = trendState, - LastUpdatedAt = DateTime.UtcNow - }; - } - - private static string FallbackCurrencyBySymbol(string symbol) - { - if (string.IsNullOrWhiteSpace(symbol)) return "EUR"; - - if (symbol.EndsWith(".DE", StringComparison.OrdinalIgnoreCase) || - symbol.EndsWith(".SG", StringComparison.OrdinalIgnoreCase) || - symbol.EndsWith(".VI", StringComparison.OrdinalIgnoreCase) || - symbol.EndsWith(".F", StringComparison.OrdinalIgnoreCase)) - { - return "EUR"; - } - - if (!symbol.Contains('.')) - { - return "USD"; - } - - return "EUR"; - } -} \ No newline at end of file diff --git a/FinlyticTechnicalAnalysis/Util/SettingKeys.cs b/FinlyticTechnicalAnalysis/Util/SettingKeys.cs deleted file mode 100644 index bfbfb8a..0000000 --- a/FinlyticTechnicalAnalysis/Util/SettingKeys.cs +++ /dev/null @@ -1,26 +0,0 @@ -using FinlyticCore.Models.Settings; - -namespace FinlyticTechnicalAnalysis.Util; - -public static class SettingKeys -{ - // --- Logging-Kanäle --- - public static readonly SettingKey TechnicalAnalysisChannel = new("Logging.Channel.TechnicalAnalysis", true); - public static readonly SettingKey MqttChannel = new("Logging.Channel.MQTT", true); - public static readonly SettingKey HealthPingChannel = new("Logging.Channel.Health", true); - - // --- Indikator-Konfiguration --- - public static readonly SettingKey RsiPeriod = new("Indicators.RsiPeriod", 14); - public static readonly SettingKey MacdFastPeriod = new("Indicators.MacdFastPeriod", 12); - public static readonly SettingKey MacdSlowPeriod = new("Indicators.MacdSlowPeriod", 26); - public static readonly SettingKey MacdSignalPeriod = new("Indicators.MacdSignalPeriod", 9); - public static readonly SettingKey EmaShortPeriod = new("Indicators.EmaShortPeriod", 50); - public static readonly SettingKey EmaLongPeriod = new("Indicators.EmaLongPeriod", 200); - public static readonly SettingKey BollingerBandsPeriod = new("Indicators.BollingerBandsPeriod", 20); - public static readonly SettingKey BollingerBandsStdDev = new("Indicators.BollingerBandsStdDev", 2.0); - public static readonly SettingKey AtrPeriod = new("Indicators.AtrPeriod", 14); - - // --- Cache & Performance --- - public static readonly SettingKey CacheDurationMinutes = new("Cache.DurationMinutes", 60); - public static readonly SettingKey EnableAutoCache = new("Feature.EnableAutoCache", true); -} diff --git a/FinlyticTechnicalAnalysis/Util/TAMqttClient.cs b/FinlyticTechnicalAnalysis/Util/TAMqttClient.cs deleted file mode 100644 index 50b7f7d..0000000 --- a/FinlyticTechnicalAnalysis/Util/TAMqttClient.cs +++ /dev/null @@ -1,301 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text.Json; -using System.Threading; -using System.Threading.Tasks; -using FinlyticCore.Dtos; -using FinlyticCore.Dtos.Settings; -using FinlyticCore.Models; -using FinlyticCore.Services; -using FinlyticCore.Util; -using FinlyticTechnicalAnalysis.Services; -using FinlyticTechnicalAnalysis.Util; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; - -namespace FinlyticTechnicalAnalysis.Util; - -public class TAMqttClient : ManagedMqttClient, IHostedService -{ - private readonly IConfiguration _configuration; - private readonly IServiceScopeFactory _scopeFactory; - private readonly ILogger _logger; - - public TAMqttClient( - ILogger logger, - IConfiguration configuration, - IServiceScopeFactory scopeFactory) : base(logger) - { - _logger = logger; - _configuration = configuration; - _scopeFactory = scopeFactory; - } - - /// - /// Starts the MQTT client. - /// - public async Task StartAsync(CancellationToken cancellationToken) - { - var host = _configuration["MQTT:Host"] ?? _configuration["MQTT__Host"] ?? "localhost"; - var portStr = _configuration["MQTT:Port"] ?? _configuration["MQTT__Port"] ?? "1883"; - var clientId = _configuration["MQTT:ClientId"] ?? "finlytic_ta_" + Guid.NewGuid().ToString("N"); - - var config = new MqttConfiguration - { - Host = host, - Port = int.TryParse(portStr, out var p) ? p : 1883, - ClientId = clientId - }; - - _logger.LogInformation("Starting Technical Analysis MQTT client. Host: {Host}, ClientId: {ClientId}", config.Host, config.ClientId); - await ConnectAsync(config); - } - - /// - /// Stops the MQTT client. - /// - public async Task StopAsync(CancellationToken cancellationToken) - { - _logger.LogInformation("Stopping Technical Analysis MQTT client."); - await DisconnectAsync(); - } - - protected override async Task OnConnectedAsync() - { - _logger.LogInformation("Technical Analysis MQTT client connected. Subscribing to RPC topics..."); - await SubscribeAsync("services/request/ta_GetAnalysis/#"); - await SubscribeAsync("services/request/tr_GetLivePrice/#"); - await SubscribeAsync("services/request/ta_settings_GetAll/#"); - await SubscribeAsync("services/request/ta_settings_Update/#"); - await SubscribeAsync("services/request/health_Ping/#"); - await SubscribeAsync("services/config/updated/#"); - - FinlyticCore.Services.FinlyticLogBroadcaster.OnLogPublished = async (logDto) => - { - if (IsConnected && string.Equals(logDto.ServiceName, "FinlyticTechnicalAnalysis", StringComparison.OrdinalIgnoreCase)) - { - await PublishAsync("finlytic/logs/FinlyticTechnicalAnalysis", logDto); - } - }; - } - - protected override async Task OnMessageReceivedAsync(string topic, string payload) - { - if (string.IsNullOrWhiteSpace(topic)) return; - - if (topic.StartsWith("services/config/updated", StringComparison.OrdinalIgnoreCase)) - { - await HandleConfigUpdatedAsync(topic, payload); - return; - } - - var segments = topic.Split('/'); - if (segments.Length < 4) return; - - var channel = segments[2]; - var correlationId = segments[^1]; - - switch (channel) - { - case "ta_GetAnalysis": - await HandleGetAnalysisAsync(payload, correlationId); - break; - - case "tr_GetLivePrice": - await HandleGetLivePriceAsync(payload, correlationId); - break; - - case "ta_settings_GetAll": - await HandleSettingsGetAllAsync(correlationId); - break; - - case "ta_settings_Update": - await HandleSettingsUpdateAsync(payload, correlationId); - break; - - case "health_Ping": - await HandleHealthPingAsync(topic, segments, correlationId); - break; - - default: - _logger.LogDebug("Received unhandled RPC channel: {Channel}", channel); - break; - } - } - - private async Task HandleSettingsGetAllAsync(string correlationId) - { - using var scope = _scopeFactory.CreateScope(); - var finlyticLogger = scope.ServiceProvider.GetRequiredService>(); - var settingsService = scope.ServiceProvider.GetRequiredService(); - - await finlyticLogger.LogInfoAsync(SettingKeys.MqttChannel, "[FinlyticTechnicalAnalysis] [Settings_GetAll] Retrieving all dynamic settings via reflection [CorrelationId: {CorrelationId}]", correlationId); - try - { - var settings = await settingsService.GetAllRegisteredSettingsAsync(new[] { typeof(SettingKeys) }); - var responseTopic = $"services/response/ta_settings_GetAll/{correlationId}"; - - await PublishAsync(responseTopic, settings); - await finlyticLogger.LogInfoAsync(SettingKeys.MqttChannel, "[FinlyticTechnicalAnalysis] [Settings_GetAll] Published {Count} settings to '{ResponseTopic}'", settings.Count, responseTopic); - } - catch (Exception ex) - { - await finlyticLogger.LogErrorAsync(SettingKeys.MqttChannel, ex, "[FinlyticTechnicalAnalysis] [Settings_GetAll] Failed to retrieve settings."); - } - } - - private async Task HandleSettingsUpdateAsync(string payload, string correlationId) - { - if (string.IsNullOrWhiteSpace(payload)) return; - - using var scope = _scopeFactory.CreateScope(); - var finlyticLogger = scope.ServiceProvider.GetRequiredService>(); - var settingsService = scope.ServiceProvider.GetRequiredService(); - - await finlyticLogger.LogInfoAsync(SettingKeys.MqttChannel, "[FinlyticTechnicalAnalysis] [Settings_Update] Processing settings update RPC [CorrelationId: {CorrelationId}]", correlationId); - try - { - Dictionary? updates = null; - try - { - updates = JsonSerializer.Deserialize>(payload); - } - catch - { - var list = JsonSerializer.Deserialize>(payload); - if (list != null) - { - updates = new Dictionary(); - foreach (var item in list) updates[item.Key] = item.Value; - } - } - - if (updates != null && updates.Count > 0) - { - await settingsService.UpdateSettingsAsync(updates); - await finlyticLogger.LogInfoAsync(SettingKeys.MqttChannel, "[FinlyticTechnicalAnalysis] [Settings_Update] Successfully updated {Count} settings in database and cache.", updates.Count); - } - - var currentSettings = await settingsService.GetAllRegisteredSettingsAsync(new[] { typeof(SettingKeys) }); - var responseTopic = $"services/response/ta_settings_Update/{correlationId}"; - await PublishAsync(responseTopic, currentSettings); - } - catch (Exception ex) - { - await finlyticLogger.LogErrorAsync(SettingKeys.MqttChannel, ex, "[FinlyticTechnicalAnalysis] [Settings_Update] Failed to update settings."); - } - } - - private async Task HandleConfigUpdatedAsync(string topic, string payload) - { - if (!topic.EndsWith("FinlyticTechnicalAnalysis", StringComparison.OrdinalIgnoreCase)) - return; - - try - { - using var doc = JsonDocument.Parse(payload); - if (doc.RootElement.TryGetProperty("settings", out var settingsProp)) - { - var dict = JsonSerializer.Deserialize>(settingsProp.GetRawText()); - if (dict != null && dict.Count > 0) - { - using var scope = _scopeFactory.CreateScope(); - var settings = scope.ServiceProvider.GetRequiredService(); - await settings.UpdateSettingsAsync(dict); - } - } - } - catch { } - } - - private async Task HandleHealthPingAsync(string topic, string[] segments, string correlationId) - { - bool isForMe = segments.Length >= 5 - ? segments[3].Equals("FinlyticTechnicalAnalysis", StringComparison.OrdinalIgnoreCase) - : topic.Contains("FinlyticTechnicalAnalysis", StringComparison.OrdinalIgnoreCase); - - if (isForMe) - { - string respTopic = $"services/response/health_Ping/{correlationId}"; - await PublishAsync(respTopic, new ServiceHealthResponse("FinlyticTechnicalAnalysis", "Online", DateTime.UtcNow, "Connected")); - - using var scope = _scopeFactory.CreateScope(); - var finlyticLogger = scope.ServiceProvider.GetRequiredService>(); - await finlyticLogger.LogInfoAsync(SettingKeys.HealthPingChannel, "[FinlyticTechnicalAnalysis] Responded to live health_Ping RPC request [CorrelationId: {CorrelationId}].", correlationId); - } - } - - private async Task HandleGetAnalysisAsync(string payload, string correlationId) - { - using var scope = _scopeFactory.CreateScope(); - var finlyticLogger = scope.ServiceProvider.GetRequiredService>(); - - await finlyticLogger.LogInfoAsync(SettingKeys.TechnicalAnalysisChannel, "[FinlyticTechnicalAnalysis] Received RPC ta_GetAnalysis request. CorrelationId: {CorrelationId}", correlationId); - - var req = JsonSerializer.Deserialize(payload, FinlyticJsonSerializerContext.Default.IsinRequest); - string responseTopic = $"services/response/ta_GetAnalysis/{correlationId}"; - - if (string.IsNullOrWhiteSpace(req?.Isin)) - { - await finlyticLogger.LogWarningAsync(SettingKeys.TechnicalAnalysisChannel, "[FinlyticTechnicalAnalysis] Request missing mandatory ISIN parameter."); - await PublishAsync(responseTopic, null); - return; - } - - try - { - var taDbService = scope.ServiceProvider.GetRequiredService(); - var analysis = await taDbService.GetAnalysisAsync(req.Isin, req.ForceRefresh, req.Ticker); - - await finlyticLogger.LogInfoAsync(SettingKeys.TechnicalAnalysisChannel, "[FinlyticTechnicalAnalysis] Publishing RPC response to {ResponseTopic}", responseTopic); - await PublishAsync(responseTopic, analysis); - } - catch (Exception ex) - { - await finlyticLogger.LogErrorAsync(SettingKeys.TechnicalAnalysisChannel, ex, "[FinlyticTechnicalAnalysis] Failed to fetch technical analysis for ISIN {Isin}", req.Isin); - try - { - await PublishAsync(responseTopic, null); - } - catch { } - } - } - - private async Task HandleGetLivePriceAsync(string payload, string correlationId) - { - using var scope = _scopeFactory.CreateScope(); - var finlyticLogger = scope.ServiceProvider.GetRequiredService>(); - - await finlyticLogger.LogInfoAsync(SettingKeys.TechnicalAnalysisChannel, "[FinlyticTechnicalAnalysis] Received RPC tr_GetLivePrice request. CorrelationId: {CorrelationId}", correlationId); - - var req = JsonSerializer.Deserialize(payload, FinlyticJsonSerializerContext.Default.IsinRequest); - string responseTopic = $"services/response/tr_GetLivePrice/{correlationId}"; - - if (string.IsNullOrWhiteSpace(req?.Isin)) - { - await finlyticLogger.LogWarningAsync(SettingKeys.TechnicalAnalysisChannel, "[FinlyticTechnicalAnalysis] tr_GetLivePrice request missing mandatory ISIN parameter."); - await PublishAsync(responseTopic, null); - return; - } - - try - { - var taDbService = scope.ServiceProvider.GetRequiredService(); - var livePrice = await taDbService.GetLivePriceAsync(req.Isin); - - await finlyticLogger.LogInfoAsync(SettingKeys.TechnicalAnalysisChannel, "[FinlyticTechnicalAnalysis] Publishing RPC response to {ResponseTopic} for ISIN {Isin}", responseTopic, req.Isin); - await PublishAsync(responseTopic, livePrice); - } - catch (Exception ex) - { - await finlyticLogger.LogErrorAsync(SettingKeys.TechnicalAnalysisChannel, ex, "[FinlyticTechnicalAnalysis] Failed to fetch live price for ISIN {Isin}", req.Isin); - try - { - await PublishAsync(responseTopic, null); - } - catch { } - } - } -} \ No newline at end of file diff --git a/FinlyticTechnicalAnalysis/appsettings.json b/FinlyticTechnicalAnalysis/appsettings.json deleted file mode 100644 index 5865715..0000000 --- a/FinlyticTechnicalAnalysis/appsettings.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft.Hosting.Lifetime": "Information", - "FinlyticCore.Services.TradeRepublic.TradeRepublicClient": "Debug" - } - }, - "ConnectionStrings": { - "DefaultConnection": "Host=localhost;Database=finlytic_ta;Username=admin;Password=admin" - }, - "MQTT": { - "Host": "localhost", - "Port": "4545", - "ClientId": "finlytic_ta" - } -} diff --git a/FinlyticTrades/Database/TradesDbContext.cs b/FinlyticTrades/Database/TradesDbContext.cs deleted file mode 100644 index 780158e..0000000 --- a/FinlyticTrades/Database/TradesDbContext.cs +++ /dev/null @@ -1,74 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using FinlyticCore.Database; -using FinlyticCore.Entities.Settings; -using FinlyticTrades.Entities; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Design; - -namespace FinlyticTrades.Database; - -public class TradesDbContext : DbContext, ISettingsDbContext -{ - public TradesDbContext(DbContextOptions options) : base(options) { } - - public DbSet DynamicSettings => Set(); - public DbSet Trades => Set(); - public DbSet TradeHourlyUpdates => Set(); - public DbSet Settings => Set(); - - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - base.OnModelCreating(modelBuilder); - - modelBuilder.Entity(entity => - { - entity.HasKey(e => e.Id); - entity.HasIndex(e => e.Key).IsUnique(); - }); - - var stringListConverter = - new Microsoft.EntityFrameworkCore.Storage.ValueConversion.ValueConverter, string>( - v => System.Text.Json.JsonSerializer.Serialize(v, (System.Text.Json.JsonSerializerOptions?)null), - v => System.Text.Json.JsonSerializer.Deserialize>(v, - (System.Text.Json.JsonSerializerOptions?)null) ?? new List() - ); - - var stringListComparer = new Microsoft.EntityFrameworkCore.ChangeTracking.ValueComparer>( - (c1, c2) => c1 != null && c2 != null ? c1.SequenceEqual(c2) : c1 == c2, - c => c.Aggregate(0, (a, v) => HashCode.Combine(a, v.GetHashCode())), - c => c.ToList() - ); - - modelBuilder.Entity(entity => - { - entity.HasIndex(e => e.TradeId).IsUnique(); - entity.HasIndex(e => e.AnalysisId); - entity.HasIndex(e => e.EventId); - entity.HasIndex(e => e.Status); - entity.HasIndex(e => e.Sector); - entity.HasIndex(e => e.Isin); - entity.HasIndex(e => e.CreatedAt); - - entity.Property(e => e.DerivativeProductCategories) - .HasConversion(stringListConverter, stringListComparer); - }); - - modelBuilder.Entity(entity => - { - entity.HasIndex(e => e.TradeId); - entity.HasIndex(e => e.Timestamp); - }); - } -} - -public class TradesDbContextFactory : IDesignTimeDbContextFactory -{ - public TradesDbContext CreateDbContext(string[] args) - { - var optionsBuilder = new DbContextOptionsBuilder(); - optionsBuilder.UseNpgsql("Host=localhost;Database=trades;Username=postgres;Password=postgres"); - return new TradesDbContext(optionsBuilder.Options); - } -} diff --git a/FinlyticTrades/Dockerfile b/FinlyticTrades/Dockerfile deleted file mode 100644 index 7a74084..0000000 --- a/FinlyticTrades/Dockerfile +++ /dev/null @@ -1,16 +0,0 @@ -FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build -WORKDIR /src -COPY ["FinlyticCore/FinlyticCore.csproj", "FinlyticCore/"] -COPY ["FinlyticTrades/FinlyticTrades.csproj", "FinlyticTrades/"] -RUN dotnet restore "FinlyticTrades/FinlyticTrades.csproj" -COPY . . -WORKDIR "/src/FinlyticTrades" -RUN dotnet build "FinlyticTrades.csproj" -c Release -o /app/build - -FROM build AS publish -RUN dotnet publish "FinlyticTrades.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", "FinlyticTrades.dll"] diff --git a/FinlyticTrades/Entities/TradeEntity.cs b/FinlyticTrades/Entities/TradeEntity.cs deleted file mode 100644 index e7ee310..0000000 --- a/FinlyticTrades/Entities/TradeEntity.cs +++ /dev/null @@ -1,156 +0,0 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; -using FinlyticCore.Models.Analyzer; -using FinlyticCore.Models.Trades; - -namespace FinlyticTrades.Entities; - -[Table("trades")] -public class TradeEntity -{ - [Key] - public Guid Id { get; set; } = Guid.NewGuid(); - - [Required] - [MaxLength(100)] - public string TradeId { get; set; } = string.Empty; - - [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; - - [MaxLength(150)] - public string CompanyName { get; set; } = string.Empty; - - public TradeStatus Status { get; set; } = TradeStatus.Proposed; - - [MaxLength(100)] - public string? UserId { get; set; } - - public bool IsGlobalProposal { get; set; } = true; - - [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; } - - [MaxLength(10)] - public string SignalType { get; set; } = "BUY"; - - [MaxLength(30)] - public string RiskTolerance { get; set; } = "Moderate"; - - [MaxLength(20)] - public string Timeframe { get; set; } = "1D"; - - [MaxLength(30)] - public string InstrumentType { get; set; } = "Stock"; - - [MaxLength(50)] - public string AssetType { get; set; } = "stock"; - - public bool HasCfd { get; set; } - - public List DerivativeProductCategories { get; set; } = new(); - - [MaxLength(20)] - public string? DerivativeIsin { get; set; } - - public double WinRate { get; set; } - public VixMarketRegime VixRegime { get; set; } - - [Column(TypeName = "decimal(18,4)")] - public decimal VixValue { get; set; } - - public int TtlMinutes { get; set; } = 60; - public string Reasoning { get; set; } = string.Empty; - - // --- New Fields for Detailed Execution & Rationale --- - [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; } // Stored as comma separated values - - [Column(TypeName = "decimal(18,4)")] - public decimal? RiskRewardRatio { get; set; } - - [Column(TypeName = "decimal(18,4)")] - public decimal? MaxLeverage { get; set; } - - public string TechnicalRationale { get; set; } = string.Empty; - public string FundamentalRationale { get; set; } = string.Empty; - public string RiskWarning { get; set; } = string.Empty; - - // --- User Exit Data --- - [Column(TypeName = "decimal(18,4)")] - public decimal? UserExitPrice { get; set; } - - public DateTime? UserExitTimestamp { get; set; } - - // --- Real Trade Execution Data --- - [Column(TypeName = "decimal(18,4)")] - public decimal? ActualEntryPrice { get; set; } - - [Column(TypeName = "decimal(18,4)")] - public decimal? PositionSize { get; set; } - - [Column(TypeName = "decimal(18,4)")] - public decimal? LeverageUsed { get; set; } - - [Column(TypeName = "decimal(18,4)")] - public decimal? EntryFee { get; set; } - - [Column(TypeName = "decimal(18,4)")] - public decimal? ExitFee { get; set; } - - public DateTime? ExecutionTimestamp { get; set; } - - [Column(TypeName = "decimal(18,4)")] - public decimal? Quantity { get; set; } - - [Column(TypeName = "decimal(18,4)")] - public decimal? KnockoutThreshold { get; set; } - - public bool IsRecurring { get; set; } = false; - - [MaxLength(50)] - public string? CloseReason { get; set; } - - [Column(TypeName = "decimal(18,4)")] - public decimal? PnlAbsolute { get; set; } - - [Column(TypeName = "decimal(18,4)")] - public decimal? PnlPercent { get; set; } - - public bool? IsWin { get; set; } - - public DateTime CreatedAt { get; set; } = DateTime.UtcNow; - public DateTime? ClosedAt { get; set; } - - public List HourlyUpdates { get; set; } = new(); -} diff --git a/FinlyticTrades/Entities/TradeHourlyUpdateEntity.cs b/FinlyticTrades/Entities/TradeHourlyUpdateEntity.cs deleted file mode 100644 index 9615a60..0000000 --- a/FinlyticTrades/Entities/TradeHourlyUpdateEntity.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System; -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; -using Microsoft.EntityFrameworkCore; - -namespace FinlyticTrades.Entities; - -[Table("trade_hourly_updates")] -[Index(nameof(TradeId), nameof(Timestamp))] -public class TradeHourlyUpdateEntity -{ - [Key] - public Guid Id { get; set; } = Guid.NewGuid(); - - [Required] - public Guid TradeId { get; set; } - - [ForeignKey(nameof(TradeId))] - public TradeEntity? Trade { get; set; } - - [Required] - [MaxLength(30)] - public string Recommendation { get; set; } = "Hold"; // "Hold", "AdjustSL", "AdjustTP", "Close" - - [Column(TypeName = "decimal(18,4)")] - public decimal CurrentPrice { get; set; } - - [Column(TypeName = "decimal(18,4)")] - public decimal? SuggestedStopLoss { get; set; } - - [Column(TypeName = "decimal(18,4)")] - public decimal? SuggestedTakeProfit { get; set; } - - [Column(TypeName = "decimal(18,4)")] - public decimal VixValue { get; set; } - - [Column(TypeName = "decimal(18,4)")] - public decimal? FloatingPnlPercent { get; set; } - - public string Reasoning { get; set; } = string.Empty; - - public DateTime Timestamp { get; set; } = DateTime.UtcNow; -} \ No newline at end of file diff --git a/FinlyticTrades/Entities/TradesSettingsEntity.cs b/FinlyticTrades/Entities/TradesSettingsEntity.cs deleted file mode 100644 index a18e9d6..0000000 --- a/FinlyticTrades/Entities/TradesSettingsEntity.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; -using System.ComponentModel.DataAnnotations; - -namespace FinlyticTrades.Entities; - -public class TradesSettingsEntity -{ - [Key] - public Guid Id { get; set; } - - public double AtrStopLossMultiplier { get; set; } = 1.5; - public double RiskPerTradePercentage { get; set; } = 1.0; - public int MaxOpenPositions { get; set; } = 5; - public DateTime UpdatedAt { get; set; } = DateTime.UtcNow; -} diff --git a/FinlyticTrades/FinlyticTrades.csproj b/FinlyticTrades/FinlyticTrades.csproj deleted file mode 100644 index 416af55..0000000 --- a/FinlyticTrades/FinlyticTrades.csproj +++ /dev/null @@ -1,24 +0,0 @@ - - - - net10.0 - enable - enable - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - - - - diff --git a/FinlyticTrades/Migrations/20260801073417_Init.Designer.cs b/FinlyticTrades/Migrations/20260801073417_Init.Designer.cs deleted file mode 100644 index 7753c08..0000000 --- a/FinlyticTrades/Migrations/20260801073417_Init.Designer.cs +++ /dev/null @@ -1,245 +0,0 @@ -// -using System; -using FinlyticTrades.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 FinlyticTrades.Migrations -{ - [DbContext(typeof(TradesDbContext))] - [Migration("20260801073417_Init")] - partial class Init - { - /// - 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("FinlyticTrades.Entities.TradeEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("AnalysisId") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.Property("CloseReason") - .HasMaxLength(50) - .HasColumnType("character varying(50)"); - - b.Property("ClosedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("CompanyName") - .IsRequired() - .HasMaxLength(150) - .HasColumnType("character varying(150)"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("EntryPrice") - .HasColumnType("decimal(18,4)"); - - b.Property("EventId") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.Property("InstrumentType") - .IsRequired() - .HasMaxLength(30) - .HasColumnType("character varying(30)"); - - b.Property("IsWin") - .HasColumnType("boolean"); - - b.Property("Isin") - .IsRequired() - .HasMaxLength(30) - .HasColumnType("character varying(30)"); - - b.Property("PnlAbsolute") - .HasColumnType("decimal(18,4)"); - - b.Property("PnlPercent") - .HasColumnType("decimal(18,4)"); - - b.Property("Reasoning") - .IsRequired() - .HasColumnType("text"); - - b.Property("RiskTolerance") - .IsRequired() - .HasMaxLength(30) - .HasColumnType("character varying(30)"); - - b.Property("Sector") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("character varying(50)"); - - b.Property("SignalType") - .IsRequired() - .HasMaxLength(10) - .HasColumnType("character varying(10)"); - - b.Property("Status") - .HasColumnType("integer"); - - b.Property("StopLoss") - .HasColumnType("decimal(18,4)"); - - b.Property("Symbol") - .IsRequired() - .HasMaxLength(30) - .HasColumnType("character varying(30)"); - - b.Property("TakeProfit") - .HasColumnType("decimal(18,4)"); - - b.Property("Timeframe") - .IsRequired() - .HasMaxLength(20) - .HasColumnType("character varying(20)"); - - b.Property("TradeId") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.Property("TtlMinutes") - .HasColumnType("integer"); - - b.Property("UserExitPrice") - .HasColumnType("decimal(18,4)"); - - b.Property("UserExitTimestamp") - .HasColumnType("timestamp with time zone"); - - b.Property("VixRegime") - .HasColumnType("integer"); - - b.Property("VixValue") - .HasColumnType("decimal(18,4)"); - - b.Property("WinRate") - .HasColumnType("double precision"); - - b.HasKey("Id"); - - b.HasIndex("AnalysisId"); - - b.HasIndex("CreatedAt"); - - b.HasIndex("EventId"); - - b.HasIndex("Isin"); - - b.HasIndex("Sector"); - - b.HasIndex("Status"); - - b.HasIndex("TradeId") - .IsUnique(); - - b.ToTable("trades"); - }); - - modelBuilder.Entity("FinlyticTrades.Entities.TradeHourlyUpdateEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("CurrentPrice") - .HasColumnType("decimal(18,4)"); - - b.Property("Reasoning") - .IsRequired() - .HasColumnType("text"); - - b.Property("Recommendation") - .IsRequired() - .HasMaxLength(30) - .HasColumnType("character varying(30)"); - - b.Property("SuggestedStopLoss") - .HasColumnType("decimal(18,4)"); - - b.Property("SuggestedTakeProfit") - .HasColumnType("decimal(18,4)"); - - b.Property("Timestamp") - .HasColumnType("timestamp with time zone"); - - b.Property("TradeId") - .HasColumnType("uuid"); - - b.Property("VixValue") - .HasColumnType("decimal(18,4)"); - - b.HasKey("Id"); - - b.HasIndex("Timestamp"); - - b.HasIndex("TradeId"); - - b.ToTable("trade_hourly_updates"); - }); - - modelBuilder.Entity("FinlyticTrades.Entities.TradesSettingsEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("AtrStopLossMultiplier") - .HasColumnType("double precision"); - - b.Property("MaxOpenPositions") - .HasColumnType("integer"); - - b.Property("RiskPerTradePercentage") - .HasColumnType("double precision"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.ToTable("Settings"); - }); - - modelBuilder.Entity("FinlyticTrades.Entities.TradeHourlyUpdateEntity", b => - { - b.HasOne("FinlyticTrades.Entities.TradeEntity", "Trade") - .WithMany("HourlyUpdates") - .HasForeignKey("TradeId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Trade"); - }); - - modelBuilder.Entity("FinlyticTrades.Entities.TradeEntity", b => - { - b.Navigation("HourlyUpdates"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/FinlyticTrades/Migrations/20260801073417_Init.cs b/FinlyticTrades/Migrations/20260801073417_Init.cs deleted file mode 100644 index a18b7ea..0000000 --- a/FinlyticTrades/Migrations/20260801073417_Init.cs +++ /dev/null @@ -1,153 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace FinlyticTrades.Migrations -{ - /// - public partial class Init : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "Settings", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - AtrStopLossMultiplier = table.Column(type: "double precision", nullable: false), - RiskPerTradePercentage = table.Column(type: "double precision", nullable: false), - MaxOpenPositions = table.Column(type: "integer", nullable: false), - UpdatedAt = table.Column(type: "timestamp with time zone", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_Settings", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "trades", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - TradeId = table.Column(type: "character varying(100)", maxLength: 100, nullable: false), - AnalysisId = table.Column(type: "character varying(100)", maxLength: 100, nullable: false), - EventId = table.Column(type: "character varying(100)", maxLength: 100, nullable: false), - Sector = table.Column(type: "character varying(50)", maxLength: 50, nullable: false), - Symbol = table.Column(type: "character varying(30)", maxLength: 30, nullable: false), - Isin = table.Column(type: "character varying(30)", maxLength: 30, nullable: false), - CompanyName = table.Column(type: "character varying(150)", maxLength: 150, nullable: false), - Status = table.Column(type: "integer", nullable: false), - EntryPrice = table.Column(type: "numeric(18,4)", nullable: false), - StopLoss = table.Column(type: "numeric(18,4)", nullable: false), - TakeProfit = table.Column(type: "numeric(18,4)", nullable: false), - SignalType = table.Column(type: "character varying(10)", maxLength: 10, nullable: false), - RiskTolerance = table.Column(type: "character varying(30)", maxLength: 30, nullable: false), - Timeframe = table.Column(type: "character varying(20)", maxLength: 20, nullable: false), - InstrumentType = table.Column(type: "character varying(30)", maxLength: 30, nullable: false), - WinRate = table.Column(type: "double precision", nullable: false), - VixRegime = table.Column(type: "integer", nullable: false), - VixValue = table.Column(type: "numeric(18,4)", nullable: false), - TtlMinutes = table.Column(type: "integer", nullable: false), - Reasoning = table.Column(type: "text", nullable: false), - UserExitPrice = table.Column(type: "numeric(18,4)", nullable: true), - UserExitTimestamp = table.Column(type: "timestamp with time zone", nullable: true), - CloseReason = table.Column(type: "character varying(50)", maxLength: 50, nullable: true), - PnlAbsolute = table.Column(type: "numeric(18,4)", nullable: true), - PnlPercent = table.Column(type: "numeric(18,4)", nullable: true), - IsWin = table.Column(type: "boolean", nullable: true), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - ClosedAt = table.Column(type: "timestamp with time zone", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_trades", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "trade_hourly_updates", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - TradeId = table.Column(type: "uuid", nullable: false), - Recommendation = table.Column(type: "character varying(30)", maxLength: 30, nullable: false), - CurrentPrice = table.Column(type: "numeric(18,4)", nullable: false), - SuggestedStopLoss = table.Column(type: "numeric(18,4)", nullable: true), - SuggestedTakeProfit = table.Column(type: "numeric(18,4)", nullable: true), - VixValue = table.Column(type: "numeric(18,4)", nullable: false), - Reasoning = table.Column(type: "text", nullable: false), - Timestamp = table.Column(type: "timestamp with time zone", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_trade_hourly_updates", x => x.Id); - table.ForeignKey( - name: "FK_trade_hourly_updates_trades_TradeId", - column: x => x.TradeId, - principalTable: "trades", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateIndex( - name: "IX_trade_hourly_updates_Timestamp", - table: "trade_hourly_updates", - column: "Timestamp"); - - migrationBuilder.CreateIndex( - name: "IX_trade_hourly_updates_TradeId", - table: "trade_hourly_updates", - column: "TradeId"); - - migrationBuilder.CreateIndex( - name: "IX_trades_AnalysisId", - table: "trades", - column: "AnalysisId"); - - migrationBuilder.CreateIndex( - name: "IX_trades_CreatedAt", - table: "trades", - column: "CreatedAt"); - - migrationBuilder.CreateIndex( - name: "IX_trades_EventId", - table: "trades", - column: "EventId"); - - migrationBuilder.CreateIndex( - name: "IX_trades_Isin", - table: "trades", - column: "Isin"); - - migrationBuilder.CreateIndex( - name: "IX_trades_Sector", - table: "trades", - column: "Sector"); - - migrationBuilder.CreateIndex( - name: "IX_trades_Status", - table: "trades", - column: "Status"); - - migrationBuilder.CreateIndex( - name: "IX_trades_TradeId", - table: "trades", - column: "TradeId", - unique: true); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "Settings"); - - migrationBuilder.DropTable( - name: "trade_hourly_updates"); - - migrationBuilder.DropTable( - name: "trades"); - } - } -} diff --git a/FinlyticTrades/Migrations/20260802205654_ExpandTradeEntity.Designer.cs b/FinlyticTrades/Migrations/20260802205654_ExpandTradeEntity.Designer.cs deleted file mode 100644 index ec1f3d8..0000000 --- a/FinlyticTrades/Migrations/20260802205654_ExpandTradeEntity.Designer.cs +++ /dev/null @@ -1,272 +0,0 @@ -// -using System; -using FinlyticTrades.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 FinlyticTrades.Migrations -{ - [DbContext(typeof(TradesDbContext))] - [Migration("20260802205654_ExpandTradeEntity")] - partial class ExpandTradeEntity - { - /// - 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("FinlyticTrades.Entities.TradeEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("AnalysisId") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.Property("CloseReason") - .HasMaxLength(50) - .HasColumnType("character varying(50)"); - - b.Property("ClosedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("CompanyName") - .IsRequired() - .HasMaxLength(150) - .HasColumnType("character varying(150)"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("EntryPrice") - .HasColumnType("decimal(18,4)"); - - b.Property("EntryZoneMax") - .HasColumnType("decimal(18,4)"); - - b.Property("EntryZoneMin") - .HasColumnType("decimal(18,4)"); - - b.Property("EventId") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.Property("FundamentalRationale") - .IsRequired() - .HasColumnType("text"); - - b.Property("InstrumentType") - .IsRequired() - .HasMaxLength(30) - .HasColumnType("character varying(30)"); - - b.Property("IsWin") - .HasColumnType("boolean"); - - b.Property("Isin") - .IsRequired() - .HasMaxLength(30) - .HasColumnType("character varying(30)"); - - b.Property("MaxLeverage") - .HasColumnType("decimal(18,4)"); - - b.Property("PnlAbsolute") - .HasColumnType("decimal(18,4)"); - - b.Property("PnlPercent") - .HasColumnType("decimal(18,4)"); - - b.Property("Reasoning") - .IsRequired() - .HasColumnType("text"); - - b.Property("RiskRewardRatio") - .HasColumnType("decimal(18,4)"); - - b.Property("RiskTolerance") - .IsRequired() - .HasMaxLength(30) - .HasColumnType("character varying(30)"); - - b.Property("RiskWarning") - .IsRequired() - .HasColumnType("text"); - - b.Property("Sector") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("character varying(50)"); - - b.Property("SignalType") - .IsRequired() - .HasMaxLength(10) - .HasColumnType("character varying(10)"); - - b.Property("Status") - .HasColumnType("integer"); - - b.Property("StopLoss") - .HasColumnType("decimal(18,4)"); - - b.Property("Symbol") - .IsRequired() - .HasMaxLength(30) - .HasColumnType("character varying(30)"); - - b.Property("TakeProfit") - .HasColumnType("decimal(18,4)"); - - b.Property("TakeProfitTargets") - .HasColumnType("text"); - - b.Property("TechnicalRationale") - .IsRequired() - .HasColumnType("text"); - - b.Property("Timeframe") - .IsRequired() - .HasMaxLength(20) - .HasColumnType("character varying(20)"); - - b.Property("TradeId") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.Property("TtlMinutes") - .HasColumnType("integer"); - - b.Property("UserExitPrice") - .HasColumnType("decimal(18,4)"); - - b.Property("UserExitTimestamp") - .HasColumnType("timestamp with time zone"); - - b.Property("VixRegime") - .HasColumnType("integer"); - - b.Property("VixValue") - .HasColumnType("decimal(18,4)"); - - b.Property("WinRate") - .HasColumnType("double precision"); - - b.HasKey("Id"); - - b.HasIndex("AnalysisId"); - - b.HasIndex("CreatedAt"); - - b.HasIndex("EventId"); - - b.HasIndex("Isin"); - - b.HasIndex("Sector"); - - b.HasIndex("Status"); - - b.HasIndex("TradeId") - .IsUnique(); - - b.ToTable("trades"); - }); - - modelBuilder.Entity("FinlyticTrades.Entities.TradeHourlyUpdateEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("CurrentPrice") - .HasColumnType("decimal(18,4)"); - - b.Property("Reasoning") - .IsRequired() - .HasColumnType("text"); - - b.Property("Recommendation") - .IsRequired() - .HasMaxLength(30) - .HasColumnType("character varying(30)"); - - b.Property("SuggestedStopLoss") - .HasColumnType("decimal(18,4)"); - - b.Property("SuggestedTakeProfit") - .HasColumnType("decimal(18,4)"); - - b.Property("Timestamp") - .HasColumnType("timestamp with time zone"); - - b.Property("TradeId") - .HasColumnType("uuid"); - - b.Property("VixValue") - .HasColumnType("decimal(18,4)"); - - b.HasKey("Id"); - - b.HasIndex("Timestamp"); - - b.HasIndex("TradeId"); - - b.ToTable("trade_hourly_updates"); - }); - - modelBuilder.Entity("FinlyticTrades.Entities.TradesSettingsEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("AtrStopLossMultiplier") - .HasColumnType("double precision"); - - b.Property("MaxOpenPositions") - .HasColumnType("integer"); - - b.Property("RiskPerTradePercentage") - .HasColumnType("double precision"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.ToTable("Settings"); - }); - - modelBuilder.Entity("FinlyticTrades.Entities.TradeHourlyUpdateEntity", b => - { - b.HasOne("FinlyticTrades.Entities.TradeEntity", "Trade") - .WithMany("HourlyUpdates") - .HasForeignKey("TradeId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Trade"); - }); - - modelBuilder.Entity("FinlyticTrades.Entities.TradeEntity", b => - { - b.Navigation("HourlyUpdates"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/FinlyticTrades/Migrations/20260802205654_ExpandTradeEntity.cs b/FinlyticTrades/Migrations/20260802205654_ExpandTradeEntity.cs deleted file mode 100644 index 8c1ff27..0000000 --- a/FinlyticTrades/Migrations/20260802205654_ExpandTradeEntity.cs +++ /dev/null @@ -1,101 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace FinlyticTrades.Migrations -{ - /// - public partial class ExpandTradeEntity : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "EntryZoneMax", - table: "trades", - type: "numeric(18,4)", - nullable: true); - - migrationBuilder.AddColumn( - name: "EntryZoneMin", - table: "trades", - type: "numeric(18,4)", - nullable: true); - - migrationBuilder.AddColumn( - name: "FundamentalRationale", - table: "trades", - type: "text", - nullable: false, - defaultValue: ""); - - migrationBuilder.AddColumn( - name: "MaxLeverage", - table: "trades", - type: "numeric(18,4)", - nullable: true); - - migrationBuilder.AddColumn( - name: "RiskRewardRatio", - table: "trades", - type: "numeric(18,4)", - nullable: true); - - migrationBuilder.AddColumn( - name: "RiskWarning", - table: "trades", - type: "text", - nullable: false, - defaultValue: ""); - - migrationBuilder.AddColumn( - name: "TakeProfitTargets", - table: "trades", - type: "text", - nullable: true); - - migrationBuilder.AddColumn( - name: "TechnicalRationale", - table: "trades", - type: "text", - nullable: false, - defaultValue: ""); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropColumn( - name: "EntryZoneMax", - table: "trades"); - - migrationBuilder.DropColumn( - name: "EntryZoneMin", - table: "trades"); - - migrationBuilder.DropColumn( - name: "FundamentalRationale", - table: "trades"); - - migrationBuilder.DropColumn( - name: "MaxLeverage", - table: "trades"); - - migrationBuilder.DropColumn( - name: "RiskRewardRatio", - table: "trades"); - - migrationBuilder.DropColumn( - name: "RiskWarning", - table: "trades"); - - migrationBuilder.DropColumn( - name: "TakeProfitTargets", - table: "trades"); - - migrationBuilder.DropColumn( - name: "TechnicalRationale", - table: "trades"); - } - } -} diff --git a/FinlyticTrades/Migrations/20260803170643_AddMultiUserTradeExecution.Designer.cs b/FinlyticTrades/Migrations/20260803170643_AddMultiUserTradeExecution.Designer.cs deleted file mode 100644 index da7d558..0000000 --- a/FinlyticTrades/Migrations/20260803170643_AddMultiUserTradeExecution.Designer.cs +++ /dev/null @@ -1,306 +0,0 @@ -// -using System; -using FinlyticTrades.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 FinlyticTrades.Migrations -{ - [DbContext(typeof(TradesDbContext))] - [Migration("20260803170643_AddMultiUserTradeExecution")] - partial class AddMultiUserTradeExecution - { - /// - 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("FinlyticTrades.Entities.TradeEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("ActualEntryPrice") - .HasColumnType("decimal(18,4)"); - - b.Property("AnalysisId") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.Property("CloseReason") - .HasMaxLength(50) - .HasColumnType("character varying(50)"); - - b.Property("ClosedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("CompanyName") - .IsRequired() - .HasMaxLength(150) - .HasColumnType("character varying(150)"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("EntryFee") - .HasColumnType("decimal(18,4)"); - - b.Property("EntryPrice") - .HasColumnType("decimal(18,4)"); - - b.Property("EntryZoneMax") - .HasColumnType("decimal(18,4)"); - - b.Property("EntryZoneMin") - .HasColumnType("decimal(18,4)"); - - b.Property("EventId") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.Property("ExecutionTimestamp") - .HasColumnType("timestamp with time zone"); - - b.Property("ExitFee") - .HasColumnType("decimal(18,4)"); - - b.Property("FundamentalRationale") - .IsRequired() - .HasColumnType("text"); - - b.Property("InstrumentType") - .IsRequired() - .HasMaxLength(30) - .HasColumnType("character varying(30)"); - - b.Property("IsGlobalProposal") - .HasColumnType("boolean"); - - b.Property("IsRecurring") - .HasColumnType("boolean"); - - b.Property("IsWin") - .HasColumnType("boolean"); - - b.Property("Isin") - .IsRequired() - .HasMaxLength(30) - .HasColumnType("character varying(30)"); - - b.Property("KnockoutThreshold") - .HasColumnType("decimal(18,4)"); - - b.Property("LeverageUsed") - .HasColumnType("decimal(18,4)"); - - b.Property("MaxLeverage") - .HasColumnType("decimal(18,4)"); - - b.Property("PnlAbsolute") - .HasColumnType("decimal(18,4)"); - - b.Property("PnlPercent") - .HasColumnType("decimal(18,4)"); - - b.Property("PositionSize") - .HasColumnType("decimal(18,4)"); - - b.Property("Quantity") - .HasColumnType("decimal(18,4)"); - - b.Property("Reasoning") - .IsRequired() - .HasColumnType("text"); - - b.Property("RiskRewardRatio") - .HasColumnType("decimal(18,4)"); - - b.Property("RiskTolerance") - .IsRequired() - .HasMaxLength(30) - .HasColumnType("character varying(30)"); - - b.Property("RiskWarning") - .IsRequired() - .HasColumnType("text"); - - b.Property("Sector") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("character varying(50)"); - - b.Property("SignalType") - .IsRequired() - .HasMaxLength(10) - .HasColumnType("character varying(10)"); - - b.Property("Status") - .HasColumnType("integer"); - - b.Property("StopLoss") - .HasColumnType("decimal(18,4)"); - - b.Property("Symbol") - .IsRequired() - .HasMaxLength(30) - .HasColumnType("character varying(30)"); - - b.Property("TakeProfit") - .HasColumnType("decimal(18,4)"); - - b.Property("TakeProfitTargets") - .HasColumnType("text"); - - b.Property("TechnicalRationale") - .IsRequired() - .HasColumnType("text"); - - b.Property("Timeframe") - .IsRequired() - .HasMaxLength(20) - .HasColumnType("character varying(20)"); - - b.Property("TradeId") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.Property("TtlMinutes") - .HasColumnType("integer"); - - b.Property("UserExitPrice") - .HasColumnType("decimal(18,4)"); - - b.Property("UserExitTimestamp") - .HasColumnType("timestamp with time zone"); - - b.Property("UserId") - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.Property("VixRegime") - .HasColumnType("integer"); - - b.Property("VixValue") - .HasColumnType("decimal(18,4)"); - - b.Property("WinRate") - .HasColumnType("double precision"); - - b.HasKey("Id"); - - b.HasIndex("AnalysisId"); - - b.HasIndex("CreatedAt"); - - b.HasIndex("EventId"); - - b.HasIndex("Isin"); - - b.HasIndex("Sector"); - - b.HasIndex("Status"); - - b.HasIndex("TradeId") - .IsUnique(); - - b.ToTable("trades"); - }); - - modelBuilder.Entity("FinlyticTrades.Entities.TradeHourlyUpdateEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("CurrentPrice") - .HasColumnType("decimal(18,4)"); - - b.Property("Reasoning") - .IsRequired() - .HasColumnType("text"); - - b.Property("Recommendation") - .IsRequired() - .HasMaxLength(30) - .HasColumnType("character varying(30)"); - - b.Property("SuggestedStopLoss") - .HasColumnType("decimal(18,4)"); - - b.Property("SuggestedTakeProfit") - .HasColumnType("decimal(18,4)"); - - b.Property("Timestamp") - .HasColumnType("timestamp with time zone"); - - b.Property("TradeId") - .HasColumnType("uuid"); - - b.Property("VixValue") - .HasColumnType("decimal(18,4)"); - - b.HasKey("Id"); - - b.HasIndex("Timestamp"); - - b.HasIndex("TradeId"); - - b.ToTable("trade_hourly_updates"); - }); - - modelBuilder.Entity("FinlyticTrades.Entities.TradesSettingsEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("AtrStopLossMultiplier") - .HasColumnType("double precision"); - - b.Property("MaxOpenPositions") - .HasColumnType("integer"); - - b.Property("RiskPerTradePercentage") - .HasColumnType("double precision"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.ToTable("Settings"); - }); - - modelBuilder.Entity("FinlyticTrades.Entities.TradeHourlyUpdateEntity", b => - { - b.HasOne("FinlyticTrades.Entities.TradeEntity", "Trade") - .WithMany("HourlyUpdates") - .HasForeignKey("TradeId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Trade"); - }); - - modelBuilder.Entity("FinlyticTrades.Entities.TradeEntity", b => - { - b.Navigation("HourlyUpdates"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/FinlyticTrades/Migrations/20260803170643_AddMultiUserTradeExecution.cs b/FinlyticTrades/Migrations/20260803170643_AddMultiUserTradeExecution.cs deleted file mode 100644 index 3a5d7bc..0000000 --- a/FinlyticTrades/Migrations/20260803170643_AddMultiUserTradeExecution.cs +++ /dev/null @@ -1,132 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace FinlyticTrades.Migrations -{ - /// - public partial class AddMultiUserTradeExecution : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "ActualEntryPrice", - table: "trades", - type: "numeric(18,4)", - nullable: true); - - migrationBuilder.AddColumn( - name: "EntryFee", - table: "trades", - type: "numeric(18,4)", - nullable: true); - - migrationBuilder.AddColumn( - name: "ExecutionTimestamp", - table: "trades", - type: "timestamp with time zone", - nullable: true); - - migrationBuilder.AddColumn( - name: "ExitFee", - table: "trades", - type: "numeric(18,4)", - nullable: true); - - migrationBuilder.AddColumn( - name: "IsGlobalProposal", - table: "trades", - type: "boolean", - nullable: false, - defaultValue: false); - - migrationBuilder.AddColumn( - name: "IsRecurring", - table: "trades", - type: "boolean", - nullable: false, - defaultValue: false); - - migrationBuilder.AddColumn( - name: "KnockoutThreshold", - table: "trades", - type: "numeric(18,4)", - nullable: true); - - migrationBuilder.AddColumn( - name: "LeverageUsed", - table: "trades", - type: "numeric(18,4)", - nullable: true); - - migrationBuilder.AddColumn( - name: "PositionSize", - table: "trades", - type: "numeric(18,4)", - nullable: true); - - migrationBuilder.AddColumn( - name: "Quantity", - table: "trades", - type: "numeric(18,4)", - nullable: true); - - migrationBuilder.AddColumn( - name: "UserId", - table: "trades", - type: "character varying(100)", - maxLength: 100, - nullable: true); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropColumn( - name: "ActualEntryPrice", - table: "trades"); - - migrationBuilder.DropColumn( - name: "EntryFee", - table: "trades"); - - migrationBuilder.DropColumn( - name: "ExecutionTimestamp", - table: "trades"); - - migrationBuilder.DropColumn( - name: "ExitFee", - table: "trades"); - - migrationBuilder.DropColumn( - name: "IsGlobalProposal", - table: "trades"); - - migrationBuilder.DropColumn( - name: "IsRecurring", - table: "trades"); - - migrationBuilder.DropColumn( - name: "KnockoutThreshold", - table: "trades"); - - migrationBuilder.DropColumn( - name: "LeverageUsed", - table: "trades"); - - migrationBuilder.DropColumn( - name: "PositionSize", - table: "trades"); - - migrationBuilder.DropColumn( - name: "Quantity", - table: "trades"); - - migrationBuilder.DropColumn( - name: "UserId", - table: "trades"); - } - } -} diff --git a/FinlyticTrades/Migrations/20260807210605_AddIndexToTradeHourlyUpdate.Designer.cs b/FinlyticTrades/Migrations/20260807210605_AddIndexToTradeHourlyUpdate.Designer.cs deleted file mode 100644 index 6aaf0ce..0000000 --- a/FinlyticTrades/Migrations/20260807210605_AddIndexToTradeHourlyUpdate.Designer.cs +++ /dev/null @@ -1,311 +0,0 @@ -// -using System; -using FinlyticTrades.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 FinlyticTrades.Migrations -{ - [DbContext(typeof(TradesDbContext))] - [Migration("20260807210605_AddIndexToTradeHourlyUpdate")] - partial class AddIndexToTradeHourlyUpdate - { - /// - 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("FinlyticTrades.Entities.TradeEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("ActualEntryPrice") - .HasColumnType("decimal(18,4)"); - - b.Property("AnalysisId") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.Property("CloseReason") - .HasMaxLength(50) - .HasColumnType("character varying(50)"); - - b.Property("ClosedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("CompanyName") - .IsRequired() - .HasMaxLength(150) - .HasColumnType("character varying(150)"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("EntryFee") - .HasColumnType("decimal(18,4)"); - - b.Property("EntryPrice") - .HasColumnType("decimal(18,4)"); - - b.Property("EntryZoneMax") - .HasColumnType("decimal(18,4)"); - - b.Property("EntryZoneMin") - .HasColumnType("decimal(18,4)"); - - b.Property("EventId") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.Property("ExecutionTimestamp") - .HasColumnType("timestamp with time zone"); - - b.Property("ExitFee") - .HasColumnType("decimal(18,4)"); - - b.Property("FundamentalRationale") - .IsRequired() - .HasColumnType("text"); - - b.Property("InstrumentType") - .IsRequired() - .HasMaxLength(30) - .HasColumnType("character varying(30)"); - - b.Property("IsGlobalProposal") - .HasColumnType("boolean"); - - b.Property("IsRecurring") - .HasColumnType("boolean"); - - b.Property("IsWin") - .HasColumnType("boolean"); - - b.Property("Isin") - .IsRequired() - .HasMaxLength(30) - .HasColumnType("character varying(30)"); - - b.Property("KnockoutThreshold") - .HasColumnType("decimal(18,4)"); - - b.Property("LeverageUsed") - .HasColumnType("decimal(18,4)"); - - b.Property("MaxLeverage") - .HasColumnType("decimal(18,4)"); - - b.Property("PnlAbsolute") - .HasColumnType("decimal(18,4)"); - - b.Property("PnlPercent") - .HasColumnType("decimal(18,4)"); - - b.Property("PositionSize") - .HasColumnType("decimal(18,4)"); - - b.Property("Quantity") - .HasColumnType("decimal(18,4)"); - - b.Property("Reasoning") - .IsRequired() - .HasColumnType("text"); - - b.Property("RiskRewardRatio") - .HasColumnType("decimal(18,4)"); - - b.Property("RiskTolerance") - .IsRequired() - .HasMaxLength(30) - .HasColumnType("character varying(30)"); - - b.Property("RiskWarning") - .IsRequired() - .HasColumnType("text"); - - b.Property("Sector") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("character varying(50)"); - - b.Property("SignalType") - .IsRequired() - .HasMaxLength(10) - .HasColumnType("character varying(10)"); - - b.Property("Status") - .HasColumnType("integer"); - - b.Property("StopLoss") - .HasColumnType("decimal(18,4)"); - - b.Property("Symbol") - .IsRequired() - .HasMaxLength(30) - .HasColumnType("character varying(30)"); - - b.Property("TakeProfit") - .HasColumnType("decimal(18,4)"); - - b.Property("TakeProfitTargets") - .HasColumnType("text"); - - b.Property("TechnicalRationale") - .IsRequired() - .HasColumnType("text"); - - b.Property("Timeframe") - .IsRequired() - .HasMaxLength(20) - .HasColumnType("character varying(20)"); - - b.Property("TradeId") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.Property("TtlMinutes") - .HasColumnType("integer"); - - b.Property("UserExitPrice") - .HasColumnType("decimal(18,4)"); - - b.Property("UserExitTimestamp") - .HasColumnType("timestamp with time zone"); - - b.Property("UserId") - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.Property("VixRegime") - .HasColumnType("integer"); - - b.Property("VixValue") - .HasColumnType("decimal(18,4)"); - - b.Property("WinRate") - .HasColumnType("double precision"); - - b.HasKey("Id"); - - b.HasIndex("AnalysisId"); - - b.HasIndex("CreatedAt"); - - b.HasIndex("EventId"); - - b.HasIndex("Isin"); - - b.HasIndex("Sector"); - - b.HasIndex("Status"); - - b.HasIndex("TradeId") - .IsUnique(); - - b.ToTable("trades"); - }); - - modelBuilder.Entity("FinlyticTrades.Entities.TradeHourlyUpdateEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("CurrentPrice") - .HasColumnType("decimal(18,4)"); - - b.Property("FloatingPnlPercent") - .HasColumnType("decimal(18,4)"); - - b.Property("Reasoning") - .IsRequired() - .HasColumnType("text"); - - b.Property("Recommendation") - .IsRequired() - .HasMaxLength(30) - .HasColumnType("character varying(30)"); - - b.Property("SuggestedStopLoss") - .HasColumnType("decimal(18,4)"); - - b.Property("SuggestedTakeProfit") - .HasColumnType("decimal(18,4)"); - - b.Property("Timestamp") - .HasColumnType("timestamp with time zone"); - - b.Property("TradeId") - .HasColumnType("uuid"); - - b.Property("VixValue") - .HasColumnType("decimal(18,4)"); - - b.HasKey("Id"); - - b.HasIndex("Timestamp"); - - b.HasIndex("TradeId"); - - b.HasIndex("TradeId", "Timestamp"); - - b.ToTable("trade_hourly_updates"); - }); - - modelBuilder.Entity("FinlyticTrades.Entities.TradesSettingsEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("AtrStopLossMultiplier") - .HasColumnType("double precision"); - - b.Property("MaxOpenPositions") - .HasColumnType("integer"); - - b.Property("RiskPerTradePercentage") - .HasColumnType("double precision"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.ToTable("Settings"); - }); - - modelBuilder.Entity("FinlyticTrades.Entities.TradeHourlyUpdateEntity", b => - { - b.HasOne("FinlyticTrades.Entities.TradeEntity", "Trade") - .WithMany("HourlyUpdates") - .HasForeignKey("TradeId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Trade"); - }); - - modelBuilder.Entity("FinlyticTrades.Entities.TradeEntity", b => - { - b.Navigation("HourlyUpdates"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/FinlyticTrades/Migrations/20260807210605_AddIndexToTradeHourlyUpdate.cs b/FinlyticTrades/Migrations/20260807210605_AddIndexToTradeHourlyUpdate.cs deleted file mode 100644 index 23d9713..0000000 --- a/FinlyticTrades/Migrations/20260807210605_AddIndexToTradeHourlyUpdate.cs +++ /dev/null @@ -1,37 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace FinlyticTrades.Migrations -{ - /// - public partial class AddIndexToTradeHourlyUpdate : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "FloatingPnlPercent", - table: "trade_hourly_updates", - type: "numeric(18,4)", - nullable: true); - - migrationBuilder.CreateIndex( - name: "IX_trade_hourly_updates_TradeId_Timestamp", - table: "trade_hourly_updates", - columns: new[] { "TradeId", "Timestamp" }); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropIndex( - name: "IX_trade_hourly_updates_TradeId_Timestamp", - table: "trade_hourly_updates"); - - migrationBuilder.DropColumn( - name: "FloatingPnlPercent", - table: "trade_hourly_updates"); - } - } -} diff --git a/FinlyticTrades/Migrations/20260812190325_DerivativeIsin.Designer.cs b/FinlyticTrades/Migrations/20260812190325_DerivativeIsin.Designer.cs deleted file mode 100644 index 1e4ea92..0000000 --- a/FinlyticTrades/Migrations/20260812190325_DerivativeIsin.Designer.cs +++ /dev/null @@ -1,315 +0,0 @@ -// -using System; -using FinlyticTrades.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 FinlyticTrades.Migrations -{ - [DbContext(typeof(TradesDbContext))] - [Migration("20260812190325_DerivativeIsin")] - partial class DerivativeIsin - { - /// - 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("FinlyticTrades.Entities.TradeEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("ActualEntryPrice") - .HasColumnType("decimal(18,4)"); - - b.Property("AnalysisId") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.Property("CloseReason") - .HasMaxLength(50) - .HasColumnType("character varying(50)"); - - b.Property("ClosedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("CompanyName") - .IsRequired() - .HasMaxLength(150) - .HasColumnType("character varying(150)"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("DerivativeIsin") - .HasMaxLength(20) - .HasColumnType("character varying(20)"); - - b.Property("EntryFee") - .HasColumnType("decimal(18,4)"); - - b.Property("EntryPrice") - .HasColumnType("decimal(18,4)"); - - b.Property("EntryZoneMax") - .HasColumnType("decimal(18,4)"); - - b.Property("EntryZoneMin") - .HasColumnType("decimal(18,4)"); - - b.Property("EventId") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.Property("ExecutionTimestamp") - .HasColumnType("timestamp with time zone"); - - b.Property("ExitFee") - .HasColumnType("decimal(18,4)"); - - b.Property("FundamentalRationale") - .IsRequired() - .HasColumnType("text"); - - b.Property("InstrumentType") - .IsRequired() - .HasMaxLength(30) - .HasColumnType("character varying(30)"); - - b.Property("IsGlobalProposal") - .HasColumnType("boolean"); - - b.Property("IsRecurring") - .HasColumnType("boolean"); - - b.Property("IsWin") - .HasColumnType("boolean"); - - b.Property("Isin") - .IsRequired() - .HasMaxLength(30) - .HasColumnType("character varying(30)"); - - b.Property("KnockoutThreshold") - .HasColumnType("decimal(18,4)"); - - b.Property("LeverageUsed") - .HasColumnType("decimal(18,4)"); - - b.Property("MaxLeverage") - .HasColumnType("decimal(18,4)"); - - b.Property("PnlAbsolute") - .HasColumnType("decimal(18,4)"); - - b.Property("PnlPercent") - .HasColumnType("decimal(18,4)"); - - b.Property("PositionSize") - .HasColumnType("decimal(18,4)"); - - b.Property("Quantity") - .HasColumnType("decimal(18,4)"); - - b.Property("Reasoning") - .IsRequired() - .HasColumnType("text"); - - b.Property("RiskRewardRatio") - .HasColumnType("decimal(18,4)"); - - b.Property("RiskTolerance") - .IsRequired() - .HasMaxLength(30) - .HasColumnType("character varying(30)"); - - b.Property("RiskWarning") - .IsRequired() - .HasColumnType("text"); - - b.Property("Sector") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("character varying(50)"); - - b.Property("SignalType") - .IsRequired() - .HasMaxLength(10) - .HasColumnType("character varying(10)"); - - b.Property("Status") - .HasColumnType("integer"); - - b.Property("StopLoss") - .HasColumnType("decimal(18,4)"); - - b.Property("Symbol") - .IsRequired() - .HasMaxLength(30) - .HasColumnType("character varying(30)"); - - b.Property("TakeProfit") - .HasColumnType("decimal(18,4)"); - - b.Property("TakeProfitTargets") - .HasColumnType("text"); - - b.Property("TechnicalRationale") - .IsRequired() - .HasColumnType("text"); - - b.Property("Timeframe") - .IsRequired() - .HasMaxLength(20) - .HasColumnType("character varying(20)"); - - b.Property("TradeId") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.Property("TtlMinutes") - .HasColumnType("integer"); - - b.Property("UserExitPrice") - .HasColumnType("decimal(18,4)"); - - b.Property("UserExitTimestamp") - .HasColumnType("timestamp with time zone"); - - b.Property("UserId") - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.Property("VixRegime") - .HasColumnType("integer"); - - b.Property("VixValue") - .HasColumnType("decimal(18,4)"); - - b.Property("WinRate") - .HasColumnType("double precision"); - - b.HasKey("Id"); - - b.HasIndex("AnalysisId"); - - b.HasIndex("CreatedAt"); - - b.HasIndex("EventId"); - - b.HasIndex("Isin"); - - b.HasIndex("Sector"); - - b.HasIndex("Status"); - - b.HasIndex("TradeId") - .IsUnique(); - - b.ToTable("trades"); - }); - - modelBuilder.Entity("FinlyticTrades.Entities.TradeHourlyUpdateEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("CurrentPrice") - .HasColumnType("decimal(18,4)"); - - b.Property("FloatingPnlPercent") - .HasColumnType("decimal(18,4)"); - - b.Property("Reasoning") - .IsRequired() - .HasColumnType("text"); - - b.Property("Recommendation") - .IsRequired() - .HasMaxLength(30) - .HasColumnType("character varying(30)"); - - b.Property("SuggestedStopLoss") - .HasColumnType("decimal(18,4)"); - - b.Property("SuggestedTakeProfit") - .HasColumnType("decimal(18,4)"); - - b.Property("Timestamp") - .HasColumnType("timestamp with time zone"); - - b.Property("TradeId") - .HasColumnType("uuid"); - - b.Property("VixValue") - .HasColumnType("decimal(18,4)"); - - b.HasKey("Id"); - - b.HasIndex("Timestamp"); - - b.HasIndex("TradeId"); - - b.HasIndex("TradeId", "Timestamp"); - - b.ToTable("trade_hourly_updates"); - }); - - modelBuilder.Entity("FinlyticTrades.Entities.TradesSettingsEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("AtrStopLossMultiplier") - .HasColumnType("double precision"); - - b.Property("MaxOpenPositions") - .HasColumnType("integer"); - - b.Property("RiskPerTradePercentage") - .HasColumnType("double precision"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.ToTable("Settings"); - }); - - modelBuilder.Entity("FinlyticTrades.Entities.TradeHourlyUpdateEntity", b => - { - b.HasOne("FinlyticTrades.Entities.TradeEntity", "Trade") - .WithMany("HourlyUpdates") - .HasForeignKey("TradeId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Trade"); - }); - - modelBuilder.Entity("FinlyticTrades.Entities.TradeEntity", b => - { - b.Navigation("HourlyUpdates"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/FinlyticTrades/Migrations/20260812190325_DerivativeIsin.cs b/FinlyticTrades/Migrations/20260812190325_DerivativeIsin.cs deleted file mode 100644 index 15f6bfd..0000000 --- a/FinlyticTrades/Migrations/20260812190325_DerivativeIsin.cs +++ /dev/null @@ -1,29 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace FinlyticTrades.Migrations -{ - /// - public partial class DerivativeIsin : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "DerivativeIsin", - table: "trades", - type: "character varying(20)", - maxLength: 20, - nullable: true); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropColumn( - name: "DerivativeIsin", - table: "trades"); - } - } -} diff --git a/FinlyticTrades/Migrations/20260813202535_CheckPendingTrades.Designer.cs b/FinlyticTrades/Migrations/20260813202535_CheckPendingTrades.Designer.cs deleted file mode 100644 index eefb2c6..0000000 --- a/FinlyticTrades/Migrations/20260813202535_CheckPendingTrades.Designer.cs +++ /dev/null @@ -1,315 +0,0 @@ -// -using System; -using FinlyticTrades.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 FinlyticTrades.Migrations -{ - [DbContext(typeof(TradesDbContext))] - [Migration("20260813202535_CheckPendingTrades")] - partial class CheckPendingTrades - { - /// - 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("FinlyticTrades.Entities.TradeEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("ActualEntryPrice") - .HasColumnType("decimal(18,4)"); - - b.Property("AnalysisId") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.Property("CloseReason") - .HasMaxLength(50) - .HasColumnType("character varying(50)"); - - b.Property("ClosedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("CompanyName") - .IsRequired() - .HasMaxLength(150) - .HasColumnType("character varying(150)"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("DerivativeIsin") - .HasMaxLength(20) - .HasColumnType("character varying(20)"); - - b.Property("EntryFee") - .HasColumnType("decimal(18,4)"); - - b.Property("EntryPrice") - .HasColumnType("decimal(18,4)"); - - b.Property("EntryZoneMax") - .HasColumnType("decimal(18,4)"); - - b.Property("EntryZoneMin") - .HasColumnType("decimal(18,4)"); - - b.Property("EventId") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.Property("ExecutionTimestamp") - .HasColumnType("timestamp with time zone"); - - b.Property("ExitFee") - .HasColumnType("decimal(18,4)"); - - b.Property("FundamentalRationale") - .IsRequired() - .HasColumnType("text"); - - b.Property("InstrumentType") - .IsRequired() - .HasMaxLength(30) - .HasColumnType("character varying(30)"); - - b.Property("IsGlobalProposal") - .HasColumnType("boolean"); - - b.Property("IsRecurring") - .HasColumnType("boolean"); - - b.Property("IsWin") - .HasColumnType("boolean"); - - b.Property("Isin") - .IsRequired() - .HasMaxLength(30) - .HasColumnType("character varying(30)"); - - b.Property("KnockoutThreshold") - .HasColumnType("decimal(18,4)"); - - b.Property("LeverageUsed") - .HasColumnType("decimal(18,4)"); - - b.Property("MaxLeverage") - .HasColumnType("decimal(18,4)"); - - b.Property("PnlAbsolute") - .HasColumnType("decimal(18,4)"); - - b.Property("PnlPercent") - .HasColumnType("decimal(18,4)"); - - b.Property("PositionSize") - .HasColumnType("decimal(18,4)"); - - b.Property("Quantity") - .HasColumnType("decimal(18,4)"); - - b.Property("Reasoning") - .IsRequired() - .HasColumnType("text"); - - b.Property("RiskRewardRatio") - .HasColumnType("decimal(18,4)"); - - b.Property("RiskTolerance") - .IsRequired() - .HasMaxLength(30) - .HasColumnType("character varying(30)"); - - b.Property("RiskWarning") - .IsRequired() - .HasColumnType("text"); - - b.Property("Sector") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("character varying(50)"); - - b.Property("SignalType") - .IsRequired() - .HasMaxLength(10) - .HasColumnType("character varying(10)"); - - b.Property("Status") - .HasColumnType("integer"); - - b.Property("StopLoss") - .HasColumnType("decimal(18,4)"); - - b.Property("Symbol") - .IsRequired() - .HasMaxLength(30) - .HasColumnType("character varying(30)"); - - b.Property("TakeProfit") - .HasColumnType("decimal(18,4)"); - - b.Property("TakeProfitTargets") - .HasColumnType("text"); - - b.Property("TechnicalRationale") - .IsRequired() - .HasColumnType("text"); - - b.Property("Timeframe") - .IsRequired() - .HasMaxLength(20) - .HasColumnType("character varying(20)"); - - b.Property("TradeId") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.Property("TtlMinutes") - .HasColumnType("integer"); - - b.Property("UserExitPrice") - .HasColumnType("decimal(18,4)"); - - b.Property("UserExitTimestamp") - .HasColumnType("timestamp with time zone"); - - b.Property("UserId") - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.Property("VixRegime") - .HasColumnType("integer"); - - b.Property("VixValue") - .HasColumnType("decimal(18,4)"); - - b.Property("WinRate") - .HasColumnType("double precision"); - - b.HasKey("Id"); - - b.HasIndex("AnalysisId"); - - b.HasIndex("CreatedAt"); - - b.HasIndex("EventId"); - - b.HasIndex("Isin"); - - b.HasIndex("Sector"); - - b.HasIndex("Status"); - - b.HasIndex("TradeId") - .IsUnique(); - - b.ToTable("trades"); - }); - - modelBuilder.Entity("FinlyticTrades.Entities.TradeHourlyUpdateEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("CurrentPrice") - .HasColumnType("decimal(18,4)"); - - b.Property("FloatingPnlPercent") - .HasColumnType("decimal(18,4)"); - - b.Property("Reasoning") - .IsRequired() - .HasColumnType("text"); - - b.Property("Recommendation") - .IsRequired() - .HasMaxLength(30) - .HasColumnType("character varying(30)"); - - b.Property("SuggestedStopLoss") - .HasColumnType("decimal(18,4)"); - - b.Property("SuggestedTakeProfit") - .HasColumnType("decimal(18,4)"); - - b.Property("Timestamp") - .HasColumnType("timestamp with time zone"); - - b.Property("TradeId") - .HasColumnType("uuid"); - - b.Property("VixValue") - .HasColumnType("decimal(18,4)"); - - b.HasKey("Id"); - - b.HasIndex("Timestamp"); - - b.HasIndex("TradeId"); - - b.HasIndex("TradeId", "Timestamp"); - - b.ToTable("trade_hourly_updates"); - }); - - modelBuilder.Entity("FinlyticTrades.Entities.TradesSettingsEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("AtrStopLossMultiplier") - .HasColumnType("double precision"); - - b.Property("MaxOpenPositions") - .HasColumnType("integer"); - - b.Property("RiskPerTradePercentage") - .HasColumnType("double precision"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.ToTable("Settings"); - }); - - modelBuilder.Entity("FinlyticTrades.Entities.TradeHourlyUpdateEntity", b => - { - b.HasOne("FinlyticTrades.Entities.TradeEntity", "Trade") - .WithMany("HourlyUpdates") - .HasForeignKey("TradeId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Trade"); - }); - - modelBuilder.Entity("FinlyticTrades.Entities.TradeEntity", b => - { - b.Navigation("HourlyUpdates"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/FinlyticTrades/Migrations/20260813202535_CheckPendingTrades.cs b/FinlyticTrades/Migrations/20260813202535_CheckPendingTrades.cs deleted file mode 100644 index 647ff7b..0000000 --- a/FinlyticTrades/Migrations/20260813202535_CheckPendingTrades.cs +++ /dev/null @@ -1,22 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace FinlyticTrades.Migrations -{ - /// - public partial class CheckPendingTrades : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - - } - } -} diff --git a/FinlyticTrades/Migrations/20260815100019_AddAssetTypeAndDerivativeCategoriesToTrades.Designer.cs b/FinlyticTrades/Migrations/20260815100019_AddAssetTypeAndDerivativeCategoriesToTrades.Designer.cs deleted file mode 100644 index 1b76773..0000000 --- a/FinlyticTrades/Migrations/20260815100019_AddAssetTypeAndDerivativeCategoriesToTrades.Designer.cs +++ /dev/null @@ -1,357 +0,0 @@ -// -using System; -using FinlyticTrades.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 FinlyticTrades.Migrations -{ - [DbContext(typeof(TradesDbContext))] - [Migration("20260815100019_AddAssetTypeAndDerivativeCategoriesToTrades")] - partial class AddAssetTypeAndDerivativeCategoriesToTrades - { - /// - 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("FinlyticCore.Entities.Settings.SettingEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Key") - .IsRequired() - .HasMaxLength(150) - .HasColumnType("character varying(150)"); - - b.Property("LastUpdatedUtc") - .HasColumnType("timestamp with time zone"); - - b.Property("ServiceIdentifier") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.Property("ValueJson") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("Id"); - - b.HasIndex("Key"); - - b.ToTable("DynamicSettings"); - }); - - modelBuilder.Entity("FinlyticTrades.Entities.TradeEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("ActualEntryPrice") - .HasColumnType("decimal(18,4)"); - - b.Property("AnalysisId") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.Property("AssetType") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("character varying(50)"); - - b.Property("CloseReason") - .HasMaxLength(50) - .HasColumnType("character varying(50)"); - - b.Property("ClosedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("CompanyName") - .IsRequired() - .HasMaxLength(150) - .HasColumnType("character varying(150)"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("DerivativeIsin") - .HasMaxLength(20) - .HasColumnType("character varying(20)"); - - b.Property("DerivativeProductCategories") - .IsRequired() - .HasColumnType("text"); - - b.Property("EntryFee") - .HasColumnType("decimal(18,4)"); - - b.Property("EntryPrice") - .HasColumnType("decimal(18,4)"); - - b.Property("EntryZoneMax") - .HasColumnType("decimal(18,4)"); - - b.Property("EntryZoneMin") - .HasColumnType("decimal(18,4)"); - - b.Property("EventId") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.Property("ExecutionTimestamp") - .HasColumnType("timestamp with time zone"); - - b.Property("ExitFee") - .HasColumnType("decimal(18,4)"); - - b.Property("FundamentalRationale") - .IsRequired() - .HasColumnType("text"); - - b.Property("HasCfd") - .HasColumnType("boolean"); - - b.Property("InstrumentType") - .IsRequired() - .HasMaxLength(30) - .HasColumnType("character varying(30)"); - - b.Property("IsGlobalProposal") - .HasColumnType("boolean"); - - b.Property("IsRecurring") - .HasColumnType("boolean"); - - b.Property("IsWin") - .HasColumnType("boolean"); - - b.Property("Isin") - .IsRequired() - .HasMaxLength(30) - .HasColumnType("character varying(30)"); - - b.Property("KnockoutThreshold") - .HasColumnType("decimal(18,4)"); - - b.Property("LeverageUsed") - .HasColumnType("decimal(18,4)"); - - b.Property("MaxLeverage") - .HasColumnType("decimal(18,4)"); - - b.Property("PnlAbsolute") - .HasColumnType("decimal(18,4)"); - - b.Property("PnlPercent") - .HasColumnType("decimal(18,4)"); - - b.Property("PositionSize") - .HasColumnType("decimal(18,4)"); - - b.Property("Quantity") - .HasColumnType("decimal(18,4)"); - - b.Property("Reasoning") - .IsRequired() - .HasColumnType("text"); - - b.Property("RiskRewardRatio") - .HasColumnType("decimal(18,4)"); - - b.Property("RiskTolerance") - .IsRequired() - .HasMaxLength(30) - .HasColumnType("character varying(30)"); - - b.Property("RiskWarning") - .IsRequired() - .HasColumnType("text"); - - b.Property("Sector") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("character varying(50)"); - - b.Property("SignalType") - .IsRequired() - .HasMaxLength(10) - .HasColumnType("character varying(10)"); - - b.Property("Status") - .HasColumnType("integer"); - - b.Property("StopLoss") - .HasColumnType("decimal(18,4)"); - - b.Property("Symbol") - .IsRequired() - .HasMaxLength(30) - .HasColumnType("character varying(30)"); - - b.Property("TakeProfit") - .HasColumnType("decimal(18,4)"); - - b.Property("TakeProfitTargets") - .HasColumnType("text"); - - b.Property("TechnicalRationale") - .IsRequired() - .HasColumnType("text"); - - b.Property("Timeframe") - .IsRequired() - .HasMaxLength(20) - .HasColumnType("character varying(20)"); - - b.Property("TradeId") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.Property("TtlMinutes") - .HasColumnType("integer"); - - b.Property("UserExitPrice") - .HasColumnType("decimal(18,4)"); - - b.Property("UserExitTimestamp") - .HasColumnType("timestamp with time zone"); - - b.Property("UserId") - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.Property("VixRegime") - .HasColumnType("integer"); - - b.Property("VixValue") - .HasColumnType("decimal(18,4)"); - - b.Property("WinRate") - .HasColumnType("double precision"); - - b.HasKey("Id"); - - b.HasIndex("AnalysisId"); - - b.HasIndex("CreatedAt"); - - b.HasIndex("EventId"); - - b.HasIndex("Isin"); - - b.HasIndex("Sector"); - - b.HasIndex("Status"); - - b.HasIndex("TradeId") - .IsUnique(); - - b.ToTable("trades"); - }); - - modelBuilder.Entity("FinlyticTrades.Entities.TradeHourlyUpdateEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("CurrentPrice") - .HasColumnType("decimal(18,4)"); - - b.Property("FloatingPnlPercent") - .HasColumnType("decimal(18,4)"); - - b.Property("Reasoning") - .IsRequired() - .HasColumnType("text"); - - b.Property("Recommendation") - .IsRequired() - .HasMaxLength(30) - .HasColumnType("character varying(30)"); - - b.Property("SuggestedStopLoss") - .HasColumnType("decimal(18,4)"); - - b.Property("SuggestedTakeProfit") - .HasColumnType("decimal(18,4)"); - - b.Property("Timestamp") - .HasColumnType("timestamp with time zone"); - - b.Property("TradeId") - .HasColumnType("uuid"); - - b.Property("VixValue") - .HasColumnType("decimal(18,4)"); - - b.HasKey("Id"); - - b.HasIndex("Timestamp"); - - b.HasIndex("TradeId"); - - b.HasIndex("TradeId", "Timestamp"); - - b.ToTable("trade_hourly_updates"); - }); - - modelBuilder.Entity("FinlyticTrades.Entities.TradesSettingsEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("AtrStopLossMultiplier") - .HasColumnType("double precision"); - - b.Property("MaxOpenPositions") - .HasColumnType("integer"); - - b.Property("RiskPerTradePercentage") - .HasColumnType("double precision"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.ToTable("Settings"); - }); - - modelBuilder.Entity("FinlyticTrades.Entities.TradeHourlyUpdateEntity", b => - { - b.HasOne("FinlyticTrades.Entities.TradeEntity", "Trade") - .WithMany("HourlyUpdates") - .HasForeignKey("TradeId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Trade"); - }); - - modelBuilder.Entity("FinlyticTrades.Entities.TradeEntity", b => - { - b.Navigation("HourlyUpdates"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/FinlyticTrades/Migrations/20260815100019_AddAssetTypeAndDerivativeCategoriesToTrades.cs b/FinlyticTrades/Migrations/20260815100019_AddAssetTypeAndDerivativeCategoriesToTrades.cs deleted file mode 100644 index 1a63d02..0000000 --- a/FinlyticTrades/Migrations/20260815100019_AddAssetTypeAndDerivativeCategoriesToTrades.cs +++ /dev/null @@ -1,76 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace FinlyticTrades.Migrations -{ - /// - public partial class AddAssetTypeAndDerivativeCategoriesToTrades : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "AssetType", - table: "trades", - type: "character varying(50)", - maxLength: 50, - nullable: false, - defaultValue: ""); - - migrationBuilder.AddColumn( - name: "DerivativeProductCategories", - table: "trades", - type: "text", - nullable: false, - defaultValue: ""); - - migrationBuilder.AddColumn( - name: "HasCfd", - table: "trades", - type: "boolean", - nullable: false, - defaultValue: false); - - migrationBuilder.CreateTable( - name: "DynamicSettings", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - Key = table.Column(type: "character varying(150)", maxLength: 150, nullable: false), - ValueJson = table.Column(type: "text", nullable: false), - ServiceIdentifier = table.Column(type: "character varying(100)", maxLength: 100, nullable: false), - LastUpdatedUtc = table.Column(type: "timestamp with time zone", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_DynamicSettings", x => x.Id); - }); - - migrationBuilder.CreateIndex( - name: "IX_DynamicSettings_Key", - table: "DynamicSettings", - column: "Key"); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "DynamicSettings"); - - migrationBuilder.DropColumn( - name: "AssetType", - table: "trades"); - - migrationBuilder.DropColumn( - name: "DerivativeProductCategories", - table: "trades"); - - migrationBuilder.DropColumn( - name: "HasCfd", - table: "trades"); - } - } -} diff --git a/FinlyticTrades/Migrations/20260815184034_AddDynamicSettings.Designer.cs b/FinlyticTrades/Migrations/20260815184034_AddDynamicSettings.Designer.cs deleted file mode 100644 index 9fdfd2b..0000000 --- a/FinlyticTrades/Migrations/20260815184034_AddDynamicSettings.Designer.cs +++ /dev/null @@ -1,358 +0,0 @@ -// -using System; -using FinlyticTrades.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 FinlyticTrades.Migrations -{ - [DbContext(typeof(TradesDbContext))] - [Migration("20260815184034_AddDynamicSettings")] - partial class AddDynamicSettings - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "10.0.9") - .HasAnnotation("Relational:MaxIdentifierLength", 63); - - NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - - modelBuilder.Entity("FinlyticCore.Entities.Settings.SettingEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Key") - .IsRequired() - .HasMaxLength(150) - .HasColumnType("character varying(150)"); - - b.Property("LastUpdatedUtc") - .HasColumnType("timestamp with time zone"); - - b.Property("ServiceIdentifier") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.Property("ValueJson") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("Id"); - - b.HasIndex("Key") - .IsUnique(); - - b.ToTable("DynamicSettings"); - }); - - modelBuilder.Entity("FinlyticTrades.Entities.TradeEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("ActualEntryPrice") - .HasColumnType("decimal(18,4)"); - - b.Property("AnalysisId") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.Property("AssetType") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("character varying(50)"); - - b.Property("CloseReason") - .HasMaxLength(50) - .HasColumnType("character varying(50)"); - - b.Property("ClosedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("CompanyName") - .IsRequired() - .HasMaxLength(150) - .HasColumnType("character varying(150)"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("DerivativeIsin") - .HasMaxLength(20) - .HasColumnType("character varying(20)"); - - b.Property("DerivativeProductCategories") - .IsRequired() - .HasColumnType("text"); - - b.Property("EntryFee") - .HasColumnType("decimal(18,4)"); - - b.Property("EntryPrice") - .HasColumnType("decimal(18,4)"); - - b.Property("EntryZoneMax") - .HasColumnType("decimal(18,4)"); - - b.Property("EntryZoneMin") - .HasColumnType("decimal(18,4)"); - - b.Property("EventId") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.Property("ExecutionTimestamp") - .HasColumnType("timestamp with time zone"); - - b.Property("ExitFee") - .HasColumnType("decimal(18,4)"); - - b.Property("FundamentalRationale") - .IsRequired() - .HasColumnType("text"); - - b.Property("HasCfd") - .HasColumnType("boolean"); - - b.Property("InstrumentType") - .IsRequired() - .HasMaxLength(30) - .HasColumnType("character varying(30)"); - - b.Property("IsGlobalProposal") - .HasColumnType("boolean"); - - b.Property("IsRecurring") - .HasColumnType("boolean"); - - b.Property("IsWin") - .HasColumnType("boolean"); - - b.Property("Isin") - .IsRequired() - .HasMaxLength(30) - .HasColumnType("character varying(30)"); - - b.Property("KnockoutThreshold") - .HasColumnType("decimal(18,4)"); - - b.Property("LeverageUsed") - .HasColumnType("decimal(18,4)"); - - b.Property("MaxLeverage") - .HasColumnType("decimal(18,4)"); - - b.Property("PnlAbsolute") - .HasColumnType("decimal(18,4)"); - - b.Property("PnlPercent") - .HasColumnType("decimal(18,4)"); - - b.Property("PositionSize") - .HasColumnType("decimal(18,4)"); - - b.Property("Quantity") - .HasColumnType("decimal(18,4)"); - - b.Property("Reasoning") - .IsRequired() - .HasColumnType("text"); - - b.Property("RiskRewardRatio") - .HasColumnType("decimal(18,4)"); - - b.Property("RiskTolerance") - .IsRequired() - .HasMaxLength(30) - .HasColumnType("character varying(30)"); - - b.Property("RiskWarning") - .IsRequired() - .HasColumnType("text"); - - b.Property("Sector") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("character varying(50)"); - - b.Property("SignalType") - .IsRequired() - .HasMaxLength(10) - .HasColumnType("character varying(10)"); - - b.Property("Status") - .HasColumnType("integer"); - - b.Property("StopLoss") - .HasColumnType("decimal(18,4)"); - - b.Property("Symbol") - .IsRequired() - .HasMaxLength(30) - .HasColumnType("character varying(30)"); - - b.Property("TakeProfit") - .HasColumnType("decimal(18,4)"); - - b.Property("TakeProfitTargets") - .HasColumnType("text"); - - b.Property("TechnicalRationale") - .IsRequired() - .HasColumnType("text"); - - b.Property("Timeframe") - .IsRequired() - .HasMaxLength(20) - .HasColumnType("character varying(20)"); - - b.Property("TradeId") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.Property("TtlMinutes") - .HasColumnType("integer"); - - b.Property("UserExitPrice") - .HasColumnType("decimal(18,4)"); - - b.Property("UserExitTimestamp") - .HasColumnType("timestamp with time zone"); - - b.Property("UserId") - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.Property("VixRegime") - .HasColumnType("integer"); - - b.Property("VixValue") - .HasColumnType("decimal(18,4)"); - - b.Property("WinRate") - .HasColumnType("double precision"); - - b.HasKey("Id"); - - b.HasIndex("AnalysisId"); - - b.HasIndex("CreatedAt"); - - b.HasIndex("EventId"); - - b.HasIndex("Isin"); - - b.HasIndex("Sector"); - - b.HasIndex("Status"); - - b.HasIndex("TradeId") - .IsUnique(); - - b.ToTable("trades"); - }); - - modelBuilder.Entity("FinlyticTrades.Entities.TradeHourlyUpdateEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("CurrentPrice") - .HasColumnType("decimal(18,4)"); - - b.Property("FloatingPnlPercent") - .HasColumnType("decimal(18,4)"); - - b.Property("Reasoning") - .IsRequired() - .HasColumnType("text"); - - b.Property("Recommendation") - .IsRequired() - .HasMaxLength(30) - .HasColumnType("character varying(30)"); - - b.Property("SuggestedStopLoss") - .HasColumnType("decimal(18,4)"); - - b.Property("SuggestedTakeProfit") - .HasColumnType("decimal(18,4)"); - - b.Property("Timestamp") - .HasColumnType("timestamp with time zone"); - - b.Property("TradeId") - .HasColumnType("uuid"); - - b.Property("VixValue") - .HasColumnType("decimal(18,4)"); - - b.HasKey("Id"); - - b.HasIndex("Timestamp"); - - b.HasIndex("TradeId"); - - b.HasIndex("TradeId", "Timestamp"); - - b.ToTable("trade_hourly_updates"); - }); - - modelBuilder.Entity("FinlyticTrades.Entities.TradesSettingsEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("AtrStopLossMultiplier") - .HasColumnType("double precision"); - - b.Property("MaxOpenPositions") - .HasColumnType("integer"); - - b.Property("RiskPerTradePercentage") - .HasColumnType("double precision"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.ToTable("Settings"); - }); - - modelBuilder.Entity("FinlyticTrades.Entities.TradeHourlyUpdateEntity", b => - { - b.HasOne("FinlyticTrades.Entities.TradeEntity", "Trade") - .WithMany("HourlyUpdates") - .HasForeignKey("TradeId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Trade"); - }); - - modelBuilder.Entity("FinlyticTrades.Entities.TradeEntity", b => - { - b.Navigation("HourlyUpdates"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/FinlyticTrades/Migrations/20260815184034_AddDynamicSettings.cs b/FinlyticTrades/Migrations/20260815184034_AddDynamicSettings.cs deleted file mode 100644 index 0f3b142..0000000 --- a/FinlyticTrades/Migrations/20260815184034_AddDynamicSettings.cs +++ /dev/null @@ -1,37 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace FinlyticTrades.Migrations -{ - /// - public partial class AddDynamicSettings : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropIndex( - name: "IX_DynamicSettings_Key", - table: "DynamicSettings"); - - migrationBuilder.CreateIndex( - name: "IX_DynamicSettings_Key", - table: "DynamicSettings", - column: "Key", - unique: true); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropIndex( - name: "IX_DynamicSettings_Key", - table: "DynamicSettings"); - - migrationBuilder.CreateIndex( - name: "IX_DynamicSettings_Key", - table: "DynamicSettings", - column: "Key"); - } - } -} diff --git a/FinlyticTrades/Migrations/TradesDbContextModelSnapshot.cs b/FinlyticTrades/Migrations/TradesDbContextModelSnapshot.cs deleted file mode 100644 index 2c9b43e..0000000 --- a/FinlyticTrades/Migrations/TradesDbContextModelSnapshot.cs +++ /dev/null @@ -1,355 +0,0 @@ -// -using System; -using FinlyticTrades.Database; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; - -#nullable disable - -namespace FinlyticTrades.Migrations -{ - [DbContext(typeof(TradesDbContext))] - partial class TradesDbContextModelSnapshot : 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("FinlyticCore.Entities.Settings.SettingEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Key") - .IsRequired() - .HasMaxLength(150) - .HasColumnType("character varying(150)"); - - b.Property("LastUpdatedUtc") - .HasColumnType("timestamp with time zone"); - - b.Property("ServiceIdentifier") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.Property("ValueJson") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("Id"); - - b.HasIndex("Key") - .IsUnique(); - - b.ToTable("DynamicSettings"); - }); - - modelBuilder.Entity("FinlyticTrades.Entities.TradeEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("ActualEntryPrice") - .HasColumnType("decimal(18,4)"); - - b.Property("AnalysisId") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.Property("AssetType") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("character varying(50)"); - - b.Property("CloseReason") - .HasMaxLength(50) - .HasColumnType("character varying(50)"); - - b.Property("ClosedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("CompanyName") - .IsRequired() - .HasMaxLength(150) - .HasColumnType("character varying(150)"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("DerivativeIsin") - .HasMaxLength(20) - .HasColumnType("character varying(20)"); - - b.Property("DerivativeProductCategories") - .IsRequired() - .HasColumnType("text"); - - b.Property("EntryFee") - .HasColumnType("decimal(18,4)"); - - b.Property("EntryPrice") - .HasColumnType("decimal(18,4)"); - - b.Property("EntryZoneMax") - .HasColumnType("decimal(18,4)"); - - b.Property("EntryZoneMin") - .HasColumnType("decimal(18,4)"); - - b.Property("EventId") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.Property("ExecutionTimestamp") - .HasColumnType("timestamp with time zone"); - - b.Property("ExitFee") - .HasColumnType("decimal(18,4)"); - - b.Property("FundamentalRationale") - .IsRequired() - .HasColumnType("text"); - - b.Property("HasCfd") - .HasColumnType("boolean"); - - b.Property("InstrumentType") - .IsRequired() - .HasMaxLength(30) - .HasColumnType("character varying(30)"); - - b.Property("IsGlobalProposal") - .HasColumnType("boolean"); - - b.Property("IsRecurring") - .HasColumnType("boolean"); - - b.Property("IsWin") - .HasColumnType("boolean"); - - b.Property("Isin") - .IsRequired() - .HasMaxLength(30) - .HasColumnType("character varying(30)"); - - b.Property("KnockoutThreshold") - .HasColumnType("decimal(18,4)"); - - b.Property("LeverageUsed") - .HasColumnType("decimal(18,4)"); - - b.Property("MaxLeverage") - .HasColumnType("decimal(18,4)"); - - b.Property("PnlAbsolute") - .HasColumnType("decimal(18,4)"); - - b.Property("PnlPercent") - .HasColumnType("decimal(18,4)"); - - b.Property("PositionSize") - .HasColumnType("decimal(18,4)"); - - b.Property("Quantity") - .HasColumnType("decimal(18,4)"); - - b.Property("Reasoning") - .IsRequired() - .HasColumnType("text"); - - b.Property("RiskRewardRatio") - .HasColumnType("decimal(18,4)"); - - b.Property("RiskTolerance") - .IsRequired() - .HasMaxLength(30) - .HasColumnType("character varying(30)"); - - b.Property("RiskWarning") - .IsRequired() - .HasColumnType("text"); - - b.Property("Sector") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("character varying(50)"); - - b.Property("SignalType") - .IsRequired() - .HasMaxLength(10) - .HasColumnType("character varying(10)"); - - b.Property("Status") - .HasColumnType("integer"); - - b.Property("StopLoss") - .HasColumnType("decimal(18,4)"); - - b.Property("Symbol") - .IsRequired() - .HasMaxLength(30) - .HasColumnType("character varying(30)"); - - b.Property("TakeProfit") - .HasColumnType("decimal(18,4)"); - - b.Property("TakeProfitTargets") - .HasColumnType("text"); - - b.Property("TechnicalRationale") - .IsRequired() - .HasColumnType("text"); - - b.Property("Timeframe") - .IsRequired() - .HasMaxLength(20) - .HasColumnType("character varying(20)"); - - b.Property("TradeId") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.Property("TtlMinutes") - .HasColumnType("integer"); - - b.Property("UserExitPrice") - .HasColumnType("decimal(18,4)"); - - b.Property("UserExitTimestamp") - .HasColumnType("timestamp with time zone"); - - b.Property("UserId") - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.Property("VixRegime") - .HasColumnType("integer"); - - b.Property("VixValue") - .HasColumnType("decimal(18,4)"); - - b.Property("WinRate") - .HasColumnType("double precision"); - - b.HasKey("Id"); - - b.HasIndex("AnalysisId"); - - b.HasIndex("CreatedAt"); - - b.HasIndex("EventId"); - - b.HasIndex("Isin"); - - b.HasIndex("Sector"); - - b.HasIndex("Status"); - - b.HasIndex("TradeId") - .IsUnique(); - - b.ToTable("trades"); - }); - - modelBuilder.Entity("FinlyticTrades.Entities.TradeHourlyUpdateEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("CurrentPrice") - .HasColumnType("decimal(18,4)"); - - b.Property("FloatingPnlPercent") - .HasColumnType("decimal(18,4)"); - - b.Property("Reasoning") - .IsRequired() - .HasColumnType("text"); - - b.Property("Recommendation") - .IsRequired() - .HasMaxLength(30) - .HasColumnType("character varying(30)"); - - b.Property("SuggestedStopLoss") - .HasColumnType("decimal(18,4)"); - - b.Property("SuggestedTakeProfit") - .HasColumnType("decimal(18,4)"); - - b.Property("Timestamp") - .HasColumnType("timestamp with time zone"); - - b.Property("TradeId") - .HasColumnType("uuid"); - - b.Property("VixValue") - .HasColumnType("decimal(18,4)"); - - b.HasKey("Id"); - - b.HasIndex("Timestamp"); - - b.HasIndex("TradeId"); - - b.HasIndex("TradeId", "Timestamp"); - - b.ToTable("trade_hourly_updates"); - }); - - modelBuilder.Entity("FinlyticTrades.Entities.TradesSettingsEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("AtrStopLossMultiplier") - .HasColumnType("double precision"); - - b.Property("MaxOpenPositions") - .HasColumnType("integer"); - - b.Property("RiskPerTradePercentage") - .HasColumnType("double precision"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.ToTable("Settings"); - }); - - modelBuilder.Entity("FinlyticTrades.Entities.TradeHourlyUpdateEntity", b => - { - b.HasOne("FinlyticTrades.Entities.TradeEntity", "Trade") - .WithMany("HourlyUpdates") - .HasForeignKey("TradeId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Trade"); - }); - - modelBuilder.Entity("FinlyticTrades.Entities.TradeEntity", b => - { - b.Navigation("HourlyUpdates"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/FinlyticTrades/Program.cs b/FinlyticTrades/Program.cs deleted file mode 100644 index eaf8c2a..0000000 --- a/FinlyticTrades/Program.cs +++ /dev/null @@ -1,49 +0,0 @@ -using System; -using FinlyticCore.Database; -using FinlyticCore.Services; -using FinlyticTrades.Database; -using FinlyticTrades.Services; -using FinlyticTrades.Util; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; - -var builder = Host.CreateApplicationBuilder(args); - -// 1. Standard DbContext (Scoped) -builder.Services.AddDbContext(options => - options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection"))); -builder.Services.AddScoped(sp => sp.GetRequiredService()); - -// 2. Core Services -builder.Services.AddSingleton(); -builder.Services.AddSingleton(typeof(IFinlyticLogger<>), typeof(FinlyticLogger<>)); - -// 3. Domain Services (Scoped) -builder.Services.AddScoped(); -builder.Services.AddScoped(); - -// 4. Hosted Services / Singletons -builder.Services.AddSingleton(); -builder.Services.AddHostedService(sp => sp.GetRequiredService()); -builder.Services.AddHostedService(); - -var host = builder.Build(); - -// DB Migrations ausführen -using (var scope = host.Services.CreateScope()) -{ - try - { - var context = scope.ServiceProvider.GetRequiredService(); - await context.Database.MigrateAsync(); - Console.WriteLine("Database migrations successfully executed for FinlyticTrades."); - } - catch (Exception ex) - { - Console.WriteLine($"Critical error during database migration for FinlyticTrades: {ex.Message}"); - } -} - -await host.RunAsync(); \ No newline at end of file diff --git a/FinlyticTrades/Project.md b/FinlyticTrades/Project.md deleted file mode 100644 index a422a15..0000000 --- a/FinlyticTrades/Project.md +++ /dev/null @@ -1,36 +0,0 @@ -# Finlytic Trades Service - -Finlytic Trades is a C# microservice managing the full lifecycle of automated trade signals and positions. It handles proposed trade validation, position tracking, TTL expiration, hourly performance updates, and trade closure. - ---- - -## Core Modules & Architecture - -1. **Trade Lifecycle Engine (`ITradeLifecycleService`)**: - - Ingests trade proposals (`TradeProposalDto`), validates parameters (Entry, Stop Loss, Take Profit, Win Rate, Risk Tolerance), and tracks positions through `Active`, `Closed`, `Expired`, or `Cancelled` states. - -2. **TTL Worker Service (`TtlWorkerService`)**: - - Periodically checks active trades against Time-To-Live (`TtlMinutes`) constraints and automatically expires stale trades. - -3. **Feedback Exporter Engine (`FeedbackExporterEngine`)**: - - Exports trade outcome data (`TradeFeedbackRecord`) for AI model retraining and win-rate calibration. - -4. **MQTT RPC & Event Communication**: - - Subscribes to `finlytic/trades/proposed/#` and `finlytic/trades/updates/#`. - - Handles RPC requests on `finlytic/trades/get_active/request` and `finlytic/trades/close/request/#`. - - Publishes position updates to `finlytic/trades/update` and `finlytic/trades/get_active/response`. - ---- - -## Feature Status - -### Implemented Features -- [x] Full Trade Lifecycle Management (`TradesDbContext` with PostgreSQL indexes). -- [x] Automated TTL Expiration Worker (`TtlWorkerService`). -- [x] AI Feedback Record Exporter (`FeedbackExporterEngine`). -- [x] Pure Worker Service Architecture (`Host.CreateApplicationBuilder`, Kestrel HTTP server removed). -- [x] Zero-Allocation MQTT RPC handlers for active trades & trade closure. - -### Planned Features -- [ ] Automated Trailing Stop Loss adjustment engine based on ATR (Average True Range). -- [ ] Direct Broker API Execution integration (Trade Republic / Interactive Brokers automated order placement). diff --git a/FinlyticTrades/Services/FeedbackExporterEngine.cs b/FinlyticTrades/Services/FeedbackExporterEngine.cs deleted file mode 100644 index 1201cfe..0000000 --- a/FinlyticTrades/Services/FeedbackExporterEngine.cs +++ /dev/null @@ -1,206 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text.Json; -using System.Text.RegularExpressions; -using System.Threading; -using System.Threading.Tasks; -using FinlyticCore.Models.Trades; -using FinlyticCore.Services; -using FinlyticTrades.Database; -using FinlyticTrades.Entities; -using FinlyticTrades.Util; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using Parquet.Serialization; - -namespace FinlyticTrades.Services; - -public interface IFeedbackExporterEngine -{ - /// - /// Exports feedback data for closed trades. - /// - Task ExportFeedbackDataAsync(CancellationToken cancellationToken = default); -} - -public class FeedbackExporterEngine : BackgroundService, IFeedbackExporterEngine -{ - private readonly IServiceScopeFactory _scopeFactory; - private readonly IFinlyticLogger _finlyticLogger; - private readonly string _feedbackDir; - - public FeedbackExporterEngine(IServiceScopeFactory scopeFactory, IFinlyticLogger finlyticLogger) - { - _scopeFactory = scopeFactory; - _finlyticLogger = finlyticLogger; - _feedbackDir = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "data", "feedback"); - - if (!Directory.Exists(_feedbackDir)) - { - Directory.CreateDirectory(_feedbackDir); - } - } - - protected override async Task ExecuteAsync(CancellationToken stoppingToken) - { - await _finlyticLogger.LogInfoAsync(SettingKeys.TradesChannel, "[FeedbackExporterEngine] Feedback Exporter Engine background service started."); - - try - { - await Task.Delay(TimeSpan.FromSeconds(30), stoppingToken); - } - catch (OperationCanceledException) - { - return; - } - - while (!stoppingToken.IsCancellationRequested) - { - try - { - await ExportFeedbackDataAsync(stoppingToken); - } - catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested) - { - break; - } - catch (Exception ex) - { - await _finlyticLogger.LogErrorAsync(SettingKeys.TradesChannel, ex, "[FeedbackExporterEngine] Error executing feedback exporter job."); - } - - try - { - await Task.Delay(TimeSpan.FromHours(6), stoppingToken); - } - catch (OperationCanceledException) - { - break; - } - } - - await _finlyticLogger.LogInfoAsync(SettingKeys.TradesChannel, "[FeedbackExporterEngine] Feedback Exporter Engine background service stopped."); - } - - /// - /// Exports feedback data for closed trades into sector-based JSON and Parquet formats. - /// Uses atomic file-writes to avoid thread-lock conflicts with reader processes. - /// - public async Task ExportFeedbackDataAsync(CancellationToken cancellationToken = default) - { - using var scope = _scopeFactory.CreateScope(); - var dbContext = scope.ServiceProvider.GetRequiredService(); - - var closedTrades = await dbContext.Trades - .AsNoTracking() - .Where(t => t.Status == TradeStatus.Closed && t.UserExitPrice.HasValue) - .ToListAsync(cancellationToken); - - if (closedTrades.Count == 0) - { - await _finlyticLogger.LogInfoAsync(SettingKeys.TradesChannel, "[FeedbackExporterEngine] No closed trades available for export."); - return; - } - - var groups = closedTrades.GroupBy(t => SanitizeSectorName(t.Sector)); - - foreach (var group in groups) - { - if (cancellationToken.IsCancellationRequested) break; - - var sectorName = group.Key; - var sectorDir = Path.Combine(_feedbackDir, sectorName); - - if (!Directory.Exists(sectorDir)) - { - Directory.CreateDirectory(sectorDir); - } - - var feedbackRecords = new List(); - - foreach (var t in group) - { - var startTime = t.ExecutionTimestamp ?? t.CreatedAt; - var endTime = t.UserExitTimestamp ?? t.ClosedAt ?? DateTime.UtcNow; - double reactionDelay = Math.Max(0, (endTime - startTime).TotalMinutes); - - decimal exitPrice = t.UserExitPrice ?? t.EntryPrice; - - decimal entryPrice = t.ActualEntryPrice.HasValue && t.ActualEntryPrice.Value > 0 - ? t.ActualEntryPrice.Value - : t.EntryPrice; - - decimal slippagePct = t.EntryPrice > 0 - ? Math.Abs((entryPrice - t.EntryPrice) / t.EntryPrice) * 100.0m - : 0m; - - var rec = new TradeFeedbackRecord - { - TradeId = t.TradeId, - AnalysisId = t.AnalysisId, - Sector = t.Sector, - Symbol = t.Symbol, - Isin = t.Isin, - EntryPrice = entryPrice, - StopLoss = t.StopLoss, - TakeProfit = t.TakeProfit, - UserExitPrice = exitPrice, - PnlAbsolute = t.PnlAbsolute ?? 0m, - PnlPercent = t.PnlPercent ?? 0m, - IsWin = t.IsWin ?? false, - CloseReason = t.CloseReason ?? "Unknown", - VixRegime = t.VixRegime, - VixValue = t.VixValue, - ReactionDelayMinutes = Math.Round(reactionDelay, 2), - SlippagePercent = Math.Round(slippagePct, 2), - CreatedAt = t.CreatedAt, - ClosedAt = endTime - }; - - feedbackRecords.Add(rec); - } - - // 1. Atomic JSON Export (.tmp -> move) - string jsonPath = Path.Combine(sectorDir, $"{sectorName}_feedback.json"); - string jsonTmpPath = Path.Combine(sectorDir, $"{sectorName}_feedback.json.tmp"); - string jsonContent = JsonSerializer.Serialize(feedbackRecords, new JsonSerializerOptions { WriteIndented = true }); - - await File.WriteAllTextAsync(jsonTmpPath, jsonContent, cancellationToken); - File.Move(jsonTmpPath, jsonPath, overwrite: true); - - // 2. Atomic Parquet Export (.tmp -> move) - try - { - string parquetPath = Path.Combine(sectorDir, $"{sectorName}_feedback.parquet"); - string parquetTmpPath = Path.Combine(sectorDir, $"{sectorName}_feedback.parquet.tmp"); - - await using (var fileStream = new FileStream(parquetTmpPath, FileMode.Create, FileAccess.Write, FileShare.None, 4096, useAsync: true)) - { - await ParquetSerializer.SerializeAsync(feedbackRecords, fileStream, cancellationToken: cancellationToken); - } - - File.Move(parquetTmpPath, parquetPath, overwrite: true); - - await _finlyticLogger.LogInfoAsync(SettingKeys.TradesChannel, "[FeedbackExporterEngine] Exported Parquet feedback file for sector '{Sector}' to {ParquetPath}", sectorName, parquetPath); - } - catch (Exception ex) - { - await _finlyticLogger.LogWarningAsync(SettingKeys.TradesChannel, ex, "[FeedbackExporterEngine] Failed to write Parquet file for sector '{Sector}'. JSON file was written successfully.", sectorName); - } - } - - await _finlyticLogger.LogInfoAsync(SettingKeys.TradesChannel, "[FeedbackExporterEngine] Successfully exported feedback data for {Count} closed trades across {Sectors} sectors.", - closedTrades.Count, groups.Count()); - } - - private static string SanitizeSectorName(string? sector) - { - if (string.IsNullOrWhiteSpace(sector)) return "general"; - - var clean = Regex.Replace(sector.Trim().ToLowerInvariant(), @"[^a-z0-9_\-]", "_"); - return string.IsNullOrWhiteSpace(clean) ? "general" : clean; - } -} \ No newline at end of file diff --git a/FinlyticTrades/Services/SettingsDbService.cs b/FinlyticTrades/Services/SettingsDbService.cs deleted file mode 100644 index 6293dba..0000000 --- a/FinlyticTrades/Services/SettingsDbService.cs +++ /dev/null @@ -1,94 +0,0 @@ -using FinlyticTrades.Database; -using FinlyticTrades.Entities; -using Microsoft.EntityFrameworkCore; - -namespace FinlyticTrades.Services; - -public interface ISettingsDbService -{ - /// - /// Gets the current settings. - /// - Task GetSettingsAsync(); - /// - /// Saves the provided settings. - /// - Task SaveSettingsAsync(TradesSettingsEntity settings); - /// - /// Updates settings from a dictionary of key-value pairs. - /// - Task UpdateSettingsFromDictionaryAsync(Dictionary dictionary); -} - -public class SettingsDbService : ISettingsDbService -{ - private readonly TradesDbContext _context; - - /// - /// Initializes a new instance of the SettingsDbService class. - /// - public SettingsDbService(TradesDbContext context) - { - _context = context; - } - - /// - /// Gets the current settings. - /// - public async Task GetSettingsAsync() - { - var settings = await _context.Settings.AsNoTracking().FirstOrDefaultAsync(); - if (settings == null) - { - settings = new TradesSettingsEntity { Id = Guid.NewGuid() }; - _context.Settings.Add(settings); - await _context.SaveChangesAsync(); - _context.ChangeTracker.Clear(); - } - return settings; - } - - /// - /// Saves the provided settings. - /// - public async Task SaveSettingsAsync(TradesSettingsEntity 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.AtrStopLossMultiplier = settings.AtrStopLossMultiplier; - existing.RiskPerTradePercentage = settings.RiskPerTradePercentage; - existing.MaxOpenPositions = settings.MaxOpenPositions; - existing.UpdatedAt = settings.UpdatedAt; - _context.Settings.Update(existing); - } - await _context.SaveChangesAsync(); - return settings; - } - - /// - /// Updates settings from a dictionary of key-value pairs. - /// - public async Task UpdateSettingsFromDictionaryAsync(Dictionary dictionary) - { - var settings = await GetSettingsAsync(); - - foreach (var (key, value) in dictionary) - { - if (string.Equals(key, "AtrStopLossMultiplier", StringComparison.OrdinalIgnoreCase) && double.TryParse(value, out var atr)) - settings.AtrStopLossMultiplier = atr; - else if (string.Equals(key, "RiskPerTradePercentage", StringComparison.OrdinalIgnoreCase) && double.TryParse(value, out var risk)) - settings.RiskPerTradePercentage = risk; - else if (string.Equals(key, "MaxOpenPositions", StringComparison.OrdinalIgnoreCase) && int.TryParse(value, out var maxPos)) - settings.MaxOpenPositions = maxPos; - } - - settings.UpdatedAt = DateTime.UtcNow; - await SaveSettingsAsync(settings); - } -} diff --git a/FinlyticTrades/Services/TradeLifecycleService.cs b/FinlyticTrades/Services/TradeLifecycleService.cs deleted file mode 100644 index 911a607..0000000 --- a/FinlyticTrades/Services/TradeLifecycleService.cs +++ /dev/null @@ -1,517 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using FinlyticCore.Models.Analyzer; -using FinlyticCore.Models.Trades; -using FinlyticCore.Services; -using FinlyticTrades.Database; -using FinlyticTrades.Entities; -using FinlyticTrades.Util; -using Microsoft.EntityFrameworkCore; - -namespace FinlyticTrades.Services; - -public interface ITradeLifecycleService -{ - Task ProcessProposedTradeAsync(TradeProposalDto proposal, CancellationToken cancellationToken = default); - Task ProcessManualAnalysisResponseAsync(ManualAnalysisResponseDto response, string userId, CancellationToken cancellationToken = default); - Task AcceptTradeAsync(TradeAcceptanceDto request, CancellationToken cancellationToken = default); - Task AddHourlyUpdateAsync(TradeHourlyUpdateDto update, CancellationToken cancellationToken = default); - Task> GetActiveTradesAsync(string? userId = null, CancellationToken cancellationToken = default); - Task> GetTradesAsync(string? isin, string? status, string? userId = null, CancellationToken cancellationToken = default); - Task CloseTradeAsync(string tradeId, CloseTradeRequest request, CancellationToken cancellationToken = default); - Task RejectTradeAsync(string tradeId, CloseTradeRequest request, CancellationToken cancellationToken = default); - void CalculatePnL(TradeEntity trade, decimal? overridePrice = null); -} - -public class TradeLifecycleService : ITradeLifecycleService -{ - private readonly TradesDbContext _dbContext; - private readonly IFinlyticLogger _finlyticLogger; - - public TradeLifecycleService(TradesDbContext dbContext, IFinlyticLogger finlyticLogger) - { - _dbContext = dbContext; - _finlyticLogger = finlyticLogger; - } - - public async Task ProcessManualAnalysisResponseAsync(ManualAnalysisResponseDto response, string userId, CancellationToken cancellationToken = default) - { - if (response == null || !response.IsTradeProposed) - { - await _finlyticLogger.LogInfoAsync(SettingKeys.TradesChannel, "[TradeLifecycleService] Manual analysis response indicated NO trade proposed (AnalysisId: {AnalysisId}). Skipping.", response?.AnalysisId); - return false; - } - - if (response.Proposal != null) - { - response.Proposal.UserId = userId; - return await ProcessProposedTradeAsync(response.Proposal, cancellationToken); - } - - if (response.N8nResponse != null) - { - var n8n = response.N8nResponse; - var exec = n8n.ExecutionPlan; - - var generatedProposal = new TradeProposalDto - { - TradeId = "PROP-" + Guid.NewGuid().ToString("N")[..10].ToUpperInvariant(), - AnalysisId = response.AnalysisId, - EventId = response.AnalysisId, - UserId = userId, - IsGlobalProposal = false, - Status = "Proposed", - SignalType = string.Equals(n8n.SuggestedDirection, "Short", StringComparison.OrdinalIgnoreCase) ? "SELL" : "BUY", - RiskTolerance = n8n.SuggestedRisk, - Timeframe = n8n.SuggestedTimeframe, - Reasoning = n8n.AiReasoning, - StopLoss = exec?.StopLoss ?? 0m, - TakeProfit = exec?.TakeProfitTargets?.FirstOrDefault() ?? 0m, - EntryZoneMin = exec?.EntryZone?.Min, - EntryZoneMax = exec?.EntryZone?.Max, - TakeProfitTargets = exec?.TakeProfitTargets, - RiskRewardRatio = exec?.RiskRewardRatio, - MaxLeverage = exec?.MaxLeverage, - TechnicalRationale = n8n.DetailedAnalysis?.TechnicalRationale ?? string.Empty, - FundamentalRationale = n8n.DetailedAnalysis?.FundamentalRationale ?? string.Empty, - RiskWarning = n8n.DetailedAnalysis?.RiskWarning ?? string.Empty, - CreatedAt = DateTime.UtcNow - }; - - return await ProcessProposedTradeAsync(generatedProposal, cancellationToken); - } - - return false; - } - - public async Task ProcessProposedTradeAsync(TradeProposalDto proposal, CancellationToken cancellationToken = default) - { - if (string.IsNullOrWhiteSpace(proposal.Symbol) && string.IsNullOrWhiteSpace(proposal.Isin)) - { - await _finlyticLogger.LogWarningAsync(SettingKeys.TradesChannel, "[TradeLifecycleService] ProcessProposedTradeAsync: Received proposal with missing Symbol and ISIN. Skipping."); - return false; - } - - var targetStatus = string.Equals(proposal.Status, "Rejected", StringComparison.OrdinalIgnoreCase) - ? TradeStatus.Rejected - : TradeStatus.Proposed; - - var existingTrade = await _dbContext.Trades - .FirstOrDefaultAsync(t => - (!string.IsNullOrWhiteSpace(proposal.TradeId) && t.TradeId == proposal.TradeId) || - (!string.IsNullOrWhiteSpace(proposal.AnalysisId) && t.AnalysisId == proposal.AnalysisId) || - (!string.IsNullOrWhiteSpace(proposal.Isin) && t.Isin == proposal.Isin && (t.Status == TradeStatus.Proposed || t.Status == TradeStatus.Active)), - cancellationToken); - - if (existingTrade != null) - { - if (existingTrade.Status == TradeStatus.Active) - { - await _finlyticLogger.LogInfoAsync(SettingKeys.TradesChannel, "[TradeLifecycleService] An ACTIVE trade {TradeId} already exists for {Symbol} ({Isin}). Skipping duplicate proposed trade creation.", - existingTrade.TradeId, proposal.Symbol, proposal.Isin); - return true; - } - - if (existingTrade.Status != TradeStatus.Closed) - { - existingTrade.Status = targetStatus; - } - - MapProposalToEntity(proposal, existingTrade); - _dbContext.Trades.Update(existingTrade); - await _dbContext.SaveChangesAsync(cancellationToken); - - await _finlyticLogger.LogInfoAsync(SettingKeys.TradesChannel, "[TradeLifecycleService] Successfully UPDATED existing trade proposal {TradeId} for Symbol {Symbol} (ISIN: {Isin}) with status {Status}", - existingTrade.TradeId, proposal.Symbol, proposal.Isin, existingTrade.Status); - - return true; - } - - string tradeId = !string.IsNullOrWhiteSpace(proposal.TradeId) ? proposal.TradeId : ("TRD-" + Guid.NewGuid().ToString("N")[..10].ToUpperInvariant()); - - var tradeEntity = new TradeEntity - { - TradeId = tradeId, - CreatedAt = DateTime.UtcNow - }; - - MapProposalToEntity(proposal, tradeEntity); - tradeEntity.Status = targetStatus; - - _dbContext.Trades.Add(tradeEntity); - await _dbContext.SaveChangesAsync(cancellationToken); - - await _finlyticLogger.LogInfoAsync(SettingKeys.TradesChannel, "[TradeLifecycleService] Successfully ingested NEW trade proposal {TradeId} for Symbol {Symbol} (ISIN: {Isin}) with status {Status}", - tradeId, proposal.Symbol, proposal.Isin, targetStatus); - - return true; - } - - public async Task AcceptTradeAsync(TradeAcceptanceDto request, CancellationToken cancellationToken = default) - { - string targetUserId = !string.IsNullOrWhiteSpace(request.UserId) ? request.UserId : "default_user"; - - // 1. Prüfen, ob DIESER spezifische Nutzer diesen Trade/AnalysisId bereits als aktiven Trade angenommen hat - var userExistingTrade = await _dbContext.Trades - .FirstOrDefaultAsync(t => - !t.IsGlobalProposal && - t.UserId == targetUserId && - ((!string.IsNullOrEmpty(request.TradeId) && t.TradeId == request.TradeId) || - (!string.IsNullOrEmpty(request.AnalysisId) && t.AnalysisId == request.AnalysisId)), - cancellationToken); - - if (userExistingTrade != null) - { - if (userExistingTrade.Status == TradeStatus.Closed) - { - await _finlyticLogger.LogWarningAsync(SettingKeys.TradesChannel, "[TradeLifecycleService] Refused to accept trade {TradeId} because user's trade is already CLOSED", userExistingTrade.TradeId); - return null; - } - - // Bestehenden User-Trade mit neuen Parametern aktualisieren - if (request.ActualEntryPrice > 0) userExistingTrade.ActualEntryPrice = request.ActualEntryPrice; - if (request.EntryPrice > 0) userExistingTrade.EntryPrice = request.EntryPrice.Value; - if (request.PositionSize > 0) userExistingTrade.PositionSize = request.PositionSize; - if (request.LeverageUsed > 0) userExistingTrade.LeverageUsed = request.LeverageUsed; - if (request.Quantity > 0) userExistingTrade.Quantity = request.Quantity; - if (request.EntryFee.HasValue) userExistingTrade.EntryFee = request.EntryFee; - if (request.ExitFee.HasValue) userExistingTrade.ExitFee = request.ExitFee; - if (request.StopLoss > 0) userExistingTrade.StopLoss = request.StopLoss.Value; - if (request.TakeProfit > 0) userExistingTrade.TakeProfit = request.TakeProfit.Value; - if (request.KnockoutThreshold > 0) userExistingTrade.KnockoutThreshold = request.KnockoutThreshold; - if (!string.IsNullOrWhiteSpace(request.Timeframe)) userExistingTrade.Timeframe = request.Timeframe; - if (!string.IsNullOrWhiteSpace(request.DerivativeIsin)) userExistingTrade.DerivativeIsin = request.DerivativeIsin; - if (!string.IsNullOrWhiteSpace(request.Reasoning)) userExistingTrade.Reasoning = request.Reasoning; - - userExistingTrade.ExecutionTimestamp = request.ExecutionTimestamp?.ToUniversalTime() ?? DateTime.UtcNow; - - userExistingTrade.PnlAbsolute = -(userExistingTrade.EntryFee ?? 0m) - (userExistingTrade.ExitFee ?? 0m); - if (userExistingTrade.PositionSize > 0) - { - userExistingTrade.PnlPercent = (userExistingTrade.PnlAbsolute / userExistingTrade.PositionSize) * 100m; - } - - _dbContext.Trades.Update(userExistingTrade); - await _dbContext.SaveChangesAsync(cancellationToken); - - await _finlyticLogger.LogInfoAsync(SettingKeys.TradesChannel, "[TradeLifecycleService] Successfully UPDATED existing trade {TradeId} for ISIN {Isin}, UserId: {UserId}", userExistingTrade.TradeId, userExistingTrade.Isin, userExistingTrade.UserId); - return userExistingTrade; - } - - // 2. Globalen Trade-Vorschlag finden (dieser bleibt unverändert in der DB, damit andere Nutzer ihn ebenfalls annehmen können) - var proposal = await _dbContext.Trades - .FirstOrDefaultAsync(t => - (t.IsGlobalProposal || t.Status == TradeStatus.Proposed) && - ((!string.IsNullOrEmpty(request.AnalysisId) && t.AnalysisId == request.AnalysisId) || - (!string.IsNullOrEmpty(request.TradeId) && t.TradeId == request.TradeId) || - (!string.IsNullOrEmpty(request.Isin) && t.Isin == request.Isin)), - cancellationToken); - - var targetTradeId = "TRD-" + Guid.NewGuid().ToString("N")[..10].ToUpperInvariant(); - - var newTrade = new TradeEntity - { - TradeId = targetTradeId, - AnalysisId = proposal?.AnalysisId ?? (string.IsNullOrWhiteSpace(request.AnalysisId) ? Guid.NewGuid().ToString("N") : request.AnalysisId), - EventId = proposal?.EventId ?? request.AnalysisId, - Sector = proposal?.Sector ?? request.Sector ?? "General", - Symbol = proposal?.Symbol ?? request.Symbol ?? request.Isin, - Isin = proposal?.Isin ?? request.Isin, - CompanyName = proposal?.CompanyName ?? request.CompanyName ?? request.Symbol ?? request.Isin, - Status = TradeStatus.Active, - IsGlobalProposal = false, - UserId = targetUserId, - - EntryPrice = proposal?.EntryPrice ?? request.EntryPrice ?? request.ActualEntryPrice ?? 0m, - StopLoss = request.StopLoss > 0 ? request.StopLoss.Value : (proposal?.StopLoss ?? 0m), - TakeProfit = request.TakeProfit > 0 ? request.TakeProfit.Value : (proposal?.TakeProfit ?? 0m), - SignalType = proposal?.SignalType ?? request.SignalType ?? "BUY", - RiskTolerance = proposal?.RiskTolerance ?? "Moderate", - Timeframe = proposal?.Timeframe ?? request.Timeframe ?? "1D", - InstrumentType = proposal?.InstrumentType ?? request.InstrumentType ?? "Stock", - DerivativeIsin = request.DerivativeIsin ?? proposal?.DerivativeIsin, - WinRate = proposal?.WinRate ?? 50, - VixRegime = proposal?.VixRegime ?? FinlyticCore.Models.Analyzer.VixMarketRegime.Normal, - VixValue = proposal?.VixValue ?? 15, - Reasoning = proposal?.Reasoning ?? request.Reasoning ?? "User Accepted Trade", - EntryZoneMin = proposal?.EntryZoneMin, - EntryZoneMax = proposal?.EntryZoneMax, - TakeProfitTargets = proposal?.TakeProfitTargets, - RiskRewardRatio = proposal?.RiskRewardRatio, - MaxLeverage = proposal?.MaxLeverage, - TechnicalRationale = proposal?.TechnicalRationale ?? string.Empty, - FundamentalRationale = proposal?.FundamentalRationale ?? string.Empty, - RiskWarning = proposal?.RiskWarning ?? string.Empty, - CreatedAt = DateTime.UtcNow, - - ActualEntryPrice = request.ActualEntryPrice > 0 ? request.ActualEntryPrice : (proposal?.EntryPrice ?? request.EntryPrice ?? 0m), - PositionSize = request.PositionSize, - LeverageUsed = request.LeverageUsed > 0 ? request.LeverageUsed : 1m, - EntryFee = request.EntryFee, - ExitFee = request.ExitFee, - ExecutionTimestamp = request.ExecutionTimestamp?.ToUniversalTime() ?? DateTime.UtcNow, - Quantity = request.Quantity > 0 ? request.Quantity : 1m, - KnockoutThreshold = request.KnockoutThreshold, - IsRecurring = request.IsRecurring, - DerivativeProductCategories = proposal?.DerivativeProductCategories != null ? new List(proposal.DerivativeProductCategories) : new List() - }; - - newTrade.PnlAbsolute = -(newTrade.EntryFee ?? 0m) - (newTrade.ExitFee ?? 0m); - if (newTrade.PositionSize > 0) - { - newTrade.PnlPercent = (newTrade.PnlAbsolute / newTrade.PositionSize) * 100m; - } - - _dbContext.Trades.Add(newTrade); - await _dbContext.SaveChangesAsync(cancellationToken); - - await _finlyticLogger.LogInfoAsync(SettingKeys.TradesChannel, "[TradeLifecycleService] Successfully CREATED individual active trade {TradeId} for ISIN {Isin}, UserId: {UserId} from proposal {AnalysisId}", - newTrade.TradeId, newTrade.Isin, newTrade.UserId, newTrade.AnalysisId); - - return newTrade; - } - - public async Task AddHourlyUpdateAsync(TradeHourlyUpdateDto update, CancellationToken cancellationToken = default) - { - var matchedTrades = await _dbContext.Trades - .Where(t => t.TradeId == update.TradeId || (t.AnalysisId != null && t.AnalysisId == update.TradeId) || t.Id.ToString() == update.TradeId) - .ToListAsync(cancellationToken); - - if (matchedTrades.Count == 0) - { - await _finlyticLogger.LogWarningAsync(SettingKeys.TradesChannel, "[TradeLifecycleService] Cannot add hourly update: No active or proposed trades found for identifier {TradeId}.", update.TradeId); - return; - } - - foreach (var trade in matchedTrades) - { - if (trade.Status != TradeStatus.Active && trade.Status != TradeStatus.Proposed) - { - continue; - } - - var updateEntity = new TradeHourlyUpdateEntity - { - TradeId = trade.Id, - Recommendation = update.Recommendation, - CurrentPrice = update.CurrentPrice, - SuggestedStopLoss = update.SuggestedStopLoss, - SuggestedTakeProfit = update.SuggestedTakeProfit, - VixValue = update.VixValue, - Reasoning = update.Reasoning, - Timestamp = update.Timestamp - }; - - _dbContext.TradeHourlyUpdates.Add(updateEntity); - - if (update.SuggestedStopLoss.HasValue && update.SuggestedStopLoss > 0) - trade.StopLoss = update.SuggestedStopLoss.Value; - if (update.SuggestedTakeProfit.HasValue && update.SuggestedTakeProfit > 0) - trade.TakeProfit = update.SuggestedTakeProfit.Value; - - if (string.Equals(update.Recommendation, "Close", StringComparison.OrdinalIgnoreCase)) - { - if (trade.IsGlobalProposal || trade.Status == TradeStatus.Proposed) - { - trade.Status = TradeStatus.Invalidated; - trade.CloseReason = "ProposalInvalidated"; - trade.ClosedAt = DateTime.UtcNow; - } - else - { - await _finlyticLogger.LogInfoAsync(SettingKeys.TradesChannel, "[TradeLifecycleService] Active trade {TradeId} (UserId: {UserId}) received Close recommendation ({Reasoning}). Trade kept Active for user action.", - trade.TradeId, trade.UserId, update.Reasoning); - } - } - } - - await _dbContext.SaveChangesAsync(cancellationToken); - await _finlyticLogger.LogInfoAsync(SettingKeys.TradesChannel, "[TradeLifecycleService] Added hourly update across {Count} matched trades for identifier {TradeId}. Rec: {Rec}, Price: {Price}", - matchedTrades.Count, update.TradeId, update.Recommendation, update.CurrentPrice); - } - - public async Task> GetActiveTradesAsync(string? userId = null, CancellationToken cancellationToken = default) - { - var query = _dbContext.Trades.AsNoTracking().Include(t => t.HourlyUpdates).AsQueryable(); - - if (!string.IsNullOrWhiteSpace(userId)) - { - query = query.Where(t => t.UserId == userId || t.IsGlobalProposal); - } - - return await query - .Where(t => t.Status == TradeStatus.Active || t.Status == TradeStatus.Proposed) - .OrderByDescending(t => t.CreatedAt) - .ToListAsync(cancellationToken); - } - - public async Task> GetTradesAsync(string? isin, string? status, string? userId = null, CancellationToken cancellationToken = default) - { - var query = _dbContext.Trades.AsNoTracking().Include(t => t.HourlyUpdates).AsQueryable(); - - if (!string.IsNullOrWhiteSpace(userId)) - { - query = query.Where(t => t.UserId == userId || t.IsGlobalProposal); - } - - if (!string.IsNullOrWhiteSpace(isin)) - { - query = query.Where(t => t.Isin == isin); - } - - if (!string.IsNullOrWhiteSpace(status) && Enum.TryParse(status, true, out var parsedStatus)) - { - query = query.Where(t => t.Status == parsedStatus); - } - - return await query.OrderByDescending(t => t.CreatedAt).ToListAsync(cancellationToken); - } - - public async Task CloseTradeAsync(string tradeId, CloseTradeRequest request, CancellationToken cancellationToken = default) - { - var trade = await _dbContext.Trades - .FirstOrDefaultAsync(t => t.TradeId == tradeId || t.Id.ToString() == tradeId, cancellationToken); - - if (trade == null) return null; - - trade.Status = TradeStatus.Closed; - trade.UserExitPrice = request.UserExitPrice; - trade.UserExitTimestamp = request.UserExitTimestamp?.ToUniversalTime() ?? DateTime.UtcNow; - if (request.ExitFee > 0m) - { - trade.ExitFee = request.ExitFee; - } - trade.CloseReason = request.CloseReason; - trade.ClosedAt = DateTime.UtcNow; - - CalculatePnL(trade); - - await _dbContext.SaveChangesAsync(cancellationToken); - await _finlyticLogger.LogInfoAsync(SettingKeys.TradesChannel, "[TradeLifecycleService] Trade {TradeId} manually closed at price {ExitPrice}. PnL: {PnlAbs} ({PnlPct:F2}%)", - trade.TradeId, trade.UserExitPrice, trade.PnlAbsolute, trade.PnlPercent); - - return trade; - } - - public async Task RejectTradeAsync(string tradeId, CloseTradeRequest request, CancellationToken cancellationToken = default) - { - var trade = await _dbContext.Trades - .FirstOrDefaultAsync(t => t.TradeId == tradeId || t.Id.ToString() == tradeId, cancellationToken); - - if (trade == null) return null; - - trade.Status = TradeStatus.Rejected; - trade.CloseReason = request.CloseReason ?? "UserRejected"; - trade.ClosedAt = DateTime.UtcNow; - - await _dbContext.SaveChangesAsync(cancellationToken); - await _finlyticLogger.LogInfoAsync(SettingKeys.TradesChannel, "[TradeLifecycleService] Trade {TradeId} rejected by user.", trade.TradeId); - - return trade; - } - - private static void MapProposalToEntity(TradeProposalDto dto, TradeEntity entity) - { - entity.AnalysisId = dto.AnalysisId; - entity.EventId = dto.EventId; - entity.UserId = !string.IsNullOrWhiteSpace(dto.UserId) ? dto.UserId : (entity.UserId ?? "default_user"); - entity.IsGlobalProposal = dto.IsGlobalProposal; - entity.Sector = dto.Sector; - entity.Symbol = dto.Symbol; - entity.Isin = dto.Isin; - entity.CompanyName = dto.CompanyName; - - entity.EntryPrice = dto.EntryPrice; - entity.StopLoss = dto.StopLoss; - entity.TakeProfit = dto.TakeProfit; - entity.SignalType = dto.SignalType; - entity.RiskTolerance = dto.RiskTolerance; - entity.Timeframe = dto.Timeframe; - entity.InstrumentType = dto.InstrumentType; - if (!string.IsNullOrWhiteSpace(dto.AssetType)) entity.AssetType = dto.AssetType; - entity.HasCfd = dto.HasCfd; - if (dto.DerivativeProductCategories.Count > 0) entity.DerivativeProductCategories = dto.DerivativeProductCategories; - if (!string.IsNullOrWhiteSpace(dto.DerivativeIsin)) entity.DerivativeIsin = dto.DerivativeIsin; - entity.WinRate = dto.WinRate; - entity.VixRegime = dto.VixRegime; - entity.VixValue = dto.VixValue; - entity.TtlMinutes = dto.TtlMinutes; - entity.Reasoning = dto.Reasoning; - - entity.EntryZoneMin = dto.EntryZoneMin; - entity.EntryZoneMax = dto.EntryZoneMax; - entity.TakeProfitTargets = dto.TakeProfitTargets != null ? string.Join(",", dto.TakeProfitTargets) : entity.TakeProfitTargets; - entity.RiskRewardRatio = dto.RiskRewardRatio; - entity.MaxLeverage = dto.MaxLeverage; - entity.TechnicalRationale = dto.TechnicalRationale; - entity.FundamentalRationale = dto.FundamentalRationale; - entity.RiskWarning = dto.RiskWarning; - - if (dto.ActualEntryPrice.HasValue) entity.ActualEntryPrice = dto.ActualEntryPrice; - if (dto.PositionSize.HasValue) entity.PositionSize = dto.PositionSize; - if (dto.LeverageUsed.HasValue) entity.LeverageUsed = dto.LeverageUsed; - if (dto.EntryFee.HasValue) entity.EntryFee = dto.EntryFee; - if (dto.ExitFee.HasValue) entity.ExitFee = dto.ExitFee; - if (dto.ExecutionTimestamp.HasValue) entity.ExecutionTimestamp = dto.ExecutionTimestamp; - if (dto.Quantity.HasValue) entity.Quantity = dto.Quantity; - if (dto.KnockoutThreshold.HasValue) entity.KnockoutThreshold = dto.KnockoutThreshold; - entity.IsRecurring = dto.IsRecurring; - } - - public void CalculatePnL(TradeEntity trade, decimal? overridePrice = null) - { - decimal? evalPrice = overridePrice ?? trade.UserExitPrice ?? trade.HourlyUpdates?.LastOrDefault()?.CurrentPrice; - if (!evalPrice.HasValue || evalPrice.Value <= 0m) return; - - decimal exitPrice = evalPrice.Value; - decimal entryPrice = trade.ActualEntryPrice.HasValue && trade.ActualEntryPrice.Value > 0m - ? trade.ActualEntryPrice.Value - : trade.EntryPrice; - - if (entryPrice <= 0m) return; - - decimal positionSize = trade.PositionSize.HasValue && trade.PositionSize.Value > 0m - ? trade.PositionSize.Value - : ((trade.Quantity ?? 1m) * entryPrice); - - decimal entryFee = trade.EntryFee ?? 0m; - decimal exitFee = trade.ExitFee ?? 0m; - decimal totalFees = entryFee + exitFee; - - decimal rawMoveRatio; - bool isShort = string.Equals(trade.SignalType, "SELL", StringComparison.OrdinalIgnoreCase) || - string.Equals(trade.SignalType, "SHORT", StringComparison.OrdinalIgnoreCase); - - if (isShort) - { - rawMoveRatio = (entryPrice - exitPrice) / entryPrice; - } - else - { - rawMoveRatio = (exitPrice - entryPrice) / entryPrice; - } - - decimal pnlAbs; - if (string.Equals(trade.InstrumentType, "KnockOut", StringComparison.OrdinalIgnoreCase) || - string.Equals(trade.InstrumentType, "Certificate", StringComparison.OrdinalIgnoreCase) || - string.Equals(trade.InstrumentType, "Option", StringComparison.OrdinalIgnoreCase)) - { - pnlAbs = (rawMoveRatio * positionSize) - totalFees; - } - else - { - decimal leverage = trade.LeverageUsed > 0m ? trade.LeverageUsed.Value : 1m; - pnlAbs = (rawMoveRatio * positionSize * leverage) - totalFees; - } - - trade.PnlAbsolute = Math.Round(pnlAbs, 4); - trade.PnlPercent = positionSize > 0m - ? Math.Round((pnlAbs / positionSize) * 100.0m, 2) - : 0m; - - trade.IsWin = pnlAbs > 0m; - } -} \ No newline at end of file diff --git a/FinlyticTrades/Util/SettingKeys.cs b/FinlyticTrades/Util/SettingKeys.cs deleted file mode 100644 index 0d99cf4..0000000 --- a/FinlyticTrades/Util/SettingKeys.cs +++ /dev/null @@ -1,22 +0,0 @@ -using FinlyticCore.Models.Settings; - -namespace FinlyticTrades.Util; - -public static class SettingKeys -{ - // --- Logging-Kanäle --- - public static readonly SettingKey TradesChannel = new("Logging.Channel.Trades", true); - public static readonly SettingKey MqttChannel = new("Logging.Channel.MQTT", true); - public static readonly SettingKey HealthPingChannel = new("Logging.Channel.Health", true); - - // --- Trade Management & Limits --- - public static readonly SettingKey MaxActiveTradesCount = new("Trades.MaxActiveTradesCount", 20); - public static readonly SettingKey AutoArchiveClosedTradesDays = new("Trades.AutoArchiveClosedTradesDays", 30); - public static readonly SettingKey DefaultSlippageTolerancePercent = new("Trades.DefaultSlippageTolerancePercent", 0.5); - public static readonly SettingKey ProposedTradeExpirationHours = new("Trades.ProposedTradeExpirationHours", 24); - - // --- Parquet / Data Export --- - public static readonly SettingKey EnableParquetExport = new("Export.EnableParquetExport", true); - public static readonly SettingKey ParquetExportIntervalHours = new("Export.ParquetExportIntervalHours", 6); - public static readonly SettingKey ParquetExportDirectory = new("Export.ParquetExportDirectory", "data/exports/trades"); -} diff --git a/FinlyticTrades/Util/TradesMqttClient.cs b/FinlyticTrades/Util/TradesMqttClient.cs deleted file mode 100644 index 4548f30..0000000 --- a/FinlyticTrades/Util/TradesMqttClient.cs +++ /dev/null @@ -1,456 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Text.Json; -using System.Threading; -using System.Threading.Tasks; -using FinlyticCore.Dtos; -using FinlyticCore.Dtos.Settings; -using FinlyticCore.Dtos.TechnicalAnalysis; -using FinlyticCore.Models; -using FinlyticCore.Models.Trades; -using FinlyticCore.Services; -using FinlyticCore.Util; -using FinlyticTrades.Entities; -using FinlyticTrades.Services; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; - -namespace FinlyticTrades.Util; - -public class TradesMqttClient : ManagedMqttClient, IHostedService -{ - private readonly IConfiguration _configuration; - private readonly IServiceScopeFactory _scopeFactory; - private readonly ILogger _logger; - - public TradesMqttClient( - IConfiguration configuration, - IServiceScopeFactory scopeFactory, - ILogger logger) : base(logger) - { - _configuration = configuration; - _scopeFactory = scopeFactory; - _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_trades")}_{Guid.NewGuid():N}" - }; - - _logger.LogInformation("Starting Unified Trades MQTT Client. Host: {Host}, ClientId: {ClientId}", config.Host, config.ClientId); - await ConnectAsync(config); - } - - public async Task StopAsync(CancellationToken cancellationToken) - { - _logger.LogInformation("Stopping Unified Trades MQTT Client."); - await DisconnectAsync(); - } - - protected override async Task OnConnectedAsync() - { - _logger.LogInformation("Trades MQTT Client connected. Subscribing to topics..."); - - await SubscribeAsync("finlytic/trades/proposed/#"); - await SubscribeAsync("finlytic/trades/updates/#"); - await SubscribeAsync("finlytic/trades/accept/#"); - await SubscribeAsync("services/request/trades_Get/#"); - await SubscribeAsync("services/request/trades_Close/#"); - await SubscribeAsync("services/request/trades_Reject/#"); - await SubscribeAsync("services/request/trades_Accept/#"); - await SubscribeAsync("services/request/trades_settings_GetAll/#"); - await SubscribeAsync("services/request/trades_settings_Update/#"); - await SubscribeAsync("services/config/updated/#"); - await SubscribeAsync("services/request/health_Ping/#"); - await SubscribeAsync("services/response/tr_GetLivePrice/#"); - - FinlyticCore.Services.FinlyticLogBroadcaster.OnLogPublished = async (logDto) => - { - if (IsConnected && string.Equals(logDto.ServiceName, "FinlyticTrades", StringComparison.OrdinalIgnoreCase)) - { - await PublishAsync("finlytic/logs/FinlyticTrades", logDto); - } - }; - - _logger.LogInformation("Successfully subscribed to all event and RPC channels."); - } - - protected override async Task OnMessageReceivedAsync(string topic, string payloadStr) - { - try - { - if (topic.Contains("health_Ping", StringComparison.OrdinalIgnoreCase)) - { - var segments = topic.Split('/'); - bool isForMe = segments.Length >= 5 - ? segments[3].Equals("FinlyticTrades", StringComparison.OrdinalIgnoreCase) - : topic.Contains("FinlyticTrades", StringComparison.OrdinalIgnoreCase); - - if (isForMe) - { - var correlationId = segments[^1]; - string respTopic = $"services/response/health_Ping/{correlationId}"; - var healthResp = new ServiceHealthResponse("FinlyticTrades", "Online", DateTime.UtcNow, "Connected"); - await PublishAsync(respTopic, healthResp); - using var scope = _scopeFactory.CreateScope(); - var finlyticLogger = scope.ServiceProvider.GetRequiredService>(); - await finlyticLogger.LogInfoAsync(SettingKeys.HealthPingChannel, "[TradesMqttClient] Responded to live health_Ping RPC request [CorrelationId: {CorrelationId}].", correlationId); - } - return; - } - - if (topic.StartsWith("services/config/updated", StringComparison.OrdinalIgnoreCase)) - { - if (topic.EndsWith("FinlyticTrades", StringComparison.OrdinalIgnoreCase)) - { - var payload = JsonSerializer.Deserialize(payloadStr, FinlyticJsonSerializerContext.Default.ServiceConfigUpdatePayload); - if (payload?.Settings != null && payload.Settings.Count > 0) - { - using var scope = _scopeFactory.CreateScope(); - var settings = scope.ServiceProvider.GetRequiredService(); - var dict = payload.Settings.ToDictionary(k => k.Key, v => (object?)v.Value); - await settings.UpdateSettingsAsync(dict); - } - } - return; - } - - if (topic.StartsWith("services/request/trades_settings_GetAll", StringComparison.OrdinalIgnoreCase)) - { - var correlationId = topic.Split('/').Last(); - await HandleSettingsGetAllAsync(correlationId); - return; - } - - if (topic.StartsWith("services/request/trades_settings_Update", StringComparison.OrdinalIgnoreCase)) - { - var correlationId = topic.Split('/').Last(); - await HandleSettingsUpdateAsync(payloadStr, correlationId); - return; - } - - using var msgScope = _scopeFactory.CreateScope(); - var tradeLifecycleService = msgScope.ServiceProvider.GetRequiredService(); - var finlyticLoggerInstance = msgScope.ServiceProvider.GetRequiredService>(); - - if (topic.StartsWith("finlytic/trades/proposed/")) - { - var proposal = JsonSerializer.Deserialize(payloadStr, FinlyticJsonSerializerContext.Default.TradeProposalDto); - if (proposal != null && (!string.IsNullOrWhiteSpace(proposal.Symbol) || !string.IsNullOrWhiteSpace(proposal.Isin))) - { - await tradeLifecycleService.ProcessProposedTradeAsync(proposal, CancellationToken.None); - } - else - { - await finlyticLoggerInstance.LogWarningAsync(SettingKeys.TradesChannel, "[TradesMqttClient] Received proposed trade payload but Symbol/ISIN is empty. Skipping ingestion."); - } - } - else if (topic.StartsWith("finlytic/trades/accept/")) - { - var acceptDto = JsonSerializer.Deserialize(payloadStr, FinlyticJsonSerializerContext.Default.TradeAcceptanceDto); - if (acceptDto != null) - { - var newTrade = await tradeLifecycleService.AcceptTradeAsync(acceptDto, CancellationToken.None); - if (newTrade != null) - { - var dto = MapToDto(newTrade); - await PublishTradeUpdateAsync(dto); - } - } - } - else if (topic.StartsWith("services/request/trades_Accept/")) - { - var correlationId = topic.Split('/').Last(); - var acceptDto = JsonSerializer.Deserialize(payloadStr, FinlyticJsonSerializerContext.Default.TradeAcceptanceDto); - if (acceptDto != null) - { - var acceptedTrade = await tradeLifecycleService.AcceptTradeAsync(acceptDto, CancellationToken.None); - if (acceptedTrade != null) - { - var acceptedDto = MapToDto(acceptedTrade); - await PublishAsync($"services/response/trades_Accept/{correlationId}", acceptedDto); - await PublishTradeUpdateAsync(acceptedDto); - } - } - } - else if (topic.StartsWith("finlytic/trades/updates/")) - { - var update = JsonSerializer.Deserialize(payloadStr, FinlyticJsonSerializerContext.Default.TradeHourlyUpdateDto); - if (update != null) - { - await tradeLifecycleService.AddHourlyUpdateAsync(update, CancellationToken.None); - } - } - else if (topic.StartsWith("services/request/trades_Get/")) - { - var correlationId = topic.Split('/').Last(); - var request = JsonSerializer.Deserialize(payloadStr, FinlyticJsonSerializerContext.Default.GetTradesRequest); - - string? isin = request?.Isin; - string? status = request?.Status; - string? userId = request?.UserId; - - var trades = await tradeLifecycleService.GetTradesAsync(isin, status, userId); - - var activeTrades = trades.Where(t => t.Status == TradeStatus.Active && !string.IsNullOrWhiteSpace(t.Isin)).ToList(); - if (activeTrades.Count > 0) - { - try - { - var priceTasks = activeTrades.Select(t => FetchLivePriceAsync(t.Isin)).ToList(); - var livePricesTask = Task.WhenAll(priceTasks); - if (await Task.WhenAny(livePricesTask, Task.Delay(1500)) == livePricesTask) - { - var livePrices = await livePricesTask; - for (int i = 0; i < activeTrades.Count; i++) - { - var lp = livePrices[i]; - if (lp != null && lp.CurrentPrice > 0m) - { - var trade = activeTrades[i]; - tradeLifecycleService.CalculatePnL(trade, lp.CurrentPrice); - } - } - } - } - catch (Exception ex) - { - await finlyticLoggerInstance.LogDebugAsync(SettingKeys.TradesChannel, "[TradesMqttClient] Live price fetch skipped or timed out during trades_Get: {Message}", ex.Message); - } - } - - var dtos = trades.Select(MapToDto).ToList(); - await PublishAsync($"services/response/trades_Get/{correlationId}", dtos); - } - else if (topic.StartsWith("services/request/trades_Close/")) - { - var parts = topic.Split('/'); - var tradeId = parts.Length > 3 ? parts[3] : string.Empty; - var correlationId = parts.Length > 4 ? parts[4] : string.Empty; - - var request = JsonSerializer.Deserialize(payloadStr, FinlyticJsonSerializerContext.Default.CloseTradeRequest); - - if (request != null && !string.IsNullOrEmpty(tradeId)) - { - var closedTrade = await tradeLifecycleService.CloseTradeAsync(tradeId, request); - if (closedTrade != null) - { - var closedDto = MapToDto(closedTrade); - await PublishAsync($"services/response/trades_Close/{correlationId}", closedDto); - - string sectorSafe = string.IsNullOrWhiteSpace(closedTrade.Sector) ? "general" : closedTrade.Sector.ToLowerInvariant(); - await PublishAsync($"finlytic/trades/closed/{sectorSafe}/{closedTrade.Symbol.ToLowerInvariant()}", closedDto); - await PublishTradeUpdateAsync(closedDto); - } - } - } - else if (topic.StartsWith("services/request/trades_Reject/")) - { - var parts = topic.Split('/'); - var tradeId = parts.Length > 3 ? parts[3] : string.Empty; - var correlationId = parts.Length > 4 ? parts[4] : string.Empty; - - var request = JsonSerializer.Deserialize(payloadStr, FinlyticJsonSerializerContext.Default.CloseTradeRequest); - - if (request != null && !string.IsNullOrEmpty(tradeId)) - { - var rejectedTrade = await tradeLifecycleService.RejectTradeAsync(tradeId, request); - if (rejectedTrade != null) - { - var rejectedDto = MapToDto(rejectedTrade); - await PublishAsync($"services/response/trades_Reject/{correlationId}", rejectedDto); - await PublishTradeUpdateAsync(rejectedDto); - } - } - } - } - catch (Exception ex) - { - using var scope = _scopeFactory.CreateScope(); - var finlyticLogger = scope.ServiceProvider.GetRequiredService>(); - await finlyticLogger.LogErrorAsync(SettingKeys.TradesChannel, ex, "[TradesMqttClient] Error processing incoming MQTT message on topic {Topic}", topic); - } - } - - private async Task HandleSettingsGetAllAsync(string correlationId) - { - using var scope = _scopeFactory.CreateScope(); - var finlyticLogger = scope.ServiceProvider.GetRequiredService>(); - var settingsService = scope.ServiceProvider.GetRequiredService(); - - await finlyticLogger.LogInfoAsync(SettingKeys.MqttChannel, "[FinlyticTrades] [Settings_GetAll] Retrieving all dynamic settings via reflection [CorrelationId: {CorrelationId}]", correlationId); - try - { - var settings = await settingsService.GetAllRegisteredSettingsAsync(new[] { typeof(SettingKeys) }); - var responseTopic = $"services/response/trades_settings_GetAll/{correlationId}"; - - await PublishAsync(responseTopic, settings); - await finlyticLogger.LogInfoAsync(SettingKeys.MqttChannel, "[FinlyticTrades] [Settings_GetAll] Published {Count} settings to '{ResponseTopic}'", settings.Count, responseTopic); - } - catch (Exception ex) - { - await finlyticLogger.LogErrorAsync(SettingKeys.MqttChannel, ex, "[FinlyticTrades] [Settings_GetAll] Failed to retrieve settings."); - } - } - - private async Task HandleSettingsUpdateAsync(string payload, string correlationId) - { - if (string.IsNullOrWhiteSpace(payload)) return; - - using var scope = _scopeFactory.CreateScope(); - var finlyticLogger = scope.ServiceProvider.GetRequiredService>(); - var settingsService = scope.ServiceProvider.GetRequiredService(); - - await finlyticLogger.LogInfoAsync(SettingKeys.MqttChannel, "[FinlyticTrades] [Settings_Update] Processing settings update RPC [CorrelationId: {CorrelationId}]", correlationId); - try - { - Dictionary? updates = null; - try - { - updates = JsonSerializer.Deserialize>(payload); - } - catch - { - var list = JsonSerializer.Deserialize>(payload); - if (list != null) - { - updates = new Dictionary(); - foreach (var item in list) updates[item.Key] = item.Value; - } - } - - if (updates != null && updates.Count > 0) - { - await settingsService.UpdateSettingsAsync(updates); - await finlyticLogger.LogInfoAsync(SettingKeys.MqttChannel, "[FinlyticTrades] [Settings_Update] Successfully updated {Count} settings in database and cache.", updates.Count); - } - - var currentSettings = await settingsService.GetAllRegisteredSettingsAsync(new[] { typeof(SettingKeys) }); - var responseTopic = $"services/response/trades_settings_Update/{correlationId}"; - await PublishAsync(responseTopic, currentSettings); - } - catch (Exception ex) - { - await finlyticLogger.LogErrorAsync(SettingKeys.MqttChannel, ex, "[FinlyticTrades] [Settings_Update] Failed to update settings."); - } - } - - public async Task PublishTradeUpdateAsync(TradeProposalDto trade) - { - await PublishAsync($"finlytic/trades/user/{trade.UserId ?? "all"}", trade); - await PublishAsync("finlytic/trades/update", trade); - } - - private async Task FetchLivePriceAsync(string isin) - { - if (string.IsNullOrWhiteSpace(isin)) return null; - try - { - return await SendRpcRequestAsync( - "tr_GetLivePrice", - new IsinRequest(isin), - TimeSpan.FromMilliseconds(1200)); - } - catch - { - return null; - } - } - - private static TradeProposalDto MapToDto(TradeEntity t) - { - List? parseTakeProfitTargets() - { - if (string.IsNullOrWhiteSpace(t.TakeProfitTargets)) return null; - - var list = new List(); - var parts = t.TakeProfitTargets.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); - foreach (var part in parts) - { - if (decimal.TryParse(part, NumberStyles.Number, CultureInfo.InvariantCulture, out var val)) - { - list.Add(val); - } - } - return list.Count > 0 ? list : null; - } - - return new TradeProposalDto - { - TradeId = t.TradeId, - Status = t.Status.ToString(), - AnalysisId = t.AnalysisId, - EventId = t.EventId, - Sector = t.Sector, - Symbol = t.Symbol, - Isin = t.Isin, - CompanyName = t.CompanyName, - EntryPrice = t.EntryPrice, - StopLoss = t.StopLoss, - TakeProfit = t.TakeProfit, - SignalType = t.SignalType, - RiskTolerance = t.RiskTolerance, - Timeframe = t.Timeframe, - InstrumentType = t.InstrumentType, - AssetType = t.AssetType, - HasCfd = t.HasCfd, - DerivativeProductCategories = t.DerivativeProductCategories ?? new List(), - DerivativeIsin = t.DerivativeIsin, - WinRate = t.WinRate, - VixRegime = t.VixRegime, - VixValue = t.VixValue, - TtlMinutes = t.TtlMinutes, - Reasoning = t.Reasoning, - EntryZoneMin = t.EntryZoneMin, - EntryZoneMax = t.EntryZoneMax, - TakeProfitTargets = parseTakeProfitTargets(), - RiskRewardRatio = t.RiskRewardRatio, - MaxLeverage = t.MaxLeverage, - TechnicalRationale = t.TechnicalRationale, - FundamentalRationale = t.FundamentalRationale, - RiskWarning = t.RiskWarning, - CreatedAt = t.CreatedAt, - - UserId = t.UserId, - IsGlobalProposal = t.IsGlobalProposal, - ActualEntryPrice = t.ActualEntryPrice, - PositionSize = t.PositionSize, - LeverageUsed = t.LeverageUsed, - EntryFee = t.EntryFee, - ExitFee = t.ExitFee, - ExecutionTimestamp = t.ExecutionTimestamp, - Quantity = t.Quantity, - KnockoutThreshold = t.KnockoutThreshold, - IsRecurring = t.IsRecurring, - PnlAbsolute = t.PnlAbsolute, - PnlPercent = t.PnlPercent, - CurrentPrice = t.UserExitPrice ?? t.HourlyUpdates?.LastOrDefault()?.CurrentPrice, - CloseReason = t.CloseReason, - UserExitTimestamp = t.UserExitTimestamp, - HasPendingExitAlert = t.Status == TradeStatus.Active && t.HourlyUpdates != null && t.HourlyUpdates.Any(u => string.Equals(u.Recommendation, "Close", StringComparison.OrdinalIgnoreCase)), - PendingExitReason = t.Status == TradeStatus.Active ? t.HourlyUpdates?.LastOrDefault(u => string.Equals(u.Recommendation, "Close", StringComparison.OrdinalIgnoreCase))?.Reasoning : null, - HourlyUpdates = t.HourlyUpdates?.OrderBy(u => u.Timestamp).Select(u => new TradeHourlyUpdateDto - { - TradeId = t.TradeId, - Recommendation = u.Recommendation, - CurrentPrice = u.CurrentPrice, - SuggestedStopLoss = u.SuggestedStopLoss, - SuggestedTakeProfit = u.SuggestedTakeProfit, - VixValue = u.VixValue, - Reasoning = u.Reasoning, - Timestamp = u.Timestamp - }).ToList() - }; - } -} \ No newline at end of file diff --git a/Project.md b/Project.md deleted file mode 100644 index c34760e..0000000 --- a/Project.md +++ /dev/null @@ -1,78 +0,0 @@ -# Finlytic Enterprise System Architecture - -Finlytic is an enterprise financial intelligence platform composed of high-performance C# .NET 8 microservices, a web gateway (`FinlyticBackend`), a Flutter application (`FinlyticApp`), a React web interface (`FinlyticWeb`), and a real-time MQTT event mesh. - ---- - -## Ecosystem Architecture Overview - -```mermaid -graph TD - App[FinlyticApp (Flutter)] -->|HTTP REST & SignalR| Backend[FinlyticBackend] - Web[FinlyticWeb (React)] -->|HTTP REST & SignalR| Backend - - Backend <-->|MQTT Pub/Sub & RPC| Broker[MQTT Broker (EMQX / Mosquitto)] - - News[FinlyticNews Service] <-->|MQTT| Broker - Sentiment[FinlyticSentiment Service] <-->|MQTT| Broker - Assets[FinlyticAssets Service] <-->|MQTT| Broker - Fundamentals[FinlyticFundamentals Service] <-->|MQTT| Broker - TA[FinlyticTechnicalAnalysis Service] <-->|MQTT| Broker - Trades[FinlyticTrades Service] <-->|MQTT| Broker - Analyzer[FinlyticAnalyzer Service] <-->|MQTT| Broker - - TR[Trade Republic WS API] <--> Assets - N8N[n8n Webhook / FinBERT] <--> News - N8N <--> Sentiment -``` - ---- - -## Core System Principles & Rules - -1. **Single Web Gateway (`FinlyticBackend`)**: - - `FinlyticBackend` is the **only** microservice hosting HTTP REST and SignalR WebSocket endpoints for external clients (`FinlyticApp`, `FinlyticWeb`). - - All background worker microservices (`FinlyticNews`, `FinlyticSentiment`, `FinlyticAssets`, `FinlyticFundamentals`, `FinlyticTechnicalAnalysis`, `FinlyticTrades`, `FinlyticAnalyzer`) operate strictly as `IHostedService` worker engines with zero Kestrel HTTP webservers. - -2. **Exclusive Inter-Service Communication via MQTT**: - - All background microservices communicate strictly over MQTT topics (Pub/Sub & RPC). - - High-performance, zero-allocation serialization is enforced using `.NET 8 JSON Source Generators` (`FinlyticJsonSerializerContext`). - -3. **Absolute Prohibition of Mock/Demo Data**: - - No mock data, hardcoded fallback arrays, or fake dummy responses are permitted in any microservice or frontend client. - - Either real data is queried from database contexts (PostgreSQL) / external APIs, or empty result sets / explicit exceptions are returned. - ---- - -## Microservices Breakdown - -| Project | Type | Description | -| :--- | :--- | :--- | -| **`FinlyticCore`** | Class Library | Shared DTOs, domain models, MQTT infrastructure (`ManagedMqttClient`), and JSON Source Generator context. | -| **`FinlyticNews`** | Worker Service | Scraping (Playwright/RSS), deduplication, n8n AI enrichment, and news state persistence. | -| **`FinlyticSentiment`** | Worker Service | FinBERT AI sentiment evaluation, ISIN/Sector sentiment aggregation over MQTT. | -| **`FinlyticAssets`** | Worker Service | Trade Republic WebSocket full-scan ingestion, asset metadata indexing (`index.json`), and ISIN JIT lookup. | -| **`FinlyticFundamentals`** | Worker Service | Financial fundamentals scraping, SEC/Financial Modeling Prep integration, and corporate calendar events. | -| **`FinlyticTechnicalAnalysis`** | Worker Service | Real-time technical indicators (RSI, MACD, EMA, Supertrend) and chart pattern detection. | -| **`FinlyticTrades`** | Worker Service | Trade lifecycle management (Active, Closed, TTL worker, Feedback exporter). | -| **`FinlyticAnalyzer`** | Worker Service | 3-layer filter engine, VIX regime tracking, win-rate calculator, and trade signal generation. | -| **`FinlyticBackend`** | Web API / Gateway | ASP.NET Core REST API, JWT authentication, SignalR Hubs (`NewsHub`, `TradeHub`), and MQTT bridge. | -| **`FinlyticWeb`** | Web Application | React/Next.js dashboard web application. | -| **`FinlyticApp`** | Mobile/Cross-Platform App | Flutter application built with Clean Architecture (`models/`, `repositories/`, `bloc/`). | - ---- - -## Status of Implemented & Planned Features - -### Implemented Features -- [x] Zero-Allocation MQTT RPC & Event Mesh across all .NET 8 microservices. -- [x] Removal of Kestrel HTTP servers from all background worker services (`FinlyticAnalyzer`, `FinlyticTrades`, etc.). -- [x] Complete removal of all mock/demo fallbacks in backend and frontend. -- [x] PostgreSQL database indexes on `PublishedAt`, `Status`, `SourceUrl`, `Isin`, `CreatedAt`. -- [x] Clean Architecture migration across all 7 modules in `FinlyticApp` (Trades, Assets, Favorites, Auth, Admin, Calendar, Search). -- [x] SignalR Real-Time Hubs (`NewsHub`, `TradeHub`) with MQTT-to-SignalR broadcasting. - -### Planned Features -- [ ] Automated backtesting engine for multi-year strategy evaluation in `FinlyticAnalyzer`. -- [ ] Order Execution Integration (automated broker API order routing). -- [ ] Push Notifications for iOS/Android via Firebase Cloud Messaging in production deployment. diff --git a/compose.yaml b/compose.yaml index 42d7ef4..3975104 100644 --- a/compose.yaml +++ b/compose.yaml @@ -1,53 +1,129 @@ services: + # ────────────────────────────────────────────────────────────────────────── + # External infrastructure this stack depends on (NOT part of this repo): + # + # 1. PostgreSQL ("OmniDB" by default) — reachable on the external Docker + # network `postgres-network` (see bottom of this file, external: true). + # Configure via: + # DB_HOST (default: OmniDB) + # DB_PORT (default: 5432) + # DB_PASSWORD (required, no default — set in .env) + # + # 2. MQTT broker — runs directly on the Windows host, unauthenticated, + # reachable from containers via the Docker Desktop DNS alias + # `host.docker.internal`. Configure via: + # MQTT_HOST (default: host.docker.internal) + # MQTT_PORT (default: 4545) + # + # On a fresh clone, on a different machine, or to point at different + # infrastructure, override the variables above in a local `.env` file — + # no edits to the service blocks below are needed. + # + # We deliberately do NOT ship a `local-infra` profile with throwaway + # Postgres/Mosquitto containers here. Reasoning: a working local Postgres + # would need to provision 9 distinct databases (finlytic_assets, + # finlytic_news, ... one per service) on first start, which the stock + # `postgres` image cannot do via environment variables alone — it needs + # an `docker-entrypoint-initdb.d` init script. That's an additional file + # outside the scope of this change (compose.yaml / Dockerfiles / + # .dockerignore only), and a second, empty, unauthenticated Postgres + # sitting next to the real one is a plausible source of "why is my data + # missing" confusion for a single-developer repo where the real OmniDB + # already holds live data. The DB_HOST/DB_PORT/MQTT_HOST/MQTT_PORT + # variables above are the actually load-bearing fix: they remove the + # 9x-duplicated hardcoding and make the stack point-elsewhere-capable + # without any code edits. If real multi-developer/CI use ever + # materializes, revisit with a proper init-script-backed local-infra + # profile at that point. + # ────────────────────────────────────────────────────────────────────────── + + # ────────────────────────────────────────────────────────────────────────── + # Build-only prerequisite: Chromium + Node + Playwright CLI base layer. + # Consumed as `FROM finlytic-playwright-base:1.49.0` by BOTH finlyticnews + # and finlyticfundamentals. Compose does not resolve FROM-references across + # services, so this MUST be built before the services that depend on it: + # + # docker compose --profile build-base build finlytic-playwright-base + # docker compose build + # + # Keep PLAYWRIGHT_VERSION in sync with the Microsoft.Playwright NuGet + # package version in FinlyticCore/FinlyticCore.csproj. + # ────────────────────────────────────────────────────────────────────────── + finlytic-playwright-base: + profiles: ["build-base"] + image: finlytic-playwright-base:1.49.0 + build: + context: FinlyticNews + dockerfile: Dockerfile.playwright-base + args: + PLAYWRIGHT_VERSION: "1.49.0" + finlyticassets: image: finlyticassets build: context: . dockerfile: FinlyticAssets/Dockerfile + # unless-stopped: this worker has no required secrets that could be + # permanently misconfigured — any crash is expected to be a transient + # DB/MQTT hiccup, so it should keep retrying indefinitely (Docker backs + # off automatically between attempts). + restart: unless-stopped networks: - postgres-network environment: - - ConnectionStrings__DefaultConnection=Host=OmniDB;Database=finlytic_assets;Username=admin;Password=${DB_PASSWORD} - - MQTT__Host=host.docker.internal - - MQTT__Port=4545 - #- MQTT__Username=admin + - ConnectionStrings__DefaultConnection=Host=${DB_HOST:-OmniDB};Port=${DB_PORT:-5432};Database=finlytic_assets;Username=admin;Password=${DB_PASSWORD} + - MQTT__Host=${MQTT_HOST:-host.docker.internal} + - MQTT__Port=${MQTT_PORT:-4545} + # Client-side MQTT Username/Password support exists (applied only when set), + # but the broker at MQTT_HOST:MQTT_PORT has no users/ACLs configured yet + # (verified — unauthenticated). Do NOT uncomment until broker-side users + # exist; doing so now would break the current anonymous connection. + # Once the broker has matching users, activate via: + #- MQTT__Username=${MQTT_USERNAME:-admin} #- MQTT__Password=${MQTT_PASSWORD} - MQTT__ClientId=finlytic_assets volumes: - - C:\Users\larsh\Documents\docker\finlytic\assets\index:/app/assets/index - - C:\Users\larsh\Documents\docker\finlytic\assets\logos:/app/assets/logos - + - ${FINLYTIC_DATA_ROOT:-C:/Users/larsh/Documents/docker/finlytic}/assets/index:/app/assets/index + - ${FINLYTIC_DATA_ROOT:-C:/Users/larsh/Documents/docker/finlytic}/assets/logos:/app/assets/logos + finlyticnews: image: finlyticnews build: context: . dockerfile: FinlyticNews/Dockerfile + restart: unless-stopped networks: - postgres-network environment: - - ConnectionStrings__DefaultConnection=Host=OmniDB;Database=finlytic_news;Username=admin;Password=${DB_PASSWORD} - - MQTT__Host=host.docker.internal - - MQTT__Port=4545 - #- MQTT__Username=admin + - ConnectionStrings__DefaultConnection=Host=${DB_HOST:-OmniDB};Port=${DB_PORT:-5432};Database=finlytic_news;Username=admin;Password=${DB_PASSWORD} + - MQTT__Host=${MQTT_HOST:-host.docker.internal} + - MQTT__Port=${MQTT_PORT:-4545} + # See finlyticassets above: broker has no auth configured yet, do not enable. + #- MQTT__Username=${MQTT_USERNAME:-admin} #- MQTT__Password=${MQTT_PASSWORD} - MQTT__ClientId=finlytic_news - - N8N__ArticleExtractionUrl=${ARTICLE_EXTRACTION_URL} + # No data/summaries mount: the legacy filesystem read of Sentiment's + # summary cache was removed and replaced by an MQTT-RPC call to + # FinlyticSentiment (verified — no code path reads that directory + # anymore). Only the asset index mount remains. volumes: - - C:\Users\larsh\Documents\docker\finlytic\assets\index:/app/assets/index:ro - - C:\Users\larsh\Documents\docker\finlytic\data\summaries:/app/data/summaries:ro - + - ${FINLYTIC_DATA_ROOT:-C:/Users/larsh/Documents/docker/finlytic}/assets/index:/app/assets/index:ro finlyticfundamentals: image: finlyticfundamentals build: context: . dockerfile: FinlyticFundamentals/Dockerfile + restart: unless-stopped networks: - postgres-network environment: - - ConnectionStrings__DefaultConnection=Host=OmniDB;Database=finlytic_fundamentals;Username=admin;Password=${DB_PASSWORD} - - MQTT__Host=host.docker.internal - - MQTT__Port=4545 + - ConnectionStrings__DefaultConnection=Host=${DB_HOST:-OmniDB};Port=${DB_PORT:-5432};Database=finlytic_fundamentals;Username=admin;Password=${DB_PASSWORD} + - MQTT__Host=${MQTT_HOST:-host.docker.internal} + - MQTT__Port=${MQTT_PORT:-4545} + # See finlyticassets above: broker has no auth configured yet, do not enable. + #- MQTT__Username=${MQTT_USERNAME:-admin} + #- MQTT__Password=${MQTT_PASSWORD} - MQTT__ClientId=finlytic_fundamentals finlyticsentiment: @@ -55,76 +131,95 @@ services: build: context: . dockerfile: FinlyticSentiment/Dockerfile + restart: unless-stopped networks: - postgres-network environment: - - ConnectionStrings__DefaultConnection=Host=OmniDB;Database=finlytic_sentimental;Username=admin;Password=${DB_PASSWORD} - - MQTT__Host=host.docker.internal - - MQTT__Port=4545 + - ConnectionStrings__DefaultConnection=Host=${DB_HOST:-OmniDB};Port=${DB_PORT:-5432};Database=finlytic_sentimental;Username=admin;Password=${DB_PASSWORD} + - MQTT__Host=${MQTT_HOST:-host.docker.internal} + - MQTT__Port=${MQTT_PORT:-4545} + # See finlyticassets above: broker has no auth configured yet, do not enable. + #- MQTT__Username=${MQTT_USERNAME:-admin} + #- MQTT__Password=${MQTT_PASSWORD} - MQTT__ClientId=finlytic_sentiment - Webhooks__German=https://n8n.kleidukos.me/webhook/sentiment/de - Webhooks__English=https://n8n.kleidukos.me/webhook/sentiment/en - volumes: - - C:\Users\larsh\Documents\docker\finlytic\data\summaries:/app/data/summaries + # No volumes: FinlyticSentiment persists to PostgreSQL only. The + # data/summaries mount here was never written to by this service + # (Storage:SummariesPath in FinlyticSentiment/appsettings.json is dead + # config) and other services now consume Sentiment's data via MQTT-RPC + # instead of shared files, so the mount has been dropped. - finlytictechnicalanalysis: - image: finlytictechnicalanalysis + finlytictechnicals: + image: finlytictechnicals build: context: . - dockerfile: FinlyticTechnicalAnalysis/Dockerfile + dockerfile: FinlyticTechnicals/Dockerfile + restart: unless-stopped networks: - postgres-network environment: - - ConnectionStrings__DefaultConnection=Host=OmniDB;Database=finlytic_ta;Username=admin;Password=${DB_PASSWORD} - - MQTT__Host=host.docker.internal - - MQTT__Port=4545 - - MQTT__ClientId=finlytic_ta + - ConnectionStrings__DefaultConnection=Host=${DB_HOST:-OmniDB};Port=${DB_PORT:-5432};Database=finlytic_ta;Username=admin;Password=${DB_PASSWORD} + - MQTT__Host=${MQTT_HOST:-host.docker.internal} + - MQTT__Port=${MQTT_PORT:-4545} + # See finlyticassets above: broker has no auth configured yet, do not enable. + #- MQTT__Username=${MQTT_USERNAME:-admin} + #- MQTT__Password=${MQTT_PASSWORD} + - MQTT__ClientId=finlytic_technicals - finlyticanalyzer: - image: finlyticanalyzer + finlyticengine: + image: finlyticengine build: context: . - dockerfile: FinlyticAnalyzer/Dockerfile + dockerfile: FinlyticEngine/Dockerfile + restart: unless-stopped networks: - postgres-network environment: - - ConnectionStrings__DefaultConnection=Host=OmniDB;Database=finlytic_analyzer;Username=admin;Password=${DB_PASSWORD} - - MQTT__Host=host.docker.internal - - MQTT__Port=4545 - - MQTT__ClientId=finlytic_analyzer - - N8N__WebhookUrl=https://n8n.kleidukos.me/webhook/gemini/analysis/auto - volumes: - - C:\Users\larsh\Documents\docker\finlytic\data\feedback:/app/data/feedback:ro + - ConnectionStrings__DefaultConnection=Host=${DB_HOST:-OmniDB};Port=${DB_PORT:-5432};Database=finlytic_engine;Username=admin;Password=${DB_PASSWORD} + - MQTT__Host=${MQTT_HOST:-host.docker.internal} + - MQTT__Port=${MQTT_PORT:-4545} + # See finlyticassets above: broker has no auth configured yet, do not enable. + #- MQTT__Username=${MQTT_USERNAME:-admin} + #- MQTT__Password=${MQTT_PASSWORD} + - MQTT__ClientId=finlytic_engine + - Ai__N8nValidationWebhookUrl=https://n8n.kleidukos.me/webhook/trade-validation - finlytictrades: - image: finlytictrades + finlyticsimulation: + image: finlyticsimulation build: context: . - dockerfile: FinlyticTrades/Dockerfile + dockerfile: FinlyticSimulation/Dockerfile + restart: unless-stopped networks: - postgres-network environment: - - ConnectionStrings__DefaultConnection=Host=OmniDB;Database=finlytic_trades;Username=admin;Password=${DB_PASSWORD} - - MQTT__Host=host.docker.internal - - MQTT__Port=4545 - - MQTT__ClientId=finlytic_trades - volumes: - - C:\Users\larsh\Documents\docker\finlytic\data\feedback:/app/data/feedback + - ConnectionStrings__DefaultConnection=Host=${DB_HOST:-OmniDB};Port=${DB_PORT:-5432};Database=finlytic_simulation;Username=admin;Password=${DB_PASSWORD} + - MQTT__Host=${MQTT_HOST:-host.docker.internal} + - MQTT__Port=${MQTT_PORT:-4545} + # See finlyticassets above: broker has no auth configured yet, do not enable. + #- MQTT__Username=${MQTT_USERNAME:-admin} + #- MQTT__Password=${MQTT_PASSWORD} + - MQTT__ClientId=finlytic_simulation finlyticbot: image: finlyticbot build: context: . dockerfile: FinlyticBot/Dockerfile + restart: unless-stopped networks: - postgres-network environment: - - ConnectionStrings__DefaultConnection=Host=OmniDB;Database=finlytic_bot;Username=admin;Password=${DB_PASSWORD} - - MQTT__Host=host.docker.internal - - MQTT__Port=4545 + - ConnectionStrings__DefaultConnection=Host=${DB_HOST:-OmniDB};Port=${DB_PORT:-5432};Database=finlytic_bot;Username=admin;Password=${DB_PASSWORD} + - MQTT__Host=${MQTT_HOST:-host.docker.internal} + - MQTT__Port=${MQTT_PORT:-4545} + # See finlyticassets above: broker has no auth configured yet, do not enable. + #- MQTT__Username=${MQTT_USERNAME:-admin} + #- MQTT__Password=${MQTT_PASSWORD} - MQTT__ClientId=finlytic_bot - - Alpaca__KeyId=${ALPACA_KEY_ID:-} - - Alpaca__SecretKey=${ALPACA_SECRET_KEY:-} + - Alpaca__KeyId=${ALPACA_KEY_ID:-PK_PAPER_PLACEHOLDER_KEY} + - Alpaca__SecretKey=${ALPACA_SECRET_KEY:-SK_PAPER_PLACEHOLDER_SECRET} - Alpaca__IsPaper=true finlyticbackend: @@ -132,22 +227,82 @@ services: build: context: . dockerfile: FinlyticBackend/Dockerfile + # on-failure (bounded), NOT unless-stopped: this is the one service that + # deliberately throws at startup if JWT_SECRET_KEY / ADMIN_DEFAULT_PASSWORD + # are missing or too weak (see Program.cs fail-fast guards). An + # unless-stopped policy would crash-loop that misconfiguration forever, + # burning CPU/log volume while masking the real problem. A bounded + # on-failure still recovers from transient startup races (e.g. DB not + # yet reachable) but eventually settles into a visibly "Exited" container + # (`docker compose ps`) once the retries are exhausted, surfacing a + # persistent config error instead of hiding it. + restart: on-failure:5 ports: - "5000:8080" networks: - postgres-network environment: - - ConnectionStrings__DefaultConnection=Host=OmniDB;Database=finlytic_backend;Username=admin;Password=${DB_PASSWORD} - - MQTT__Host=host.docker.internal - - MQTT__Port=4545 + - ConnectionStrings__DefaultConnection=Host=${DB_HOST:-OmniDB};Port=${DB_PORT:-5432};Database=finlytic_backend;Username=admin;Password=${DB_PASSWORD} + - MQTT__Host=${MQTT_HOST:-host.docker.internal} + - MQTT__Port=${MQTT_PORT:-4545} + # See finlyticassets above: broker has no auth configured yet, do not enable. + #- MQTT__Username=${MQTT_USERNAME:-admin} + #- MQTT__Password=${MQTT_PASSWORD} - MQTT__ClientId=finlytic_backend - - JWT__SecretKey=${JWT_SECRET_KEY:-FinlyticEnterpriseUltraSecureJwtSecretKey_2026_AtLeast32Chars!} - - ADMIN__DefaultPassword=${ADMIN_DEFAULT_PASSWORD:-AdminDefaultPassword2026!} - - Services__TradesServiceUrl=http://finlytictrades:8080/api/v1/trades/active + - JWT__SecretKey=${JWT_SECRET_KEY} + - ADMIN__DefaultPassword=${ADMIN_DEFAULT_PASSWORD} volumes: - - C:\Users\larsh\Documents\docker\finlytic\assets\index:/app/assets/index - - C:\Users\larsh\Documents\docker\finlytic\assets\logos:/app/assets/logos + - ${FINLYTIC_DATA_ROOT:-C:/Users/larsh/Documents/docker/finlytic}/assets/index:/app/assets/index + - ${FINLYTIC_DATA_ROOT:-C:/Users/larsh/Documents/docker/finlytic}/assets/logos:/app/assets/logos + # Honest healthcheck: actually opens a TCP connection to the real Kestrel + # port and parses the real HTTP status line from the real GET /health + # endpoint (Program.cs, AllowAnonymous, no auth required). The final image + # (mcr.microsoft.com/dotnet/aspnet:10.0) has neither curl nor wget + # installed (verified) — installing one just for this would add an extra + # apt layer, so instead we use bash's built-in /dev/tcp (bash itself IS + # present in the base image, verified), invoked directly via exec form so + # it does not go through /bin/sh (which is dash on this image and does + # NOT support /dev/tcp). + healthcheck: + test: + - CMD + - bash + - -c + - >- + exec 3<>/dev/tcp/127.0.0.1/8080 && + printf 'GET /health HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n' >&3 && + head -n1 <&3 | grep -q '200' + interval: 30s + timeout: 5s + retries: 3 + start_period: 20s + + # ────────────────────────────────────────────────────────────────────────── + # No HEALTHCHECK on the 8 worker services above (finlyticassets, finlyticnews, + # finlyticfundamentals, finlyticsentiment, finlytictechnicals, finlyticengine, + # finlyticsimulation, finlyticbot) — and this is deliberate, not an omission: + # + # - They are Microsoft.NET.Sdk.Worker projects and MUST NOT host an HTTP + # server (Rules.md §5), so there is no `GET /health`-style endpoint to + # probe, by design. + # - Docker already restarts/reports a dead PID 1 via the `restart` policy + # above without any HEALTHCHECK — a HEALTHCHECK only adds value if it + # can distinguish "process alive but broken" from "process alive and + # working", which requires touching something specific to the app. + # - The only things reachable from inside these containers without an + # app-level probe endpoint are the external DB/MQTT dependencies + # themselves (e.g. via bash's /dev/tcp, as used for finlyticbackend + # above). But a bare TCP-reachability check to OmniDB/MQTT tests the + # network path, not the worker — it would report "healthy" while the + # worker is deadlocked, and "unhealthy" during a legitimate external + # outage the worker's own retry logic is already handling. That is + # placebo/misleading in both directions, not an honest signal. + # + # Conclusion: no meaningful, non-cosmetic healthcheck is possible here + # without adding an HTTP endpoint (forbidden by Rules.md §5). Leaving + # HEALTHCHECK unset is the honest choice. + # ────────────────────────────────────────────────────────────────────────── + networks: postgres-network: - external: true - + external: true diff --git a/rebuild-playwright-base.ps1 b/rebuild-playwright-base.ps1 index 52afadd..b9502db 100644 --- a/rebuild-playwright-base.ps1 +++ b/rebuild-playwright-base.ps1 @@ -1,9 +1,20 @@ # rebuild-playwright-base.ps1 # ───────────────────────────────────────────────────────────────────────────── -# Rebuild the Playwright base image for FinlyticNews. +# Build the shared Playwright base image (Chromium + Node + Playwright CLI). # -# Run this script ONLY when you update the Playwright NuGet package version. -# After running this, a normal `docker compose build` will be fast again. +# CONSUMERS — both reference this image as `FROM finlytic-playwright-base:`: +# * FinlyticNews/Dockerfile +# * FinlyticFundamentals/Dockerfile +# +# This image is NOT built by `docker compose build` (Compose does not resolve +# FROM-references between services). It must exist locally BEFORE building +# those two services, otherwise their build fails with "pull access denied". +# +# Equivalent Compose route (same image tag, declared in compose.yaml): +# docker compose --profile build-base build finlytic-playwright-base +# +# Run this ONLY when the Microsoft.Playwright NuGet version changes +# (see FinlyticCore/FinlyticCore.csproj) — otherwise the cached layer is reused. # # Usage: # .\rebuild-playwright-base.ps1 @@ -34,6 +45,7 @@ docker build ` if ($LASTEXITCODE -eq 0) { Write-Host "" Write-Host "✅ Base image '$ImageName' built and cached locally." -ForegroundColor Green + Write-Host " Consumers: FinlyticNews, FinlyticFundamentals" -ForegroundColor Green Write-Host " You can now run 'docker compose build' as usual." -ForegroundColor Green Write-Host "" diff --git a/update.py b/update.py deleted file mode 100644 index fcb849f..0000000 --- a/update.py +++ /dev/null @@ -1,29 +0,0 @@ -import re - -file_path = r"E:\Projects\Finlytic\FinlyticCore\Dtos\Fundamentals\AssetFundamentalsDto.cs" -with open(file_path, "r", encoding="utf-8") as f: - lines = f.readlines() - -out_lines = [] -has_using = any("using System.Text.Json.Serialization;" in l for l in lines) -if not has_using: - for i, line in enumerate(lines): - if "using System;" in line: - out_lines.append(line) - out_lines.append("using System.Text.Json.Serialization;\n") - lines = lines[i+1:] - break - -for i, line in enumerate(lines): - match = re.search(r'^(\s*)public (.+?) ([A-Z][a-zA-Z0-9_]*)( \{.*)$', line) - if match and " record " not in line and " class " not in line: - # Check if previous line has JsonPropertyName - if i == 0 or "JsonPropertyName" not in lines[i-1]: - indent = match.group(1) - prop_name = match.group(3) - camel_name = prop_name[0].lower() + prop_name[1:] - out_lines.append(f'{indent}[JsonPropertyName("{camel_name}")]\n') - out_lines.append(line) - -with open(file_path, "w", encoding="utf-8") as f: - f.writelines(out_lines)