chore: remove deprecated microservices, update solution and docker compose configurations

This commit is contained in:
2026-08-24 21:37:57 +02:00
parent 0894c40f07
commit 2b7d59f40d
96 changed files with 455 additions and 13121 deletions
+13
View File
@@ -23,3 +23,16 @@
**/values.dev.yaml
LICENSE
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/
+127
View File
@@ -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 ""
+80 -26
View File
@@ -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
@@ -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<ManualAnalysisController> _finlyticLogger;
public ManualAnalysisController(
IVixTrackerService vixTracker,
IN8nEvaluationService n8nService,
IWinRateCalculator winRateCalculator,
AnalyzerDbContext dbContext,
IFinlyticLogger<ManualAnalysisController> finlyticLogger)
{
_vixTracker = vixTracker;
_n8nService = n8nService;
_winRateCalculator = winRateCalculator;
_dbContext = dbContext;
_finlyticLogger = finlyticLogger;
}
/// <summary>
/// Runs a manual analysis based on the provided request.
/// </summary>
[HttpPost("manual")]
public async Task<IActionResult> RunManualAnalysis([FromBody] ManualAnalysisRequest request, CancellationToken cancellationToken)
{
if (string.IsNullOrWhiteSpace(request.Symbol) && string.IsNullOrWhiteSpace(request.Isin))
{
return BadRequest(new { error = "Symbol or ISIN is required." });
}
var regime = _vixTracker.GetCurrentRegime();
var currentVix = _vixTracker.GetCurrentVix();
string analysisId = Guid.NewGuid().ToString("N");
double winRate = _winRateCalculator.CalculateWinRate(request.Sector, request.Symbol, regime);
string riskLabel = request.RiskScore > 70 ? $"Aggressiv ({request.RiskScore}/100)" : (request.RiskScore > 30 ? $"Balanced ({request.RiskScore}/100)" : $"Konservativ ({request.RiskScore}/100)");
string timeframeFormatted = $"{request.MinTimeframeValue}-{request.MaxTimeframeValue} {request.TimeframeUnit}";
var n8nRequest = new N8nAnalysisRequestDto
{
RequestId = analysisId,
Timestamp = DateTime.UtcNow,
TriggerType = "Manual",
TargetAsset = new TargetAssetInfo
{
Symbol = request.Symbol.ToUpperInvariant(),
Isin = request.Isin.ToUpperInvariant(),
Sector = request.Sector
},
MarketContext = new MarketContextInfo
{
Vix = currentVix,
MarketRegime = regime.ToString()
},
FilterContext = new FilterContextInfo
{
ImpactScore = 1.0,
RawNewsHeadline = string.IsNullOrWhiteSpace(request.Headline) ? "Manual User Trigger" : request.Headline
},
UserPreferences = new UserPreferencesInfo
{
RiskScore = request.RiskScore,
RiskTolerance = riskLabel,
MinTimeframeValue = request.MinTimeframeValue,
MaxTimeframeValue = request.MaxTimeframeValue,
TimeframeUnit = request.TimeframeUnit,
TimeframeFormatted = timeframeFormatted,
InstrumentType = request.InstrumentType,
UserNotes = request.UserNotes
},
TradeFeedback = new TradeFeedbackInfo
{
TotalAssetTrades = 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
});
}
}
@@ -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<AnalyzerDbContext> options) : base(options) { }
public DbSet<SettingEntity> DynamicSettings => Set<SettingEntity>();
public DbSet<AnalysisEntity> Analyses => Set<AnalysisEntity>();
public DbSet<AnalyzerSettingsEntity> Settings => Set<AnalyzerSettingsEntity>();
public DbSet<TradeProposalEntity> TradeProposals => Set<TradeProposalEntity>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<SettingEntity>(entity =>
{
entity.HasKey(e => e.Id);
entity.HasIndex(e => e.Key).IsUnique();
});
modelBuilder.Entity<AnalysisEntity>(entity =>
{
entity.HasIndex(e => e.AnalysisId).IsUnique();
entity.HasIndex(e => e.EventId);
entity.HasIndex(e => e.Isin);
entity.HasIndex(e => e.Sector);
entity.HasIndex(e => e.CreatedAt);
});
modelBuilder.Entity<TradeProposalEntity>(entity =>
{
entity.HasIndex(e => e.Isin);
entity.HasIndex(e => e.ExpiresAt);
});
}
}
public class AnalyzerDbContextFactory : IDesignTimeDbContextFactory<AnalyzerDbContext>
{
public AnalyzerDbContext CreateDbContext(string[] args)
{
var optionsBuilder = new DbContextOptionsBuilder<AnalyzerDbContext>();
optionsBuilder.UseNpgsql("Host=localhost;Database=analyzer;Username=postgres;Password=postgres");
return new AnalyzerDbContext(optionsBuilder.Options);
}
}
-16
View File
@@ -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"]
@@ -1,60 +0,0 @@
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using FinlyticCore.Models.Analyzer;
namespace FinlyticAnalyzer.Entities;
/// <summary>
/// Persisted raw news, market context, AI prompt payload & response in PostgreSQL.
/// </summary>
[Table("analyses")]
public class AnalysisEntity
{
[Key]
public Guid Id { get; set; } = Guid.NewGuid();
[Required]
[MaxLength(100)]
public string AnalysisId { get; set; } = string.Empty;
[Required]
[MaxLength(100)]
public string EventId { get; set; } = string.Empty;
[Required]
[MaxLength(50)]
public string Sector { get; set; } = string.Empty;
[Required]
[MaxLength(30)]
public string Symbol { get; set; } = string.Empty;
[Required]
[MaxLength(30)]
public string Isin { get; set; } = string.Empty;
public VixMarketRegime VixRegime { get; set; }
public decimal VixValue { get; set; }
public double ImpactScore { get; set; }
public double WinRate { get; set; }
[Column(TypeName = "jsonb")]
public string RawDataJson { get; set; } = "{}";
[Column(TypeName = "jsonb")]
public string AiOutputJson { get; set; } = "{}";
[Column(TypeName = "jsonb")]
public string N8nResponseJson { get; set; } = "{}";
public double N8nEvalScore { get; set; }
[MaxLength(30)]
public string N8nDecision { get; set; } = string.Empty;
public bool IsTradeProposed { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
}
@@ -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;
}
@@ -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;
/// <summary>
/// KI-Entscheidung ("BUY", "SELL", "HOLD", "REJECTED")
/// </summary>
[MaxLength(20)]
public string ProposedAction { get; set; } = "BUY";
public double ConfidenceScore { get; set; }
// --- KI Execution Plan (Vorgeschlagene Preismarken) ---
[Column(TypeName = "decimal(18,4)")]
public decimal EntryPrice { get; set; }
[Column(TypeName = "decimal(18,4)")]
public decimal StopLoss { get; set; }
[Column(TypeName = "decimal(18,4)")]
public decimal TakeProfit { get; set; }
[Column(TypeName = "decimal(18,4)")]
public decimal? EntryZoneMin { get; set; }
[Column(TypeName = "decimal(18,4)")]
public decimal? EntryZoneMax { get; set; }
public string? TakeProfitTargets { get; set; } // Comma-separated or JSON
[Column(TypeName = "decimal(18,4)")]
public decimal? RiskRewardRatio { get; set; }
[Column(TypeName = "decimal(18,4)")]
public decimal? MaxLeverage { get; set; }
// --- Kontext aus Request & KI ---
public string ReasonSummary { get; set; } = string.Empty;
public string TechnicalRationale { get; set; } = string.Empty;
public string FundamentalRationale { get; set; } = string.Empty;
public string RiskWarning { get; set; } = string.Empty;
[MaxLength(30)]
public string RiskTolerance { get; set; } = "Balanced";
[MaxLength(20)]
public string Timeframe { get; set; } = "1-7 Tage";
[MaxLength(30)]
public string InstrumentType { get; set; } = "KnockOut";
public VixMarketRegime VixRegime { get; set; }
[Column(TypeName = "decimal(18,4)")]
public decimal VixValue { get; set; }
public double WinRate { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime ExpiresAt { get; set; } = DateTime.UtcNow.AddHours(3);
}
-26
View File
@@ -1,26 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.9" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.9">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.9" />
<PackageReference Include="Microsoft.Extensions.Http" Version="10.0.9" />
<PackageReference Include="MQTTnet" Version="5.1.0.1559" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.2" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\FinlyticCore\FinlyticCore.csproj" />
</ItemGroup>
</Project>
@@ -1,136 +0,0 @@
// <auto-generated />
using System;
using FinlyticAnalyzer.Database;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace FinlyticAnalyzer.Migrations
{
[DbContext(typeof(AnalyzerDbContext))]
[Migration("20260801073402_Init")]
partial class Init
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "10.0.9")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("FinlyticAnalyzer.Entities.AnalysisEntity", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("AiOutputJson")
.IsRequired()
.HasColumnType("jsonb");
b.Property<string>("AnalysisId")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("EventId")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<double>("ImpactScore")
.HasColumnType("double precision");
b.Property<bool>("IsTradeProposed")
.HasColumnType("boolean");
b.Property<string>("Isin")
.IsRequired()
.HasMaxLength(30)
.HasColumnType("character varying(30)");
b.Property<string>("N8nDecision")
.IsRequired()
.HasMaxLength(30)
.HasColumnType("character varying(30)");
b.Property<double>("N8nEvalScore")
.HasColumnType("double precision");
b.Property<string>("N8nResponseJson")
.IsRequired()
.HasColumnType("jsonb");
b.Property<string>("RawDataJson")
.IsRequired()
.HasColumnType("jsonb");
b.Property<string>("Sector")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<string>("Symbol")
.IsRequired()
.HasMaxLength(30)
.HasColumnType("character varying(30)");
b.Property<int>("VixRegime")
.HasColumnType("integer");
b.Property<decimal>("VixValue")
.HasColumnType("numeric");
b.Property<double>("WinRate")
.HasColumnType("double precision");
b.HasKey("Id");
b.HasIndex("AnalysisId")
.IsUnique();
b.HasIndex("CreatedAt");
b.HasIndex("EventId");
b.HasIndex("Isin");
b.HasIndex("Sector");
b.ToTable("analyses");
});
modelBuilder.Entity("FinlyticAnalyzer.Entities.AnalyzerSettingsEntity", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<double>("MinSignalScore")
.HasColumnType("double precision");
b.Property<string>("ScanCronSchedule")
.IsRequired()
.HasColumnType("text");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.ToTable("Settings");
});
#pragma warning restore 612, 618
}
}
}
@@ -1,92 +0,0 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace FinlyticAnalyzer.Migrations
{
/// <inheritdoc />
public partial class Init : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "analyses",
columns: table => new
{
Id = table.Column<Guid>(type: "uuid", nullable: false),
AnalysisId = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false),
EventId = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false),
Sector = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: false),
Symbol = table.Column<string>(type: "character varying(30)", maxLength: 30, nullable: false),
Isin = table.Column<string>(type: "character varying(30)", maxLength: 30, nullable: false),
VixRegime = table.Column<int>(type: "integer", nullable: false),
VixValue = table.Column<decimal>(type: "numeric", nullable: false),
ImpactScore = table.Column<double>(type: "double precision", nullable: false),
WinRate = table.Column<double>(type: "double precision", nullable: false),
RawDataJson = table.Column<string>(type: "jsonb", nullable: false),
AiOutputJson = table.Column<string>(type: "jsonb", nullable: false),
N8nResponseJson = table.Column<string>(type: "jsonb", nullable: false),
N8nEvalScore = table.Column<double>(type: "double precision", nullable: false),
N8nDecision = table.Column<string>(type: "character varying(30)", maxLength: 30, nullable: false),
IsTradeProposed = table.Column<bool>(type: "boolean", nullable: false),
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_analyses", x => x.Id);
});
migrationBuilder.CreateTable(
name: "Settings",
columns: table => new
{
Id = table.Column<Guid>(type: "uuid", nullable: false),
ScanCronSchedule = table.Column<string>(type: "text", nullable: false),
MinSignalScore = table.Column<double>(type: "double precision", nullable: false),
UpdatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Settings", x => x.Id);
});
migrationBuilder.CreateIndex(
name: "IX_analyses_AnalysisId",
table: "analyses",
column: "AnalysisId",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_analyses_CreatedAt",
table: "analyses",
column: "CreatedAt");
migrationBuilder.CreateIndex(
name: "IX_analyses_EventId",
table: "analyses",
column: "EventId");
migrationBuilder.CreateIndex(
name: "IX_analyses_Isin",
table: "analyses",
column: "Isin");
migrationBuilder.CreateIndex(
name: "IX_analyses_Sector",
table: "analyses",
column: "Sector");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "analyses");
migrationBuilder.DropTable(
name: "Settings");
}
}
}
@@ -1,151 +0,0 @@
// <auto-generated />
using System;
using FinlyticAnalyzer.Database;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace FinlyticAnalyzer.Migrations
{
[DbContext(typeof(AnalyzerDbContext))]
[Migration("20260803185020_AddLogFilterSettings")]
partial class AddLogFilterSettings
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "10.0.9")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("FinlyticAnalyzer.Entities.AnalysisEntity", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("AiOutputJson")
.IsRequired()
.HasColumnType("jsonb");
b.Property<string>("AnalysisId")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("EventId")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<double>("ImpactScore")
.HasColumnType("double precision");
b.Property<bool>("IsTradeProposed")
.HasColumnType("boolean");
b.Property<string>("Isin")
.IsRequired()
.HasMaxLength(30)
.HasColumnType("character varying(30)");
b.Property<string>("N8nDecision")
.IsRequired()
.HasMaxLength(30)
.HasColumnType("character varying(30)");
b.Property<double>("N8nEvalScore")
.HasColumnType("double precision");
b.Property<string>("N8nResponseJson")
.IsRequired()
.HasColumnType("jsonb");
b.Property<string>("RawDataJson")
.IsRequired()
.HasColumnType("jsonb");
b.Property<string>("Sector")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<string>("Symbol")
.IsRequired()
.HasMaxLength(30)
.HasColumnType("character varying(30)");
b.Property<int>("VixRegime")
.HasColumnType("integer");
b.Property<decimal>("VixValue")
.HasColumnType("numeric");
b.Property<double>("WinRate")
.HasColumnType("double precision");
b.HasKey("Id");
b.HasIndex("AnalysisId")
.IsUnique();
b.HasIndex("CreatedAt");
b.HasIndex("EventId");
b.HasIndex("Isin");
b.HasIndex("Sector");
b.ToTable("analyses");
});
modelBuilder.Entity("FinlyticAnalyzer.Entities.AnalyzerSettingsEntity", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<bool>("EnableLogAnalyzerAuto")
.HasColumnType("boolean");
b.Property<bool>("EnableLogAnalyzerManual")
.HasColumnType("boolean");
b.Property<bool>("EnableLogDatabaseOps")
.HasColumnType("boolean");
b.Property<bool>("EnableLogMqttGeneral")
.HasColumnType("boolean");
b.Property<bool>("EnableLogMqttHealthPing")
.HasColumnType("boolean");
b.Property<double>("MinSignalScore")
.HasColumnType("double precision");
b.Property<string>("ScanCronSchedule")
.IsRequired()
.HasColumnType("text");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.ToTable("Settings");
});
#pragma warning restore 612, 618
}
}
}
@@ -1,73 +0,0 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace FinlyticAnalyzer.Migrations
{
/// <inheritdoc />
public partial class AddLogFilterSettings : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<bool>(
name: "EnableLogAnalyzerAuto",
table: "Settings",
type: "boolean",
nullable: false,
defaultValue: false);
migrationBuilder.AddColumn<bool>(
name: "EnableLogAnalyzerManual",
table: "Settings",
type: "boolean",
nullable: false,
defaultValue: false);
migrationBuilder.AddColumn<bool>(
name: "EnableLogDatabaseOps",
table: "Settings",
type: "boolean",
nullable: false,
defaultValue: false);
migrationBuilder.AddColumn<bool>(
name: "EnableLogMqttGeneral",
table: "Settings",
type: "boolean",
nullable: false,
defaultValue: false);
migrationBuilder.AddColumn<bool>(
name: "EnableLogMqttHealthPing",
table: "Settings",
type: "boolean",
nullable: false,
defaultValue: false);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "EnableLogAnalyzerAuto",
table: "Settings");
migrationBuilder.DropColumn(
name: "EnableLogAnalyzerManual",
table: "Settings");
migrationBuilder.DropColumn(
name: "EnableLogDatabaseOps",
table: "Settings");
migrationBuilder.DropColumn(
name: "EnableLogMqttGeneral",
table: "Settings");
migrationBuilder.DropColumn(
name: "EnableLogMqttHealthPing",
table: "Settings");
}
}
}
@@ -1,151 +0,0 @@
// <auto-generated />
using System;
using FinlyticAnalyzer.Database;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace FinlyticAnalyzer.Migrations
{
[DbContext(typeof(AnalyzerDbContext))]
[Migration("20260804184350_CheckPendingMigrations")]
partial class CheckPendingMigrations
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "10.0.9")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("FinlyticAnalyzer.Entities.AnalysisEntity", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("AiOutputJson")
.IsRequired()
.HasColumnType("jsonb");
b.Property<string>("AnalysisId")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("EventId")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<double>("ImpactScore")
.HasColumnType("double precision");
b.Property<bool>("IsTradeProposed")
.HasColumnType("boolean");
b.Property<string>("Isin")
.IsRequired()
.HasMaxLength(30)
.HasColumnType("character varying(30)");
b.Property<string>("N8nDecision")
.IsRequired()
.HasMaxLength(30)
.HasColumnType("character varying(30)");
b.Property<double>("N8nEvalScore")
.HasColumnType("double precision");
b.Property<string>("N8nResponseJson")
.IsRequired()
.HasColumnType("jsonb");
b.Property<string>("RawDataJson")
.IsRequired()
.HasColumnType("jsonb");
b.Property<string>("Sector")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<string>("Symbol")
.IsRequired()
.HasMaxLength(30)
.HasColumnType("character varying(30)");
b.Property<int>("VixRegime")
.HasColumnType("integer");
b.Property<decimal>("VixValue")
.HasColumnType("numeric");
b.Property<double>("WinRate")
.HasColumnType("double precision");
b.HasKey("Id");
b.HasIndex("AnalysisId")
.IsUnique();
b.HasIndex("CreatedAt");
b.HasIndex("EventId");
b.HasIndex("Isin");
b.HasIndex("Sector");
b.ToTable("analyses");
});
modelBuilder.Entity("FinlyticAnalyzer.Entities.AnalyzerSettingsEntity", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<bool>("EnableLogAnalyzerAuto")
.HasColumnType("boolean");
b.Property<bool>("EnableLogAnalyzerManual")
.HasColumnType("boolean");
b.Property<bool>("EnableLogDatabaseOps")
.HasColumnType("boolean");
b.Property<bool>("EnableLogMqttGeneral")
.HasColumnType("boolean");
b.Property<bool>("EnableLogMqttHealthPing")
.HasColumnType("boolean");
b.Property<double>("MinSignalScore")
.HasColumnType("double precision");
b.Property<string>("ScanCronSchedule")
.IsRequired()
.HasColumnType("text");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.ToTable("Settings");
});
#pragma warning restore 612, 618
}
}
}
@@ -1,22 +0,0 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace FinlyticAnalyzer.Migrations
{
/// <inheritdoc />
public partial class CheckPendingMigrations : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
}
}
}
@@ -1,194 +0,0 @@
// <auto-generated />
using System;
using FinlyticAnalyzer.Database;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace FinlyticAnalyzer.Migrations
{
[DbContext(typeof(AnalyzerDbContext))]
[Migration("20260805184638_AddTradeProposals")]
partial class AddTradeProposals
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "10.0.9")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("FinlyticAnalyzer.Entities.AnalysisEntity", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("AiOutputJson")
.IsRequired()
.HasColumnType("jsonb");
b.Property<string>("AnalysisId")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("EventId")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<double>("ImpactScore")
.HasColumnType("double precision");
b.Property<bool>("IsTradeProposed")
.HasColumnType("boolean");
b.Property<string>("Isin")
.IsRequired()
.HasMaxLength(30)
.HasColumnType("character varying(30)");
b.Property<string>("N8nDecision")
.IsRequired()
.HasMaxLength(30)
.HasColumnType("character varying(30)");
b.Property<double>("N8nEvalScore")
.HasColumnType("double precision");
b.Property<string>("N8nResponseJson")
.IsRequired()
.HasColumnType("jsonb");
b.Property<string>("RawDataJson")
.IsRequired()
.HasColumnType("jsonb");
b.Property<string>("Sector")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<string>("Symbol")
.IsRequired()
.HasMaxLength(30)
.HasColumnType("character varying(30)");
b.Property<int>("VixRegime")
.HasColumnType("integer");
b.Property<decimal>("VixValue")
.HasColumnType("numeric");
b.Property<double>("WinRate")
.HasColumnType("double precision");
b.HasKey("Id");
b.HasIndex("AnalysisId")
.IsUnique();
b.HasIndex("CreatedAt");
b.HasIndex("EventId");
b.HasIndex("Isin");
b.HasIndex("Sector");
b.ToTable("analyses");
});
modelBuilder.Entity("FinlyticAnalyzer.Entities.AnalyzerSettingsEntity", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<bool>("EnableLogAnalyzerAuto")
.HasColumnType("boolean");
b.Property<bool>("EnableLogAnalyzerManual")
.HasColumnType("boolean");
b.Property<bool>("EnableLogDatabaseOps")
.HasColumnType("boolean");
b.Property<bool>("EnableLogMqttGeneral")
.HasColumnType("boolean");
b.Property<bool>("EnableLogMqttHealthPing")
.HasColumnType("boolean");
b.Property<double>("MinSignalScore")
.HasColumnType("double precision");
b.Property<string>("ScanCronSchedule")
.IsRequired()
.HasColumnType("text");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.ToTable("Settings");
});
modelBuilder.Entity("FinlyticAnalyzer.Entities.TradeProposalEntity", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<double>("ConfidenceScore")
.HasColumnType("double precision");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<DateTime>("ExpiresAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Isin")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.Property<string>("ProposedAction")
.IsRequired()
.HasColumnType("text");
b.Property<string>("ReasonSummary")
.IsRequired()
.HasColumnType("text");
b.Property<int>("Type")
.HasColumnType("integer");
b.HasKey("Id");
b.HasIndex("ExpiresAt");
b.HasIndex("Isin");
b.ToTable("TradeProposals");
});
#pragma warning restore 612, 618
}
}
}
@@ -1,51 +0,0 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace FinlyticAnalyzer.Migrations
{
/// <inheritdoc />
public partial class AddTradeProposals : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "TradeProposals",
columns: table => new
{
Id = table.Column<Guid>(type: "uuid", nullable: false),
Isin = table.Column<string>(type: "text", nullable: false),
Name = table.Column<string>(type: "text", nullable: false),
Type = table.Column<int>(type: "integer", nullable: false),
ProposedAction = table.Column<string>(type: "text", nullable: false),
ConfidenceScore = table.Column<double>(type: "double precision", nullable: false),
ReasonSummary = table.Column<string>(type: "text", nullable: false),
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
ExpiresAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_TradeProposals", x => x.Id);
});
migrationBuilder.CreateIndex(
name: "IX_TradeProposals_ExpiresAt",
table: "TradeProposals",
column: "ExpiresAt");
migrationBuilder.CreateIndex(
name: "IX_TradeProposals_Isin",
table: "TradeProposals",
column: "Isin");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "TradeProposals");
}
}
}
@@ -1,277 +0,0 @@
// <auto-generated />
using System;
using FinlyticAnalyzer.Database;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace FinlyticAnalyzer.Migrations
{
[DbContext(typeof(AnalyzerDbContext))]
[Migration("20260813202556_CheckPendingAnalyzer")]
partial class CheckPendingAnalyzer
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "10.0.9")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("FinlyticAnalyzer.Entities.AnalysisEntity", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("AiOutputJson")
.IsRequired()
.HasColumnType("jsonb");
b.Property<string>("AnalysisId")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("EventId")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<double>("ImpactScore")
.HasColumnType("double precision");
b.Property<bool>("IsTradeProposed")
.HasColumnType("boolean");
b.Property<string>("Isin")
.IsRequired()
.HasMaxLength(30)
.HasColumnType("character varying(30)");
b.Property<string>("N8nDecision")
.IsRequired()
.HasMaxLength(30)
.HasColumnType("character varying(30)");
b.Property<double>("N8nEvalScore")
.HasColumnType("double precision");
b.Property<string>("N8nResponseJson")
.IsRequired()
.HasColumnType("jsonb");
b.Property<string>("RawDataJson")
.IsRequired()
.HasColumnType("jsonb");
b.Property<string>("Sector")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<string>("Symbol")
.IsRequired()
.HasMaxLength(30)
.HasColumnType("character varying(30)");
b.Property<int>("VixRegime")
.HasColumnType("integer");
b.Property<decimal>("VixValue")
.HasColumnType("numeric");
b.Property<double>("WinRate")
.HasColumnType("double precision");
b.HasKey("Id");
b.HasIndex("AnalysisId")
.IsUnique();
b.HasIndex("CreatedAt");
b.HasIndex("EventId");
b.HasIndex("Isin");
b.HasIndex("Sector");
b.ToTable("analyses");
});
modelBuilder.Entity("FinlyticAnalyzer.Entities.AnalyzerSettingsEntity", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<bool>("EnableLogAnalyzerAuto")
.HasColumnType("boolean");
b.Property<bool>("EnableLogAnalyzerManual")
.HasColumnType("boolean");
b.Property<bool>("EnableLogDatabaseOps")
.HasColumnType("boolean");
b.Property<bool>("EnableLogMqttGeneral")
.HasColumnType("boolean");
b.Property<bool>("EnableLogMqttHealthPing")
.HasColumnType("boolean");
b.Property<double>("MinSignalScore")
.HasColumnType("double precision");
b.Property<string>("ScanCronSchedule")
.IsRequired()
.HasColumnType("text");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.ToTable("Settings");
});
modelBuilder.Entity("FinlyticAnalyzer.Entities.TradeProposalEntity", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("AnalysisId")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<double>("ConfidenceScore")
.HasColumnType("double precision");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<decimal>("EntryPrice")
.HasColumnType("decimal(18,4)");
b.Property<decimal?>("EntryZoneMax")
.HasColumnType("decimal(18,4)");
b.Property<decimal?>("EntryZoneMin")
.HasColumnType("decimal(18,4)");
b.Property<string>("EventId")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<DateTime>("ExpiresAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("FundamentalRationale")
.IsRequired()
.HasColumnType("text");
b.Property<string>("InstrumentType")
.IsRequired()
.HasMaxLength(30)
.HasColumnType("character varying(30)");
b.Property<string>("Isin")
.IsRequired()
.HasMaxLength(30)
.HasColumnType("character varying(30)");
b.Property<decimal?>("MaxLeverage")
.HasColumnType("decimal(18,4)");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(150)
.HasColumnType("character varying(150)");
b.Property<string>("ProposedAction")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("character varying(20)");
b.Property<string>("ReasonSummary")
.IsRequired()
.HasColumnType("text");
b.Property<decimal?>("RiskRewardRatio")
.HasColumnType("decimal(18,4)");
b.Property<string>("RiskTolerance")
.IsRequired()
.HasMaxLength(30)
.HasColumnType("character varying(30)");
b.Property<string>("RiskWarning")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Sector")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<decimal>("StopLoss")
.HasColumnType("decimal(18,4)");
b.Property<string>("Symbol")
.IsRequired()
.HasMaxLength(30)
.HasColumnType("character varying(30)");
b.Property<decimal>("TakeProfit")
.HasColumnType("decimal(18,4)");
b.Property<string>("TakeProfitTargets")
.HasColumnType("text");
b.Property<string>("TechnicalRationale")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Timeframe")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("character varying(20)");
b.Property<int>("Type")
.HasColumnType("integer");
b.Property<int>("VixRegime")
.HasColumnType("integer");
b.Property<decimal>("VixValue")
.HasColumnType("decimal(18,4)");
b.Property<double>("WinRate")
.HasColumnType("double precision");
b.HasKey("Id");
b.HasIndex("ExpiresAt");
b.HasIndex("Isin");
b.ToTable("trade_proposals");
});
#pragma warning restore 612, 618
}
}
}
@@ -1,351 +0,0 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace FinlyticAnalyzer.Migrations
{
/// <inheritdoc />
public partial class CheckPendingAnalyzer : Migration
{
/// <inheritdoc />
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<string>(
name: "ProposedAction",
table: "trade_proposals",
type: "character varying(20)",
maxLength: 20,
nullable: false,
oldClrType: typeof(string),
oldType: "text");
migrationBuilder.AlterColumn<string>(
name: "Name",
table: "trade_proposals",
type: "character varying(150)",
maxLength: 150,
nullable: false,
oldClrType: typeof(string),
oldType: "text");
migrationBuilder.AlterColumn<string>(
name: "Isin",
table: "trade_proposals",
type: "character varying(30)",
maxLength: 30,
nullable: false,
oldClrType: typeof(string),
oldType: "text");
migrationBuilder.AddColumn<string>(
name: "AnalysisId",
table: "trade_proposals",
type: "character varying(100)",
maxLength: 100,
nullable: false,
defaultValue: "");
migrationBuilder.AddColumn<decimal>(
name: "EntryPrice",
table: "trade_proposals",
type: "numeric(18,4)",
nullable: false,
defaultValue: 0m);
migrationBuilder.AddColumn<decimal>(
name: "EntryZoneMax",
table: "trade_proposals",
type: "numeric(18,4)",
nullable: true);
migrationBuilder.AddColumn<decimal>(
name: "EntryZoneMin",
table: "trade_proposals",
type: "numeric(18,4)",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "EventId",
table: "trade_proposals",
type: "character varying(100)",
maxLength: 100,
nullable: false,
defaultValue: "");
migrationBuilder.AddColumn<string>(
name: "FundamentalRationale",
table: "trade_proposals",
type: "text",
nullable: false,
defaultValue: "");
migrationBuilder.AddColumn<string>(
name: "InstrumentType",
table: "trade_proposals",
type: "character varying(30)",
maxLength: 30,
nullable: false,
defaultValue: "");
migrationBuilder.AddColumn<decimal>(
name: "MaxLeverage",
table: "trade_proposals",
type: "numeric(18,4)",
nullable: true);
migrationBuilder.AddColumn<decimal>(
name: "RiskRewardRatio",
table: "trade_proposals",
type: "numeric(18,4)",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "RiskTolerance",
table: "trade_proposals",
type: "character varying(30)",
maxLength: 30,
nullable: false,
defaultValue: "");
migrationBuilder.AddColumn<string>(
name: "RiskWarning",
table: "trade_proposals",
type: "text",
nullable: false,
defaultValue: "");
migrationBuilder.AddColumn<string>(
name: "Sector",
table: "trade_proposals",
type: "character varying(50)",
maxLength: 50,
nullable: false,
defaultValue: "");
migrationBuilder.AddColumn<decimal>(
name: "StopLoss",
table: "trade_proposals",
type: "numeric(18,4)",
nullable: false,
defaultValue: 0m);
migrationBuilder.AddColumn<string>(
name: "Symbol",
table: "trade_proposals",
type: "character varying(30)",
maxLength: 30,
nullable: false,
defaultValue: "");
migrationBuilder.AddColumn<decimal>(
name: "TakeProfit",
table: "trade_proposals",
type: "numeric(18,4)",
nullable: false,
defaultValue: 0m);
migrationBuilder.AddColumn<string>(
name: "TakeProfitTargets",
table: "trade_proposals",
type: "text",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "TechnicalRationale",
table: "trade_proposals",
type: "text",
nullable: false,
defaultValue: "");
migrationBuilder.AddColumn<string>(
name: "Timeframe",
table: "trade_proposals",
type: "character varying(20)",
maxLength: 20,
nullable: false,
defaultValue: "");
migrationBuilder.AddColumn<int>(
name: "VixRegime",
table: "trade_proposals",
type: "integer",
nullable: false,
defaultValue: 0);
migrationBuilder.AddColumn<decimal>(
name: "VixValue",
table: "trade_proposals",
type: "numeric(18,4)",
nullable: false,
defaultValue: 0m);
migrationBuilder.AddColumn<double>(
name: "WinRate",
table: "trade_proposals",
type: "double precision",
nullable: false,
defaultValue: 0.0);
migrationBuilder.AddPrimaryKey(
name: "PK_trade_proposals",
table: "trade_proposals",
column: "Id");
}
/// <inheritdoc />
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<string>(
name: "ProposedAction",
table: "TradeProposals",
type: "text",
nullable: false,
oldClrType: typeof(string),
oldType: "character varying(20)",
oldMaxLength: 20);
migrationBuilder.AlterColumn<string>(
name: "Name",
table: "TradeProposals",
type: "text",
nullable: false,
oldClrType: typeof(string),
oldType: "character varying(150)",
oldMaxLength: 150);
migrationBuilder.AlterColumn<string>(
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");
}
}
}
@@ -1,308 +0,0 @@
// <auto-generated />
using System;
using FinlyticAnalyzer.Database;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace FinlyticAnalyzer.Migrations
{
[DbContext(typeof(AnalyzerDbContext))]
[Migration("20260815184017_AddDynamicSettings")]
partial class AddDynamicSettings
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "10.0.9")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("FinlyticAnalyzer.Entities.AnalysisEntity", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("AiOutputJson")
.IsRequired()
.HasColumnType("jsonb");
b.Property<string>("AnalysisId")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("EventId")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<double>("ImpactScore")
.HasColumnType("double precision");
b.Property<bool>("IsTradeProposed")
.HasColumnType("boolean");
b.Property<string>("Isin")
.IsRequired()
.HasMaxLength(30)
.HasColumnType("character varying(30)");
b.Property<string>("N8nDecision")
.IsRequired()
.HasMaxLength(30)
.HasColumnType("character varying(30)");
b.Property<double>("N8nEvalScore")
.HasColumnType("double precision");
b.Property<string>("N8nResponseJson")
.IsRequired()
.HasColumnType("jsonb");
b.Property<string>("RawDataJson")
.IsRequired()
.HasColumnType("jsonb");
b.Property<string>("Sector")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<string>("Symbol")
.IsRequired()
.HasMaxLength(30)
.HasColumnType("character varying(30)");
b.Property<int>("VixRegime")
.HasColumnType("integer");
b.Property<decimal>("VixValue")
.HasColumnType("numeric");
b.Property<double>("WinRate")
.HasColumnType("double precision");
b.HasKey("Id");
b.HasIndex("AnalysisId")
.IsUnique();
b.HasIndex("CreatedAt");
b.HasIndex("EventId");
b.HasIndex("Isin");
b.HasIndex("Sector");
b.ToTable("analyses");
});
modelBuilder.Entity("FinlyticAnalyzer.Entities.AnalyzerSettingsEntity", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<bool>("EnableLogAnalyzerAuto")
.HasColumnType("boolean");
b.Property<bool>("EnableLogAnalyzerManual")
.HasColumnType("boolean");
b.Property<bool>("EnableLogDatabaseOps")
.HasColumnType("boolean");
b.Property<bool>("EnableLogMqttGeneral")
.HasColumnType("boolean");
b.Property<bool>("EnableLogMqttHealthPing")
.HasColumnType("boolean");
b.Property<double>("MinSignalScore")
.HasColumnType("double precision");
b.Property<string>("ScanCronSchedule")
.IsRequired()
.HasColumnType("text");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.ToTable("Settings");
});
modelBuilder.Entity("FinlyticAnalyzer.Entities.TradeProposalEntity", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("AnalysisId")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<double>("ConfidenceScore")
.HasColumnType("double precision");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<decimal>("EntryPrice")
.HasColumnType("decimal(18,4)");
b.Property<decimal?>("EntryZoneMax")
.HasColumnType("decimal(18,4)");
b.Property<decimal?>("EntryZoneMin")
.HasColumnType("decimal(18,4)");
b.Property<string>("EventId")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<DateTime>("ExpiresAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("FundamentalRationale")
.IsRequired()
.HasColumnType("text");
b.Property<string>("InstrumentType")
.IsRequired()
.HasMaxLength(30)
.HasColumnType("character varying(30)");
b.Property<string>("Isin")
.IsRequired()
.HasMaxLength(30)
.HasColumnType("character varying(30)");
b.Property<decimal?>("MaxLeverage")
.HasColumnType("decimal(18,4)");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(150)
.HasColumnType("character varying(150)");
b.Property<string>("ProposedAction")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("character varying(20)");
b.Property<string>("ReasonSummary")
.IsRequired()
.HasColumnType("text");
b.Property<decimal?>("RiskRewardRatio")
.HasColumnType("decimal(18,4)");
b.Property<string>("RiskTolerance")
.IsRequired()
.HasMaxLength(30)
.HasColumnType("character varying(30)");
b.Property<string>("RiskWarning")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Sector")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<decimal>("StopLoss")
.HasColumnType("decimal(18,4)");
b.Property<string>("Symbol")
.IsRequired()
.HasMaxLength(30)
.HasColumnType("character varying(30)");
b.Property<decimal>("TakeProfit")
.HasColumnType("decimal(18,4)");
b.Property<string>("TakeProfitTargets")
.HasColumnType("text");
b.Property<string>("TechnicalRationale")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Timeframe")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("character varying(20)");
b.Property<int>("Type")
.HasColumnType("integer");
b.Property<int>("VixRegime")
.HasColumnType("integer");
b.Property<decimal>("VixValue")
.HasColumnType("decimal(18,4)");
b.Property<double>("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<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("Key")
.IsRequired()
.HasMaxLength(150)
.HasColumnType("character varying(150)");
b.Property<DateTime>("LastUpdatedUtc")
.HasColumnType("timestamp with time zone");
b.Property<string>("ServiceIdentifier")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<string>("ValueJson")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("Key")
.IsUnique();
b.ToTable("DynamicSettings");
});
#pragma warning restore 612, 618
}
}
}
@@ -1,43 +0,0 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace FinlyticAnalyzer.Migrations
{
/// <inheritdoc />
public partial class AddDynamicSettings : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "DynamicSettings",
columns: table => new
{
Id = table.Column<Guid>(type: "uuid", nullable: false),
Key = table.Column<string>(type: "character varying(150)", maxLength: 150, nullable: false),
ValueJson = table.Column<string>(type: "text", nullable: false),
ServiceIdentifier = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false),
LastUpdatedUtc = table.Column<DateTime>(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);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "DynamicSettings");
}
}
}
@@ -1,305 +0,0 @@
// <auto-generated />
using System;
using FinlyticAnalyzer.Database;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace FinlyticAnalyzer.Migrations
{
[DbContext(typeof(AnalyzerDbContext))]
partial class AnalyzerDbContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "10.0.9")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("FinlyticAnalyzer.Entities.AnalysisEntity", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("AiOutputJson")
.IsRequired()
.HasColumnType("jsonb");
b.Property<string>("AnalysisId")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("EventId")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<double>("ImpactScore")
.HasColumnType("double precision");
b.Property<bool>("IsTradeProposed")
.HasColumnType("boolean");
b.Property<string>("Isin")
.IsRequired()
.HasMaxLength(30)
.HasColumnType("character varying(30)");
b.Property<string>("N8nDecision")
.IsRequired()
.HasMaxLength(30)
.HasColumnType("character varying(30)");
b.Property<double>("N8nEvalScore")
.HasColumnType("double precision");
b.Property<string>("N8nResponseJson")
.IsRequired()
.HasColumnType("jsonb");
b.Property<string>("RawDataJson")
.IsRequired()
.HasColumnType("jsonb");
b.Property<string>("Sector")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<string>("Symbol")
.IsRequired()
.HasMaxLength(30)
.HasColumnType("character varying(30)");
b.Property<int>("VixRegime")
.HasColumnType("integer");
b.Property<decimal>("VixValue")
.HasColumnType("numeric");
b.Property<double>("WinRate")
.HasColumnType("double precision");
b.HasKey("Id");
b.HasIndex("AnalysisId")
.IsUnique();
b.HasIndex("CreatedAt");
b.HasIndex("EventId");
b.HasIndex("Isin");
b.HasIndex("Sector");
b.ToTable("analyses");
});
modelBuilder.Entity("FinlyticAnalyzer.Entities.AnalyzerSettingsEntity", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<bool>("EnableLogAnalyzerAuto")
.HasColumnType("boolean");
b.Property<bool>("EnableLogAnalyzerManual")
.HasColumnType("boolean");
b.Property<bool>("EnableLogDatabaseOps")
.HasColumnType("boolean");
b.Property<bool>("EnableLogMqttGeneral")
.HasColumnType("boolean");
b.Property<bool>("EnableLogMqttHealthPing")
.HasColumnType("boolean");
b.Property<double>("MinSignalScore")
.HasColumnType("double precision");
b.Property<string>("ScanCronSchedule")
.IsRequired()
.HasColumnType("text");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.ToTable("Settings");
});
modelBuilder.Entity("FinlyticAnalyzer.Entities.TradeProposalEntity", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("AnalysisId")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<double>("ConfidenceScore")
.HasColumnType("double precision");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<decimal>("EntryPrice")
.HasColumnType("decimal(18,4)");
b.Property<decimal?>("EntryZoneMax")
.HasColumnType("decimal(18,4)");
b.Property<decimal?>("EntryZoneMin")
.HasColumnType("decimal(18,4)");
b.Property<string>("EventId")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<DateTime>("ExpiresAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("FundamentalRationale")
.IsRequired()
.HasColumnType("text");
b.Property<string>("InstrumentType")
.IsRequired()
.HasMaxLength(30)
.HasColumnType("character varying(30)");
b.Property<string>("Isin")
.IsRequired()
.HasMaxLength(30)
.HasColumnType("character varying(30)");
b.Property<decimal?>("MaxLeverage")
.HasColumnType("decimal(18,4)");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(150)
.HasColumnType("character varying(150)");
b.Property<string>("ProposedAction")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("character varying(20)");
b.Property<string>("ReasonSummary")
.IsRequired()
.HasColumnType("text");
b.Property<decimal?>("RiskRewardRatio")
.HasColumnType("decimal(18,4)");
b.Property<string>("RiskTolerance")
.IsRequired()
.HasMaxLength(30)
.HasColumnType("character varying(30)");
b.Property<string>("RiskWarning")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Sector")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<decimal>("StopLoss")
.HasColumnType("decimal(18,4)");
b.Property<string>("Symbol")
.IsRequired()
.HasMaxLength(30)
.HasColumnType("character varying(30)");
b.Property<decimal>("TakeProfit")
.HasColumnType("decimal(18,4)");
b.Property<string>("TakeProfitTargets")
.HasColumnType("text");
b.Property<string>("TechnicalRationale")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Timeframe")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("character varying(20)");
b.Property<int>("Type")
.HasColumnType("integer");
b.Property<int>("VixRegime")
.HasColumnType("integer");
b.Property<decimal>("VixValue")
.HasColumnType("decimal(18,4)");
b.Property<double>("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<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("Key")
.IsRequired()
.HasMaxLength(150)
.HasColumnType("character varying(150)");
b.Property<DateTime>("LastUpdatedUtc")
.HasColumnType("timestamp with time zone");
b.Property<string>("ServiceIdentifier")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<string>("ValueJson")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("Key")
.IsUnique();
b.ToTable("DynamicSettings");
});
#pragma warning restore 612, 618
}
}
}
-65
View File
@@ -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<AnalyzerDbContext>(options =>
options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection")));
builder.Services.AddScoped<ISettingsDbContext>(sp => sp.GetRequiredService<AnalyzerDbContext>());
// Register Core Services
builder.Services.AddSingleton<ISettingsService, SettingsService>();
builder.Services.AddSingleton(typeof(IFinlyticLogger<>), typeof(FinlyticLogger<>));
// Register HTTP Clients for external webhooks (HttpClientFactory manages pool)
builder.Services.AddHttpClient<IN8nEvaluationService, N8nEvaluationService>();
// Register Domain Services
builder.Services.AddSingleton<IVixTrackerService, VixTrackerService>();
builder.Services.AddSingleton<IThreeLayerFilterEngine, ThreeLayerFilterEngine>();
builder.Services.AddSingleton<IWinRateCalculator, WinRateCalculator>();
builder.Services.AddScoped<ISettingsDbService, SettingsDbService>();
builder.Services.AddSingleton<YahooFinanceClient>();
// Unified MQTT Client (Handles both Events and RPC)
builder.Services.AddSingleton<AnalyzerMqttClient>();
builder.Services.AddHostedService(provider => provider.GetRequiredService<AnalyzerMqttClient>());
// Register Active Trade Monitor
builder.Services.AddHostedService<ActiveTradeMonitorWorker>();
var host = builder.Build();
// Run DB Migrations
using (var scope = host.Services.CreateScope())
{
try
{
var context = scope.ServiceProvider.GetRequiredService<AnalyzerDbContext>();
await context.Database.MigrateAsync();
Console.WriteLine("Database migrations successfully executed for FinlyticAnalyzer.");
}
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<IVixTrackerService>();
await vixService.PollVixAsync();
}
await host.RunAsync();
-37
View File
@@ -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.
@@ -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<ActiveTradeMonitorWorker> _finlyticLogger;
private readonly IServiceScopeFactory _scopeFactory;
private readonly AnalyzerMqttClient _mqttClient;
public ActiveTradeMonitorWorker(
IFinlyticLogger<ActiveTradeMonitorWorker> 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<List<TradeProposalDto>, GetTradesRequest>(
"trades_Get",
new GetTradesRequest(null, "Active"),
TimeSpan.FromSeconds(10));
var proposedTrades = await _mqttClient.SendRpcRequestAsync<List<TradeProposalDto>, GetTradesRequest>(
"trades_Get",
new GetTradesRequest(null, "Proposed"),
TimeSpan.FromSeconds(10));
var trades = new List<TradeProposalDto>();
if (activeTrades != null) trades.AddRange(activeTrades);
if (proposedTrades != null) trades.AddRange(proposedTrades.Where(t => t.IsGlobalProposal));
if (trades.Count == 0)
{
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<IN8nEvaluationService>();
var vixService = scope.ServiceProvider.GetRequiredService<IVixTrackerService>();
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<LivePriceDto, IsinRequest>(
"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<TechnicalAnalysisDto, IsinRequest>(
"ta_GetAnalysis", livePriceReq, TimeSpan.FromSeconds(5));
var latestIndicator = taResult?.Indicators?.LastOrDefault();
var taInfo = new TechnicalContextInfo
{
Rsi = latestIndicator?.Rsi14?.ToString("F1") ?? "N/A",
SupertrendStatus = latestIndicator?.SupertrendDirection ?? "N/A",
Atr = latestIndicator?.Atr14?.ToString("F2") ?? "N/A",
Sma50 = (double?)latestIndicator?.Sma50,
Sma200 = (double?)latestIndicator?.Sma200,
DetectedPatterns = taResult?.Patterns?.Select(p => new PatternContextInfo
{
PatternName = p.Type,
BreakoutDirection = p.BreakoutSignal?.Direction,
TargetPrice = (double?)p.BreakoutSignal?.TargetPrice,
PotentialPercent = (double?)p.BreakoutSignal?.PotentialPercent
}).ToList() ?? new List<PatternContextInfo>()
};
var n8nReq = new N8nAnalysisRequestDto
{
RequestId = Guid.NewGuid().ToString("N"),
Timestamp = DateTime.UtcNow,
TriggerType = "HourlyMonitor",
TargetAsset = new TargetAssetInfo
{
Symbol = trade.Symbol,
Isin = trade.Isin,
Sector = trade.Sector
},
MarketContext = new MarketContextInfo
{
Vix = 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<int>();
string currentNum = "";
foreach (char c in timeframe)
{
if (char.IsDigit(c))
{
currentNum += c;
}
else if (currentNum.Length > 0)
{
if (int.TryParse(currentNum, out int n)) numbers.Add(n);
currentNum = "";
}
}
if (currentNum.Length > 0 && int.TryParse(currentNum, out int lastN)) numbers.Add(lastN);
int maxNum = numbers.Count > 0 ? numbers.Max() : 14;
if (maxNum == 0) maxNum = 14;
if (multiplier == 1 && maxNum < 3) maxNum = 3;
return maxNum * multiplier;
}
}
@@ -1,13 +0,0 @@
using System.Threading;
using System.Threading.Tasks;
using FinlyticCore.Models.Analyzer;
namespace FinlyticAnalyzer.Services;
public interface IN8nEvaluationService
{
/// <summary>
/// Evaluates an asset asynchronously using N8n.
/// </summary>
Task<N8nAnalysisResponseDto?> EvaluateAssetAsync(N8nAnalysisRequestDto request, CancellationToken cancellationToken = default);
}
@@ -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
{
/// <summary>
/// Evaluates news based on market regime and returns a filter result.
/// </summary>
FilterResult EvaluateNews(NewsArticleDto newsEvent, VixMarketRegime regime);
}
@@ -1,28 +0,0 @@
using System.Threading;
using System.Threading.Tasks;
using FinlyticCore.Models.Analyzer;
namespace FinlyticAnalyzer.Services;
public interface IVixTrackerService
{
/// <summary>
/// Gets the current VIX value.
/// </summary>
decimal GetCurrentVix();
/// <summary>
/// Gets the current market regime based on VIX.
/// </summary>
VixMarketRegime GetCurrentRegime();
/// <summary>
/// Updates the VIX tracker with a new tick value.
/// </summary>
void UpdateVixFromTick(decimal vixValue);
/// <summary>
/// Polls the VIX asynchronously and returns its value.
/// </summary>
Task<decimal> PollVixAsync(CancellationToken cancellationToken = default);
}
@@ -1,25 +0,0 @@
using FinlyticCore.Models.Analyzer;
namespace FinlyticAnalyzer.Services;
public interface IWinRateCalculator
{
/// <summary>
/// Calculates the win rate for a given sector and symbol under the specified market regime.
/// </summary>
double CalculateWinRate(string sector, string symbol, VixMarketRegime regime);
/// <summary>
/// Calculates a multi-factor dynamic AI Win-Rate / Confidence Score using technicals, sentiment, fundamentals, AI eval score, and market regime.
/// </summary>
double CalculateDynamicWinRate(
string sector,
string symbol,
VixMarketRegime regime,
double? n8nEvalScore = null,
double? technicalScore = null,
double? sentimentScore = null,
double? fundamentalScore = null,
string signalType = "BUY");
}
@@ -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;
/// <summary>
/// Checks if a given log category is enabled.
/// </summary>
public static bool IsEnabled(LogCategory category)
{
return category switch
{
LogCategory.MqttHealthPing => EnableLogMqttHealthPing,
LogCategory.MqttGeneral => EnableLogMqttGeneral,
LogCategory.AnalyzerAuto => EnableLogAnalyzerAuto,
LogCategory.AnalyzerManual => EnableLogAnalyzerManual,
LogCategory.DatabaseOps => EnableLogDatabaseOps,
_ => true
};
}
}
@@ -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<N8nEvaluationService> _finlyticLogger;
public N8nEvaluationService(
HttpClient httpClient,
ISettingsService settingsService,
IConfiguration configuration,
IFinlyticLogger<N8nEvaluationService> finlyticLogger)
{
_httpClient = httpClient;
_settingsService = settingsService;
_configuration = configuration;
_finlyticLogger = finlyticLogger;
_httpClient.Timeout = TimeSpan.FromSeconds(45);
}
/// <summary>
/// Evaluates an asset asynchronously using N8n / Gemini workflows.
/// </summary>
public async Task<N8nAnalysisResponseDto?> EvaluateAssetAsync(N8nAnalysisRequestDto request, CancellationToken cancellationToken = default)
{
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
};
}
}
@@ -1,124 +0,0 @@
using FinlyticAnalyzer.Database;
using FinlyticAnalyzer.Entities;
using Microsoft.EntityFrameworkCore;
namespace FinlyticAnalyzer.Services;
public interface ISettingsDbService
{
/// <summary>
/// Gets the analyzer settings asynchronously.
/// </summary>
Task<AnalyzerSettingsEntity> GetSettingsAsync();
/// <summary>
/// Saves the analyzer settings asynchronously.
/// </summary>
Task<AnalyzerSettingsEntity> SaveSettingsAsync(AnalyzerSettingsEntity settings);
/// <summary>
/// Updates settings from a dictionary asynchronously.
/// </summary>
Task UpdateSettingsFromDictionaryAsync(Dictionary<string, string> dictionary);
}
public class SettingsDbService : ISettingsDbService
{
private readonly AnalyzerDbContext _context;
public SettingsDbService(AnalyzerDbContext context)
{
_context = context;
}
/// <summary>
/// Gets the analyzer settings asynchronously.
/// </summary>
public async Task<AnalyzerSettingsEntity> GetSettingsAsync()
{
var settings = await _context.Settings.AsNoTracking().FirstOrDefaultAsync();
if (settings == null)
{
settings = new AnalyzerSettingsEntity { Id = Guid.NewGuid(), 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;
}
/// <summary>
/// Saves the analyzer settings asynchronously.
/// </summary>
public async Task<AnalyzerSettingsEntity> 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;
}
/// <summary>
/// Updates settings from a dictionary asynchronously.
/// </summary>
public async Task UpdateSettingsFromDictionaryAsync(Dictionary<string, string> dictionary)
{
var settings = await GetSettingsAsync();
foreach (var (key, value) in dictionary)
{
if (string.Equals(key, "ScanCronSchedule", StringComparison.OrdinalIgnoreCase) && !string.IsNullOrWhiteSpace(value))
settings.ScanCronSchedule = value.Trim();
else if (string.Equals(key, "MinSignalScore", StringComparison.OrdinalIgnoreCase) && double.TryParse(value, out var score))
settings.MinSignalScore = score;
else if (string.Equals(key, "EnableLog_MqttHealthPing", StringComparison.OrdinalIgnoreCase) && bool.TryParse(value, out var b1))
settings.EnableLogMqttHealthPing = b1;
else if (string.Equals(key, "EnableLog_MqttGeneral", StringComparison.OrdinalIgnoreCase) && bool.TryParse(value, out var b2))
settings.EnableLogMqttGeneral = b2;
else if (string.Equals(key, "EnableLog_AnalyzerAuto", StringComparison.OrdinalIgnoreCase) && bool.TryParse(value, out var b3))
settings.EnableLogAnalyzerAuto = b3;
else if (string.Equals(key, "EnableLog_AnalyzerManual", StringComparison.OrdinalIgnoreCase) && bool.TryParse(value, out var b4))
settings.EnableLogAnalyzerManual = b4;
else if (string.Equals(key, "EnableLog_DatabaseOps", StringComparison.OrdinalIgnoreCase) && bool.TryParse(value, out var b5))
settings.EnableLogDatabaseOps = b5;
}
settings.UpdatedAt = DateTime.UtcNow;
await SaveSettingsAsync(settings);
}
}
@@ -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<ThreeLayerFilterEngine> _finlyticLogger;
private readonly ConcurrentDictionary<string, DateTime> _seenEvents = new();
private readonly object _cleanupLock = new();
private DateTime _lastCleanupTime = DateTime.UtcNow;
public ThreeLayerFilterEngine(IFinlyticLogger<ThreeLayerFilterEngine> finlyticLogger)
{
_finlyticLogger = finlyticLogger;
}
/// <summary>
/// Evaluates news strictly based on ISIN and dynamic VIX market regime.
/// </summary>
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 _);
}
}
}
}
@@ -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<VixTrackerService> _finlyticLogger;
private decimal _currentVix = 18.5m;
private VixMarketRegime _currentRegime = VixMarketRegime.Normal;
private readonly object _lock = new();
public VixTrackerService(YahooFinanceClient yahooClient, IFinlyticLogger<VixTrackerService> 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<decimal> PollVixAsync(CancellationToken cancellationToken = default)
{
try
{
var vix = await _yahooClient.GetLivePriceAsync("^VIX", cancellationToken);
if (vix.HasValue && vix.Value > 0m)
{
UpdateVixFromTick(vix.Value);
return vix.Value;
}
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
}
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
};
}
}
@@ -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<WinRateCalculator> _finlyticLogger;
private readonly string _feedbackDir;
private readonly object _cacheLock = new();
private List<TradeFeedbackRecord>? _cachedRecords;
private DateTime _lastCacheTime = DateTime.MinValue;
private static readonly TimeSpan CacheTtl = TimeSpan.FromMinutes(3);
public WinRateCalculator(IFinlyticLogger<WinRateCalculator> finlyticLogger)
{
_finlyticLogger = finlyticLogger;
_feedbackDir = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "data", "feedback");
if (!Directory.Exists(_feedbackDir))
{
Directory.CreateDirectory(_feedbackDir);
}
}
/// <summary>
/// Calculates the win rate for a given sector and symbol under the specified market regime.
/// </summary>
public double CalculateWinRate(string sector, string symbol, VixMarketRegime regime)
{
return CalculateDynamicWinRate(sector, symbol, regime);
}
/// <summary>
/// Calculates a multi-factor dynamic AI Win-Rate / Confidence Score using technicals, sentiment, fundamentals, AI eval score, and market regime.
/// </summary>
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<TradeFeedbackRecord> GetCachedOrLoadRecords()
{
lock (_cacheLock)
{
if (_cachedRecords != null && (DateTime.UtcNow - _lastCacheTime) < CacheTtl)
{
return _cachedRecords;
}
var loadedList = new List<TradeFeedbackRecord>();
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<TradeFeedbackRecord[]>(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;
}
}
}
-931
View File
@@ -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;
/// <summary>
/// Unified Managed MQTT Client for FinlyticAnalyzer.
/// Handles event subscriptions, market screening, manual AI evaluation triggers,
/// and dispatches trade proposals via MQTT.
/// </summary>
public class AnalyzerMqttClient : ManagedMqttClient, IHostedService
{
private readonly IConfiguration _configuration;
private readonly IServiceScopeFactory _scopeFactory;
private readonly IVixTrackerService _vixTracker;
private readonly IThreeLayerFilterEngine _filterEngine;
private readonly IWinRateCalculator _winRateCalculator;
private readonly IN8nEvaluationService _n8nService;
private readonly ILogger<AnalyzerMqttClient> _logger;
public AnalyzerMqttClient(
IConfiguration configuration,
IServiceScopeFactory scopeFactory,
IVixTrackerService vixTracker,
IThreeLayerFilterEngine filterEngine,
IWinRateCalculator winRateCalculator,
IN8nEvaluationService n8nService,
ILogger<AnalyzerMqttClient> logger) : base(logger)
{
_configuration = configuration;
_scopeFactory = scopeFactory;
_vixTracker = vixTracker;
_filterEngine = filterEngine;
_winRateCalculator = winRateCalculator;
_n8nService = n8nService;
_logger = logger;
}
public async Task StartAsync(CancellationToken cancellationToken)
{
var config = new MqttConfiguration
{
Host = _configuration["MQTT:Host"] ?? _configuration["MQTT__Host"] ?? "localhost",
Port = Convert.ToInt32(_configuration["MQTT:Port"] ?? _configuration["MQTT__Port"] ?? "1883"),
Username = _configuration["MQTT:Username"] ?? _configuration["MQTT__Username"],
Password = _configuration["MQTT:Password"] ?? _configuration["MQTT__Password"],
ClientId = $"{(_configuration["MQTT:ClientId"] ?? _configuration["MQTT__ClientId"] ?? "finlytic_analyzer")}_{Guid.NewGuid():N}"
};
_logger.LogInformation("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<IFinlyticLogger<AnalyzerMqttClient>>();
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<ISettingsService>();
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<IFinlyticLogger<AnalyzerMqttClient>>();
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<IFinlyticLogger<AnalyzerMqttClient>>();
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<IFinlyticLogger<AnalyzerMqttClient>>();
var settingsService = scope.ServiceProvider.GetRequiredService<ISettingsService>();
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<IFinlyticLogger<AnalyzerMqttClient>>();
var settingsService = scope.ServiceProvider.GetRequiredService<ISettingsService>();
await finlyticLogger.LogInfoAsync(SettingKeys.MqttChannel, "[FinlyticAnalyzer] [Settings_Update] Processing settings update RPC [CorrelationId: {CorrelationId}]", correlationId);
try
{
Dictionary<string, object?>? updates = null;
try
{
updates = JsonSerializer.Deserialize<Dictionary<string, object?>>(payload);
}
catch
{
var list = JsonSerializer.Deserialize<List<DynamicSettingDto>>(payload);
if (list != null)
{
updates = new Dictionary<string, object?>();
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<IFinlyticLogger<AnalyzerMqttClient>>();
try
{
var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
var closedDto = JsonSerializer.Deserialize<TradeProposalDto>(payloadStr, options);
if (closedDto != null && !string.IsNullOrWhiteSpace(closedDto.TradeId))
{
bool isWin = closedDto.Status?.Contains("Profit", StringComparison.OrdinalIgnoreCase) == 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<IFinlyticLogger<AnalyzerMqttClient>>();
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<AnalyzerDbContext>();
var regime = _vixTracker.GetCurrentRegime();
var currentVix = _vixTracker.GetCurrentVix();
string analysisId = Guid.NewGuid().ToString("N");
double winRate = _winRateCalculator.CalculateWinRate(manualReq.Sector, manualReq.Symbol, regime);
string riskLabel = manualReq.RiskScore > 70 ? $"Aggressiv ({manualReq.RiskScore}/100)" : (manualReq.RiskScore > 30 ? $"Balanced ({manualReq.RiskScore}/100)" : $"Konservativ ({manualReq.RiskScore}/100)");
string timeframeFormatted = $"{manualReq.MinTimeframeValue}-{manualReq.MaxTimeframeValue} {manualReq.TimeframeUnit}";
var n8nRequest = new N8nAnalysisRequestDto
{
RequestId = analysisId,
Timestamp = DateTime.UtcNow,
TriggerType = "Manual",
TargetAsset = new TargetAssetInfo
{
Symbol = manualReq.FundamentalsData?.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<PatternContextInfo>()
},
SentimentContext = new SentimentContextInfo
{
AssetSentimentScore = manualReq.SentimentData?.CurrentSummary?.CompoundScore ?? 0.0,
SectorSentimentScore = 0.0,
NewsSentimentSummary = manualReq.SentimentData?.CurrentSummary?.SentimentLabel ?? "Neutral"
},
FundamentalContext = new FundamentalContextInfo
{
PeRatio = (double?)manualReq.FundamentalsData?.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<ISettingsService>();
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<IFinlyticLogger<AnalyzerMqttClient>>();
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<FinlyticCore.Dtos.TechnicalAnalysis.LivePriceDto, IsinRequest>(
"tr_GetLivePrice", isinReq, TimeSpan.FromSeconds(5));
var taTask = SendRpcRequestAsync<FinlyticCore.Dtos.TechnicalAnalysis.TechnicalAnalysisDto, IsinRequest>(
"ta_GetAnalysis", isinReq, TimeSpan.FromSeconds(5));
var fundTask = SendRpcRequestAsync<FinlyticCore.Dtos.Fundamentals.AssetFundamentalsDto, IsinRequest>(
"fundamentals_Get", isinReq, TimeSpan.FromSeconds(5));
var sentTask = SendRpcRequestAsync<FinlyticCore.Dtos.Sentiment.IsinSentimentSummaryDto, IsinRequest>(
"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<PatternContextInfo>()
};
}
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<ISettingsService>();
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<double>();
var resistanceLevels = new List<double>();
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<AnalyzerDbContext>();
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;
}
-33
View File
@@ -1,33 +0,0 @@
using FinlyticCore.Models.Settings;
namespace FinlyticAnalyzer.Util;
public static class SettingKeys
{
// --- Logging-Kanäle ---
public static readonly SettingKey<bool> AnalyzerChannel = new("Logging.Channel.Analyzer", true);
public static readonly SettingKey<bool> MqttChannel = new("Logging.Channel.MQTT", true);
public static readonly SettingKey<bool> HealthPingChannel = new("Logging.Channel.Health", true);
// --- Makro & VIX Schwellenwerte ---
public static readonly SettingKey<double> VixPanicThreshold = new("Macro.VixPanicThreshold", 28.0);
public static readonly SettingKey<double> VixElevatedThreshold = new("Macro.VixElevatedThreshold", 20.0);
public static readonly SettingKey<int> VixPollIntervalSeconds = new("Macro.VixPollIntervalSeconds", 60);
// --- Filter & Winrate-Logik ---
public static readonly SettingKey<double> MinWinRateThreshold = new("Filter.MinWinRateThreshold", 60.0);
public static readonly SettingKey<double> WeightMacro = new("Filter.WeightMacro", 0.30);
public static readonly SettingKey<double> WeightFundamental = new("Filter.WeightFundamental", 0.30);
public static readonly SettingKey<double> WeightSentiment = new("Filter.WeightSentiment", 0.20);
public static readonly SettingKey<double> WeightTechnical = new("Filter.WeightTechnical", 0.20);
// --- Trade & Risiko-Parameter ---
public static readonly SettingKey<double> DefaultTakeProfitPercent = new("Trade.DefaultTakeProfitPercent", 15.0);
public static readonly SettingKey<double> DefaultStopLossPercent = new("Trade.DefaultStopLossPercent", 5.0);
public static readonly SettingKey<int> MaxAllowedLeverage = new("Trade.MaxAllowedLeverage", 10);
public static readonly SettingKey<double> MaxRiskPerTradePercent = new("Trade.MaxRiskPerTradePercent", 2.0);
public static readonly SettingKey<int> ProposalValidityHours = new("Trade.ProposalValidityHours", 24);
// --- N8N / Webhook-Konfiguration ---
public static readonly SettingKey<string> N8nWebhookUrl = new("N8N.WebhookUrl", "https://n8n.kleidukos.me/webhook/gemini/analysis/auto");
}
-16
View File
@@ -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"
}
}
@@ -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<TechnicalAnalysisDbContext> options) : base(options)
{
}
public DbSet<SettingEntity> DynamicSettings => Set<SettingEntity>();
public DbSet<MarketCandleEntity> MarketCandles => Set<MarketCandleEntity>();
public DbSet<MacroDataEntity> MacroData => Set<MacroDataEntity>();
public DbSet<CachedAnalysisEntity> CachedAnalyses => Set<CachedAnalysisEntity>();
public DbSet<TaSettingsEntity> Settings => Set<TaSettingsEntity>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<SettingEntity>(entity =>
{
entity.HasKey(e => e.Id);
entity.HasIndex(e => e.Key).IsUnique();
});
modelBuilder.Entity<MarketCandleEntity>()
.HasIndex(c => new { c.Symbol, c.Interval, c.Timestamp })
.IsUnique();
modelBuilder.Entity<CachedAnalysisEntity>()
.HasIndex(c => c.Isin);
}
}
public class TechnicalAnalysisDbContextFactory : IDesignTimeDbContextFactory<TechnicalAnalysisDbContext>
{
public TechnicalAnalysisDbContext CreateDbContext(string[] args)
{
var optionsBuilder = new DbContextOptionsBuilder<TechnicalAnalysisDbContext>();
optionsBuilder.UseNpgsql("Host=localhost;Database=ta;Username=postgres;Password=postgres");
return new TechnicalAnalysisDbContext(optionsBuilder.Options);
}
}
-22
View File
@@ -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"]
@@ -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;
}
@@ -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;
}
@@ -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; }
}
@@ -1,22 +0,0 @@
using System;
using System.ComponentModel.DataAnnotations;
namespace FinlyticTechnicalAnalysis.Entities;
/// <summary>
/// Entity representing global indicator and strategy settings for FinlyticTechnicalAnalysis.
/// Persisted in PostgreSQL and updated dynamically via Admin Panel MQTT events.
/// </summary>
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;
}
@@ -1,31 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk.Worker">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.9" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.9">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.9">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.9" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.1" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.2" />
<PackageReference Include="Microsoft.Extensions.Http" Version="10.0.1" />
<PackageReference Include="Skender.Stock.Indicators" Version="2.7.3" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\FinlyticCore\FinlyticCore.csproj" />
</ItemGroup>
</Project>
@@ -1,162 +0,0 @@
// <auto-generated />
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
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "10.0.9")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("FinlyticTechnicalAnalysis.Entities.CachedAnalysisEntity", b =>
{
b.Property<string>("Isin")
.HasMaxLength(20)
.HasColumnType("character varying(20)");
b.Property<string>("AnalysisJson")
.IsRequired()
.HasColumnType("jsonb");
b.Property<DateTime>("CalculatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("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<string>("Symbol")
.HasMaxLength(20)
.HasColumnType("character varying(20)");
b.Property<DateTime>("LastUpdatedAt")
.HasColumnType("timestamp with time zone");
b.Property<decimal>("PreviousClose")
.HasColumnType("decimal(18, 6)");
b.Property<string>("TrendState")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<decimal>("Value")
.HasColumnType("decimal(18, 6)");
b.HasKey("Symbol");
b.ToTable("MacroData");
});
modelBuilder.Entity("FinlyticTechnicalAnalysis.Entities.MarketCandleEntity", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<decimal?>("Ask")
.HasColumnType("decimal(18, 6)");
b.Property<decimal?>("Bid")
.HasColumnType("decimal(18, 6)");
b.Property<decimal>("Close")
.HasColumnType("decimal(18, 6)");
b.Property<decimal>("High")
.HasColumnType("decimal(18, 6)");
b.Property<string>("Interval")
.IsRequired()
.HasMaxLength(10)
.HasColumnType("character varying(10)");
b.Property<decimal>("Low")
.HasColumnType("decimal(18, 6)");
b.Property<decimal>("Open")
.HasColumnType("decimal(18, 6)");
b.Property<string>("Symbol")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("character varying(20)");
b.Property<DateTime>("Timestamp")
.HasColumnType("timestamp with time zone");
b.Property<long>("Volume")
.HasColumnType("bigint");
b.HasKey("Id");
b.HasIndex("Symbol", "Interval", "Timestamp")
.IsUnique();
b.ToTable("MarketCandles");
});
modelBuilder.Entity("FinlyticTechnicalAnalysis.Entities.TaSettingsEntity", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<int>("EmaShortPeriod")
.HasColumnType("integer");
b.Property<double>("RsiOverboughtLimit")
.HasColumnType("double precision");
b.Property<double>("RsiOversoldLimit")
.HasColumnType("double precision");
b.Property<int>("SmaLongPeriod")
.HasColumnType("integer");
b.Property<int>("SmaMediumPeriod")
.HasColumnType("integer");
b.Property<double>("SupertrendMultiplier")
.HasColumnType("double precision");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.ToTable("Settings");
});
#pragma warning restore 612, 618
}
}
}
@@ -1,112 +0,0 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace FinlyticTechnicalAnalysis.Migrations
{
/// <inheritdoc />
public partial class Init : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "CachedAnalyses",
columns: table => new
{
Isin = table.Column<string>(type: "character varying(20)", maxLength: 20, nullable: false),
Ticker = table.Column<string>(type: "character varying(20)", maxLength: 20, nullable: false),
AnalysisJson = table.Column<string>(type: "jsonb", nullable: false),
CalculatedAt = table.Column<DateTime>(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<string>(type: "character varying(20)", maxLength: 20, nullable: false),
Value = table.Column<decimal>(type: "numeric(18,6)", nullable: false),
PreviousClose = table.Column<decimal>(type: "numeric(18,6)", nullable: false),
TrendState = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: false),
LastUpdatedAt = table.Column<DateTime>(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<long>(type: "bigint", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
Symbol = table.Column<string>(type: "character varying(20)", maxLength: 20, nullable: false),
Interval = table.Column<string>(type: "character varying(10)", maxLength: 10, nullable: false),
Timestamp = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
Open = table.Column<decimal>(type: "numeric(18,6)", nullable: false),
High = table.Column<decimal>(type: "numeric(18,6)", nullable: false),
Low = table.Column<decimal>(type: "numeric(18,6)", nullable: false),
Close = table.Column<decimal>(type: "numeric(18,6)", nullable: false),
Volume = table.Column<long>(type: "bigint", nullable: false),
Bid = table.Column<decimal>(type: "numeric(18,6)", nullable: true),
Ask = table.Column<decimal>(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<Guid>(type: "uuid", nullable: false),
EmaShortPeriod = table.Column<int>(type: "integer", nullable: false),
SmaMediumPeriod = table.Column<int>(type: "integer", nullable: false),
SmaLongPeriod = table.Column<int>(type: "integer", nullable: false),
RsiOverboughtLimit = table.Column<double>(type: "double precision", nullable: false),
RsiOversoldLimit = table.Column<double>(type: "double precision", nullable: false),
SupertrendMultiplier = table.Column<double>(type: "double precision", nullable: false),
UpdatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Settings", x => x.Id);
});
migrationBuilder.CreateIndex(
name: "IX_CachedAnalyses_Isin",
table: "CachedAnalyses",
column: "Isin");
migrationBuilder.CreateIndex(
name: "IX_MarketCandles_Symbol_Interval_Timestamp",
table: "MarketCandles",
columns: new[] { "Symbol", "Interval", "Timestamp" },
unique: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "CachedAnalyses");
migrationBuilder.DropTable(
name: "MacroData");
migrationBuilder.DropTable(
name: "MarketCandles");
migrationBuilder.DropTable(
name: "Settings");
}
}
}
@@ -1,162 +0,0 @@
// <auto-generated />
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
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "10.0.9")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("FinlyticTechnicalAnalysis.Entities.CachedAnalysisEntity", b =>
{
b.Property<string>("Isin")
.HasMaxLength(20)
.HasColumnType("character varying(20)");
b.Property<string>("AnalysisJson")
.IsRequired()
.HasColumnType("jsonb");
b.Property<DateTime>("CalculatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("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<string>("Symbol")
.HasMaxLength(20)
.HasColumnType("character varying(20)");
b.Property<DateTime>("LastUpdatedAt")
.HasColumnType("timestamp with time zone");
b.Property<decimal>("PreviousClose")
.HasColumnType("decimal(18, 6)");
b.Property<string>("TrendState")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<decimal>("Value")
.HasColumnType("decimal(18, 6)");
b.HasKey("Symbol");
b.ToTable("MacroData");
});
modelBuilder.Entity("FinlyticTechnicalAnalysis.Entities.MarketCandleEntity", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<decimal?>("Ask")
.HasColumnType("decimal(18, 6)");
b.Property<decimal?>("Bid")
.HasColumnType("decimal(18, 6)");
b.Property<decimal>("Close")
.HasColumnType("decimal(18, 6)");
b.Property<decimal>("High")
.HasColumnType("decimal(18, 6)");
b.Property<string>("Interval")
.IsRequired()
.HasMaxLength(10)
.HasColumnType("character varying(10)");
b.Property<decimal>("Low")
.HasColumnType("decimal(18, 6)");
b.Property<decimal>("Open")
.HasColumnType("decimal(18, 6)");
b.Property<string>("Symbol")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("character varying(20)");
b.Property<DateTime>("Timestamp")
.HasColumnType("timestamp with time zone");
b.Property<long>("Volume")
.HasColumnType("bigint");
b.HasKey("Id");
b.HasIndex("Symbol", "Interval", "Timestamp")
.IsUnique();
b.ToTable("MarketCandles");
});
modelBuilder.Entity("FinlyticTechnicalAnalysis.Entities.TaSettingsEntity", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<int>("EmaShortPeriod")
.HasColumnType("integer");
b.Property<double>("RsiOverboughtLimit")
.HasColumnType("double precision");
b.Property<double>("RsiOversoldLimit")
.HasColumnType("double precision");
b.Property<int>("SmaLongPeriod")
.HasColumnType("integer");
b.Property<int>("SmaMediumPeriod")
.HasColumnType("integer");
b.Property<double>("SupertrendMultiplier")
.HasColumnType("double precision");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.ToTable("Settings");
});
#pragma warning restore 612, 618
}
}
}
@@ -1,22 +0,0 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace FinlyticTechnicalAnalysis.Migrations
{
/// <inheritdoc />
public partial class CheckPendingTechnicalAnalysis : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
}
}
}
@@ -1,193 +0,0 @@
// <auto-generated />
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
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "10.0.9")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("FinlyticCore.Entities.Settings.SettingEntity", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("Key")
.IsRequired()
.HasMaxLength(150)
.HasColumnType("character varying(150)");
b.Property<DateTime>("LastUpdatedUtc")
.HasColumnType("timestamp with time zone");
b.Property<string>("ServiceIdentifier")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<string>("ValueJson")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("Key")
.IsUnique();
b.ToTable("DynamicSettings");
});
modelBuilder.Entity("FinlyticTechnicalAnalysis.Entities.CachedAnalysisEntity", b =>
{
b.Property<string>("Isin")
.HasMaxLength(20)
.HasColumnType("character varying(20)");
b.Property<string>("AnalysisJson")
.IsRequired()
.HasColumnType("jsonb");
b.Property<DateTime>("CalculatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("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<string>("Symbol")
.HasMaxLength(20)
.HasColumnType("character varying(20)");
b.Property<DateTime>("LastUpdatedAt")
.HasColumnType("timestamp with time zone");
b.Property<decimal>("PreviousClose")
.HasColumnType("decimal(18, 6)");
b.Property<string>("TrendState")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<decimal>("Value")
.HasColumnType("decimal(18, 6)");
b.HasKey("Symbol");
b.ToTable("MacroData");
});
modelBuilder.Entity("FinlyticTechnicalAnalysis.Entities.MarketCandleEntity", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<decimal?>("Ask")
.HasColumnType("decimal(18, 6)");
b.Property<decimal?>("Bid")
.HasColumnType("decimal(18, 6)");
b.Property<decimal>("Close")
.HasColumnType("decimal(18, 6)");
b.Property<decimal>("High")
.HasColumnType("decimal(18, 6)");
b.Property<string>("Interval")
.IsRequired()
.HasMaxLength(10)
.HasColumnType("character varying(10)");
b.Property<decimal>("Low")
.HasColumnType("decimal(18, 6)");
b.Property<decimal>("Open")
.HasColumnType("decimal(18, 6)");
b.Property<string>("Symbol")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("character varying(20)");
b.Property<DateTime>("Timestamp")
.HasColumnType("timestamp with time zone");
b.Property<long>("Volume")
.HasColumnType("bigint");
b.HasKey("Id");
b.HasIndex("Symbol", "Interval", "Timestamp")
.IsUnique();
b.ToTable("MarketCandles");
});
modelBuilder.Entity("FinlyticTechnicalAnalysis.Entities.TaSettingsEntity", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<int>("EmaShortPeriod")
.HasColumnType("integer");
b.Property<double>("RsiOverboughtLimit")
.HasColumnType("double precision");
b.Property<double>("RsiOversoldLimit")
.HasColumnType("double precision");
b.Property<int>("SmaLongPeriod")
.HasColumnType("integer");
b.Property<int>("SmaMediumPeriod")
.HasColumnType("integer");
b.Property<double>("SupertrendMultiplier")
.HasColumnType("double precision");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.ToTable("Settings");
});
#pragma warning restore 612, 618
}
}
}
@@ -1,43 +0,0 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace FinlyticTechnicalAnalysis.Migrations
{
/// <inheritdoc />
public partial class AddDynamicSettings : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "DynamicSettings",
columns: table => new
{
Id = table.Column<Guid>(type: "uuid", nullable: false),
Key = table.Column<string>(type: "character varying(150)", maxLength: 150, nullable: false),
ValueJson = table.Column<string>(type: "text", nullable: false),
ServiceIdentifier = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false),
LastUpdatedUtc = table.Column<DateTime>(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);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "DynamicSettings");
}
}
}
@@ -1,190 +0,0 @@
// <auto-generated />
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<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("Key")
.IsRequired()
.HasMaxLength(150)
.HasColumnType("character varying(150)");
b.Property<DateTime>("LastUpdatedUtc")
.HasColumnType("timestamp with time zone");
b.Property<string>("ServiceIdentifier")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<string>("ValueJson")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("Key")
.IsUnique();
b.ToTable("DynamicSettings");
});
modelBuilder.Entity("FinlyticTechnicalAnalysis.Entities.CachedAnalysisEntity", b =>
{
b.Property<string>("Isin")
.HasMaxLength(20)
.HasColumnType("character varying(20)");
b.Property<string>("AnalysisJson")
.IsRequired()
.HasColumnType("jsonb");
b.Property<DateTime>("CalculatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("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<string>("Symbol")
.HasMaxLength(20)
.HasColumnType("character varying(20)");
b.Property<DateTime>("LastUpdatedAt")
.HasColumnType("timestamp with time zone");
b.Property<decimal>("PreviousClose")
.HasColumnType("decimal(18, 6)");
b.Property<string>("TrendState")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<decimal>("Value")
.HasColumnType("decimal(18, 6)");
b.HasKey("Symbol");
b.ToTable("MacroData");
});
modelBuilder.Entity("FinlyticTechnicalAnalysis.Entities.MarketCandleEntity", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<decimal?>("Ask")
.HasColumnType("decimal(18, 6)");
b.Property<decimal?>("Bid")
.HasColumnType("decimal(18, 6)");
b.Property<decimal>("Close")
.HasColumnType("decimal(18, 6)");
b.Property<decimal>("High")
.HasColumnType("decimal(18, 6)");
b.Property<string>("Interval")
.IsRequired()
.HasMaxLength(10)
.HasColumnType("character varying(10)");
b.Property<decimal>("Low")
.HasColumnType("decimal(18, 6)");
b.Property<decimal>("Open")
.HasColumnType("decimal(18, 6)");
b.Property<string>("Symbol")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("character varying(20)");
b.Property<DateTime>("Timestamp")
.HasColumnType("timestamp with time zone");
b.Property<long>("Volume")
.HasColumnType("bigint");
b.HasKey("Id");
b.HasIndex("Symbol", "Interval", "Timestamp")
.IsUnique();
b.ToTable("MarketCandles");
});
modelBuilder.Entity("FinlyticTechnicalAnalysis.Entities.TaSettingsEntity", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<int>("EmaShortPeriod")
.HasColumnType("integer");
b.Property<double>("RsiOverboughtLimit")
.HasColumnType("double precision");
b.Property<double>("RsiOversoldLimit")
.HasColumnType("double precision");
b.Property<int>("SmaLongPeriod")
.HasColumnType("integer");
b.Property<int>("SmaMediumPeriod")
.HasColumnType("integer");
b.Property<double>("SupertrendMultiplier")
.HasColumnType("double precision");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.ToTable("Settings");
});
#pragma warning restore 612, 618
}
}
}
-67
View File
@@ -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<TechnicalAnalysisDbContext>(options =>
options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection")));
builder.Services.AddScoped<ISettingsDbContext>(sp => sp.GetRequiredService<TechnicalAnalysisDbContext>());
// Register Core Services & Logger
builder.Services.AddSingleton<ISettingsService, SettingsService>();
builder.Services.AddSingleton(typeof(IFinlyticLogger<>), typeof(FinlyticLogger<>));
// Register HTTP Clients
builder.Services.AddHttpClient<IYahooMarketDataScraper, YahooMarketDataScraper>()
.ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler
{
UseCookies = true,
CookieContainer = new System.Net.CookieContainer()
});
// Register Trade Republic WebSocket Client & Services
builder.Services.AddSingleton<TradeRepublicClient>();
builder.Services.AddSingleton<ITradeRepublicService, TradeRepublicService>();
// Register Technical Analysis Services
builder.Services.AddSingleton<ITechnicalAnalysisCalculator, TechnicalAnalysisCalculator>();
builder.Services.AddTransient<ITechnicalAnalysisDbService, TechnicalAnalysisDbService>();
builder.Services.AddScoped<ISettingsDbService, SettingsDbService>();
builder.Services.AddTransient<IYahooMarketDataScraper, YahooMarketDataScraper>();
builder.Services.AddSingleton<YahooFinanceClient>();
// Register MQTT Client (as a Hosted Service)
builder.Services.AddHostedService<TAMqttClient>();
var host = builder.Build();
// Run startup database migrations
using (var scope = host.Services.CreateScope())
{
try
{
var context = scope.ServiceProvider.GetRequiredService<TechnicalAnalysisDbContext>();
await context.Database.MigrateAsync();
Console.WriteLine("Database migrations successfully executed for FinlyticTechnicalAnalysis.");
var settingsService = scope.ServiceProvider.GetRequiredService<ISettingsDbService>();
await settingsService.GetSettingsAsync();
}
catch (Exception ex)
{
Console.WriteLine($"Critical error during database migration: {ex.Message}");
}
}
await host.RunAsync();
-34
View File
@@ -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).
@@ -1,100 +0,0 @@
using FinlyticTechnicalAnalysis.Database;
using FinlyticTechnicalAnalysis.Entities;
using Microsoft.EntityFrameworkCore;
namespace FinlyticTechnicalAnalysis.Services;
public interface ISettingsDbService
{
/// <summary>
/// Gets the settings.
/// </summary>
Task<TaSettingsEntity> GetSettingsAsync();
/// <summary>
/// Saves the settings.
/// </summary>
Task<TaSettingsEntity> SaveSettingsAsync(TaSettingsEntity settings);
/// <summary>
/// Updates settings from a dictionary.
/// </summary>
Task UpdateSettingsFromDictionaryAsync(Dictionary<string, string> dictionary);
}
public class SettingsDbService : ISettingsDbService
{
private readonly TechnicalAnalysisDbContext _context;
public SettingsDbService(TechnicalAnalysisDbContext context)
{
_context = context;
}
/// <summary>
/// Gets the settings.
/// </summary>
public async Task<TaSettingsEntity> 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;
}
/// <summary>
/// Saves the settings.
/// </summary>
public async Task<TaSettingsEntity> 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;
}
/// <summary>
/// Updates settings from a dictionary.
/// </summary>
public async Task UpdateSettingsFromDictionaryAsync(Dictionary<string, string> 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);
}
}
@@ -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
{
/// <summary>
/// Calculates the technical analysis using Skender.StockIndicators for math and custom algorithms for pattern detection.
/// </summary>
(List<IndicatorValuesDto> Indicators, List<ChartPatternDto> Patterns, List<StrategySignalDto> Signals) CalculateAnalysis(List<MarketCandleEntity> candles, string currency = "EUR");
}
public class TechnicalAnalysisCalculator : ITechnicalAnalysisCalculator
{
public (List<IndicatorValuesDto> Indicators, List<ChartPatternDto> Patterns, List<StrategySignalDto> Signals) CalculateAnalysis(List<MarketCandleEntity> candles, string currency = "EUR")
{
var indicators = new List<IndicatorValuesDto>();
var patterns = new List<ChartPatternDto>();
var signals = new List<StrategySignalDto>();
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<MarketCandleEntity> candles, List<double?> sma50, List<double?> sma200, List<double?> rsi14, List<StrategySignalDto> 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<MarketCandleEntity> sortedCandles, List<ChartPatternDto> patterns, string curSym)
{
if (sortedCandles.Count < 20) return;
int[] windowSizes = { 20, 30, 45, 60, 90, 120 };
var candidatePatterns = new List<ChartPatternDto>();
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<int> FindPivotLows(List<MarketCandleEntity> candles, int lookback = 3)
{
var result = new List<int>();
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<int> FindPivotHighs(List<MarketCandleEntity> candles, int lookback = 3)
{
var result = new List<int>();
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<MarketCandleEntity> slice, List<ChartPatternDto> 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<PatternPointDto>
{
new(slice[idx1].Timestamp, neckline),
new(futureTime, neckline)
},
LowerLine: new List<PatternPointDto>
{
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<MarketCandleEntity> slice, List<ChartPatternDto> 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<PatternPointDto>
{
new(slice[idx1].Timestamp, high1),
new(slice[idx2].Timestamp, high2)
},
LowerLine: new List<PatternPointDto>
{
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<MarketCandleEntity> slice, List<ChartPatternDto> 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<PatternPointDto>
{
new(slice[lsIdx].Timestamp, ls),
new(slice[headIdx].Timestamp, head),
new(slice[rsIdx].Timestamp, rs)
},
LowerLine: new List<PatternPointDto>
{
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<MarketCandleEntity> slice, List<ChartPatternDto> 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<PatternPointDto> { new(tH1, resistance), new(apexTime, resistance) },
LowerLine: new List<PatternPointDto> { 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<PatternPointDto> { new(tH1, yH1), new(tH2, yH2), new(apexTime, support) },
LowerLine: new List<PatternPointDto> { 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<PatternPointDto> { new(tH1, yH1), new(tH2, yH2), new(apexTime, apexPrice) },
LowerLine: new List<PatternPointDto> { 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));
}
}
}
}
}
}
@@ -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<TechnicalAnalysisDto?> GetAnalysisAsync(string isin, bool forceRefresh = false, string? ticker = null,
CancellationToken cancellationToken = default);
Task<LivePriceDto?> 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<TechnicalAnalysisDbService> _finlyticLogger;
private static readonly ConcurrentDictionary<string, (List<MarketCandleEntity> Candles, string Symbol, string Currency, DateTime FetchedAt)> _candleCache = new();
private static readonly ConcurrentDictionary<string, SemaphoreSlim> _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<TechnicalAnalysisDbService> finlyticLogger)
{
_scopeFactory = scopeFactory;
_yahooScraper = yahooScraper;
_trService = trService;
_calculator = calculator;
_finlyticLogger = finlyticLogger;
}
public async Task<TechnicalAnalysisDto?> 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<LivePriceDto?> 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<TechnicalAnalysisDto?> 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<TechnicalAnalysisDto> BuildAnalysisWithLivePriceAsync(
string cleanIsin, List<MarketCandleEntity> 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<MarketCandleEntity> 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<MarketCandleEntity> 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<bool>(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<MarketCandleEntity> 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<TechnicalAnalysisDto?> GetFromDbCacheAsync(string cleanIsin, string? requestedTicker, CancellationToken cancellationToken)
{
try
{
using var scope = _scopeFactory.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<TechnicalAnalysisDbContext>();
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<TechnicalAnalysisDto>(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<TechnicalAnalysisDbContext>();
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
};
}
@@ -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<MarketCandleEntity> Candles,
string Currency
);
public interface IYahooMarketDataScraper
{
/// <summary>
/// Resolves ticker from ISIN.
/// </summary>
Task<string?> ResolveTickerFromIsinAsync(string isin, CancellationToken cancellationToken = default);
/// <summary>
/// Fetches historical candles.
/// </summary>
Task<List<MarketCandleEntity>> FetchHistoricalCandlesAsync(string symbol, string range = "1y", string interval = "1d", CancellationToken cancellationToken = default);
/// <summary>
/// Fetches historical candles with currency.
/// </summary>
Task<YahooCandlesResult> FetchHistoricalCandlesWithCurrencyAsync(string symbol, string range = "1y", string interval = "1d", CancellationToken cancellationToken = default);
/// <summary>
/// Fetches macro ticker.
/// </summary>
Task<MacroDataEntity?> FetchMacroTickerAsync(string symbol, CancellationToken cancellationToken = default);
}
public class YahooMarketDataScraper : IYahooMarketDataScraper
{
private readonly YahooFinanceClient _yahooClient;
private readonly IConfiguration _configuration;
private readonly IFinlyticLogger<YahooMarketDataScraper> _finlyticLogger;
public YahooMarketDataScraper(
YahooFinanceClient yahooClient,
IConfiguration configuration,
IFinlyticLogger<YahooMarketDataScraper> finlyticLogger)
{
_yahooClient = yahooClient;
_configuration = configuration;
_finlyticLogger = finlyticLogger;
}
/// <summary>
/// Resolves ticker from ISIN using Yahoo Search API or Crypto Subtitle resolution for internal ISINs.
/// </summary>
public async Task<string?> 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;
}
/// <summary>
/// Fetches historical candles.
/// </summary>
public async Task<List<MarketCandleEntity>> FetchHistoricalCandlesAsync(string symbol, string range = "1y", string interval = "1d", CancellationToken cancellationToken = default)
{
var result = await FetchHistoricalCandlesWithCurrencyAsync(symbol, range, interval, cancellationToken);
return result.Candles;
}
/// <summary>
/// Fetches historical candles with currency metadata using authenticated Crumb/Cookie flow.
/// </summary>
public async Task<YahooCandlesResult> FetchHistoricalCandlesWithCurrencyAsync(string symbol, string range = "1y", string interval = "1d", CancellationToken cancellationToken = default)
{
var results = new List<MarketCandleEntity>();
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);
}
/// <summary>
/// Fetches macro ticker data (e.g., ^VIX, ^GSPC, DX-Y.NY).
/// </summary>
public async Task<MacroDataEntity?> 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";
}
}
@@ -1,26 +0,0 @@
using FinlyticCore.Models.Settings;
namespace FinlyticTechnicalAnalysis.Util;
public static class SettingKeys
{
// --- Logging-Kanäle ---
public static readonly SettingKey<bool> TechnicalAnalysisChannel = new("Logging.Channel.TechnicalAnalysis", true);
public static readonly SettingKey<bool> MqttChannel = new("Logging.Channel.MQTT", true);
public static readonly SettingKey<bool> HealthPingChannel = new("Logging.Channel.Health", true);
// --- Indikator-Konfiguration ---
public static readonly SettingKey<int> RsiPeriod = new("Indicators.RsiPeriod", 14);
public static readonly SettingKey<int> MacdFastPeriod = new("Indicators.MacdFastPeriod", 12);
public static readonly SettingKey<int> MacdSlowPeriod = new("Indicators.MacdSlowPeriod", 26);
public static readonly SettingKey<int> MacdSignalPeriod = new("Indicators.MacdSignalPeriod", 9);
public static readonly SettingKey<int> EmaShortPeriod = new("Indicators.EmaShortPeriod", 50);
public static readonly SettingKey<int> EmaLongPeriod = new("Indicators.EmaLongPeriod", 200);
public static readonly SettingKey<int> BollingerBandsPeriod = new("Indicators.BollingerBandsPeriod", 20);
public static readonly SettingKey<double> BollingerBandsStdDev = new("Indicators.BollingerBandsStdDev", 2.0);
public static readonly SettingKey<int> AtrPeriod = new("Indicators.AtrPeriod", 14);
// --- Cache & Performance ---
public static readonly SettingKey<int> CacheDurationMinutes = new("Cache.DurationMinutes", 60);
public static readonly SettingKey<bool> EnableAutoCache = new("Feature.EnableAutoCache", true);
}
@@ -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<TAMqttClient> _logger;
public TAMqttClient(
ILogger<TAMqttClient> logger,
IConfiguration configuration,
IServiceScopeFactory scopeFactory) : base(logger)
{
_logger = logger;
_configuration = configuration;
_scopeFactory = scopeFactory;
}
/// <summary>
/// Starts the MQTT client.
/// </summary>
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);
}
/// <summary>
/// Stops the MQTT client.
/// </summary>
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<IFinlyticLogger<TAMqttClient>>();
var settingsService = scope.ServiceProvider.GetRequiredService<ISettingsService>();
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<IFinlyticLogger<TAMqttClient>>();
var settingsService = scope.ServiceProvider.GetRequiredService<ISettingsService>();
await finlyticLogger.LogInfoAsync(SettingKeys.MqttChannel, "[FinlyticTechnicalAnalysis] [Settings_Update] Processing settings update RPC [CorrelationId: {CorrelationId}]", correlationId);
try
{
Dictionary<string, object?>? updates = null;
try
{
updates = JsonSerializer.Deserialize<Dictionary<string, object?>>(payload);
}
catch
{
var list = JsonSerializer.Deserialize<List<DynamicSettingDto>>(payload);
if (list != null)
{
updates = new Dictionary<string, object?>();
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<Dictionary<string, object?>>(settingsProp.GetRawText());
if (dict != null && dict.Count > 0)
{
using var scope = _scopeFactory.CreateScope();
var settings = scope.ServiceProvider.GetRequiredService<ISettingsService>();
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<IFinlyticLogger<TAMqttClient>>();
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<IFinlyticLogger<TAMqttClient>>();
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<object?>(responseTopic, null);
return;
}
try
{
var taDbService = scope.ServiceProvider.GetRequiredService<ITechnicalAnalysisDbService>();
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<object?>(responseTopic, null);
}
catch { }
}
}
private async Task HandleGetLivePriceAsync(string payload, string correlationId)
{
using var scope = _scopeFactory.CreateScope();
var finlyticLogger = scope.ServiceProvider.GetRequiredService<IFinlyticLogger<TAMqttClient>>();
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<object?>(responseTopic, null);
return;
}
try
{
var taDbService = scope.ServiceProvider.GetRequiredService<ITechnicalAnalysisDbService>();
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<object?>(responseTopic, null);
}
catch { }
}
}
}
@@ -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"
}
}
@@ -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<TradesDbContext> options) : base(options) { }
public DbSet<SettingEntity> DynamicSettings => Set<SettingEntity>();
public DbSet<TradeEntity> Trades => Set<TradeEntity>();
public DbSet<TradeHourlyUpdateEntity> TradeHourlyUpdates => Set<TradeHourlyUpdateEntity>();
public DbSet<TradesSettingsEntity> Settings => Set<TradesSettingsEntity>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<SettingEntity>(entity =>
{
entity.HasKey(e => e.Id);
entity.HasIndex(e => e.Key).IsUnique();
});
var stringListConverter =
new Microsoft.EntityFrameworkCore.Storage.ValueConversion.ValueConverter<List<string>, string>(
v => System.Text.Json.JsonSerializer.Serialize(v, (System.Text.Json.JsonSerializerOptions?)null),
v => System.Text.Json.JsonSerializer.Deserialize<List<string>>(v,
(System.Text.Json.JsonSerializerOptions?)null) ?? new List<string>()
);
var stringListComparer = new Microsoft.EntityFrameworkCore.ChangeTracking.ValueComparer<List<string>>(
(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<TradeEntity>(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<TradeHourlyUpdateEntity>(entity =>
{
entity.HasIndex(e => e.TradeId);
entity.HasIndex(e => e.Timestamp);
});
}
}
public class TradesDbContextFactory : IDesignTimeDbContextFactory<TradesDbContext>
{
public TradesDbContext CreateDbContext(string[] args)
{
var optionsBuilder = new DbContextOptionsBuilder<TradesDbContext>();
optionsBuilder.UseNpgsql("Host=localhost;Database=trades;Username=postgres;Password=postgres");
return new TradesDbContext(optionsBuilder.Options);
}
}
-16
View File
@@ -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"]
-156
View File
@@ -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<string> 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<TradeHourlyUpdateEntity> HourlyUpdates { get; set; } = new();
}
@@ -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;
}
@@ -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;
}
-24
View File
@@ -1,24 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.9" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.9">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="MQTTnet" Version="5.1.0.1559" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.2" />
<PackageReference Include="Parquet.Net" Version="5.0.2" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\FinlyticCore\FinlyticCore.csproj" />
</ItemGroup>
</Project>
-245
View File
@@ -1,245 +0,0 @@
// <auto-generated />
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
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "10.0.9")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("FinlyticTrades.Entities.TradeEntity", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("AnalysisId")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<string>("CloseReason")
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<DateTime?>("ClosedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("CompanyName")
.IsRequired()
.HasMaxLength(150)
.HasColumnType("character varying(150)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<decimal>("EntryPrice")
.HasColumnType("decimal(18,4)");
b.Property<string>("EventId")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<string>("InstrumentType")
.IsRequired()
.HasMaxLength(30)
.HasColumnType("character varying(30)");
b.Property<bool?>("IsWin")
.HasColumnType("boolean");
b.Property<string>("Isin")
.IsRequired()
.HasMaxLength(30)
.HasColumnType("character varying(30)");
b.Property<decimal?>("PnlAbsolute")
.HasColumnType("decimal(18,4)");
b.Property<decimal?>("PnlPercent")
.HasColumnType("decimal(18,4)");
b.Property<string>("Reasoning")
.IsRequired()
.HasColumnType("text");
b.Property<string>("RiskTolerance")
.IsRequired()
.HasMaxLength(30)
.HasColumnType("character varying(30)");
b.Property<string>("Sector")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<string>("SignalType")
.IsRequired()
.HasMaxLength(10)
.HasColumnType("character varying(10)");
b.Property<int>("Status")
.HasColumnType("integer");
b.Property<decimal>("StopLoss")
.HasColumnType("decimal(18,4)");
b.Property<string>("Symbol")
.IsRequired()
.HasMaxLength(30)
.HasColumnType("character varying(30)");
b.Property<decimal>("TakeProfit")
.HasColumnType("decimal(18,4)");
b.Property<string>("Timeframe")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("character varying(20)");
b.Property<string>("TradeId")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<int>("TtlMinutes")
.HasColumnType("integer");
b.Property<decimal?>("UserExitPrice")
.HasColumnType("decimal(18,4)");
b.Property<DateTime?>("UserExitTimestamp")
.HasColumnType("timestamp with time zone");
b.Property<int>("VixRegime")
.HasColumnType("integer");
b.Property<decimal>("VixValue")
.HasColumnType("decimal(18,4)");
b.Property<double>("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<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<decimal>("CurrentPrice")
.HasColumnType("decimal(18,4)");
b.Property<string>("Reasoning")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Recommendation")
.IsRequired()
.HasMaxLength(30)
.HasColumnType("character varying(30)");
b.Property<decimal?>("SuggestedStopLoss")
.HasColumnType("decimal(18,4)");
b.Property<decimal?>("SuggestedTakeProfit")
.HasColumnType("decimal(18,4)");
b.Property<DateTime>("Timestamp")
.HasColumnType("timestamp with time zone");
b.Property<Guid>("TradeId")
.HasColumnType("uuid");
b.Property<decimal>("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<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<double>("AtrStopLossMultiplier")
.HasColumnType("double precision");
b.Property<int>("MaxOpenPositions")
.HasColumnType("integer");
b.Property<double>("RiskPerTradePercentage")
.HasColumnType("double precision");
b.Property<DateTime>("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
}
}
}
@@ -1,153 +0,0 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace FinlyticTrades.Migrations
{
/// <inheritdoc />
public partial class Init : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Settings",
columns: table => new
{
Id = table.Column<Guid>(type: "uuid", nullable: false),
AtrStopLossMultiplier = table.Column<double>(type: "double precision", nullable: false),
RiskPerTradePercentage = table.Column<double>(type: "double precision", nullable: false),
MaxOpenPositions = table.Column<int>(type: "integer", nullable: false),
UpdatedAt = table.Column<DateTime>(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<Guid>(type: "uuid", nullable: false),
TradeId = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false),
AnalysisId = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false),
EventId = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false),
Sector = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: false),
Symbol = table.Column<string>(type: "character varying(30)", maxLength: 30, nullable: false),
Isin = table.Column<string>(type: "character varying(30)", maxLength: 30, nullable: false),
CompanyName = table.Column<string>(type: "character varying(150)", maxLength: 150, nullable: false),
Status = table.Column<int>(type: "integer", nullable: false),
EntryPrice = table.Column<decimal>(type: "numeric(18,4)", nullable: false),
StopLoss = table.Column<decimal>(type: "numeric(18,4)", nullable: false),
TakeProfit = table.Column<decimal>(type: "numeric(18,4)", nullable: false),
SignalType = table.Column<string>(type: "character varying(10)", maxLength: 10, nullable: false),
RiskTolerance = table.Column<string>(type: "character varying(30)", maxLength: 30, nullable: false),
Timeframe = table.Column<string>(type: "character varying(20)", maxLength: 20, nullable: false),
InstrumentType = table.Column<string>(type: "character varying(30)", maxLength: 30, nullable: false),
WinRate = table.Column<double>(type: "double precision", nullable: false),
VixRegime = table.Column<int>(type: "integer", nullable: false),
VixValue = table.Column<decimal>(type: "numeric(18,4)", nullable: false),
TtlMinutes = table.Column<int>(type: "integer", nullable: false),
Reasoning = table.Column<string>(type: "text", nullable: false),
UserExitPrice = table.Column<decimal>(type: "numeric(18,4)", nullable: true),
UserExitTimestamp = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
CloseReason = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: true),
PnlAbsolute = table.Column<decimal>(type: "numeric(18,4)", nullable: true),
PnlPercent = table.Column<decimal>(type: "numeric(18,4)", nullable: true),
IsWin = table.Column<bool>(type: "boolean", nullable: true),
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
ClosedAt = table.Column<DateTime>(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<Guid>(type: "uuid", nullable: false),
TradeId = table.Column<Guid>(type: "uuid", nullable: false),
Recommendation = table.Column<string>(type: "character varying(30)", maxLength: 30, nullable: false),
CurrentPrice = table.Column<decimal>(type: "numeric(18,4)", nullable: false),
SuggestedStopLoss = table.Column<decimal>(type: "numeric(18,4)", nullable: true),
SuggestedTakeProfit = table.Column<decimal>(type: "numeric(18,4)", nullable: true),
VixValue = table.Column<decimal>(type: "numeric(18,4)", nullable: false),
Reasoning = table.Column<string>(type: "text", nullable: false),
Timestamp = table.Column<DateTime>(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);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "Settings");
migrationBuilder.DropTable(
name: "trade_hourly_updates");
migrationBuilder.DropTable(
name: "trades");
}
}
}
@@ -1,272 +0,0 @@
// <auto-generated />
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
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "10.0.9")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("FinlyticTrades.Entities.TradeEntity", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("AnalysisId")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<string>("CloseReason")
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<DateTime?>("ClosedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("CompanyName")
.IsRequired()
.HasMaxLength(150)
.HasColumnType("character varying(150)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<decimal>("EntryPrice")
.HasColumnType("decimal(18,4)");
b.Property<decimal?>("EntryZoneMax")
.HasColumnType("decimal(18,4)");
b.Property<decimal?>("EntryZoneMin")
.HasColumnType("decimal(18,4)");
b.Property<string>("EventId")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<string>("FundamentalRationale")
.IsRequired()
.HasColumnType("text");
b.Property<string>("InstrumentType")
.IsRequired()
.HasMaxLength(30)
.HasColumnType("character varying(30)");
b.Property<bool?>("IsWin")
.HasColumnType("boolean");
b.Property<string>("Isin")
.IsRequired()
.HasMaxLength(30)
.HasColumnType("character varying(30)");
b.Property<decimal?>("MaxLeverage")
.HasColumnType("decimal(18,4)");
b.Property<decimal?>("PnlAbsolute")
.HasColumnType("decimal(18,4)");
b.Property<decimal?>("PnlPercent")
.HasColumnType("decimal(18,4)");
b.Property<string>("Reasoning")
.IsRequired()
.HasColumnType("text");
b.Property<decimal?>("RiskRewardRatio")
.HasColumnType("decimal(18,4)");
b.Property<string>("RiskTolerance")
.IsRequired()
.HasMaxLength(30)
.HasColumnType("character varying(30)");
b.Property<string>("RiskWarning")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Sector")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<string>("SignalType")
.IsRequired()
.HasMaxLength(10)
.HasColumnType("character varying(10)");
b.Property<int>("Status")
.HasColumnType("integer");
b.Property<decimal>("StopLoss")
.HasColumnType("decimal(18,4)");
b.Property<string>("Symbol")
.IsRequired()
.HasMaxLength(30)
.HasColumnType("character varying(30)");
b.Property<decimal>("TakeProfit")
.HasColumnType("decimal(18,4)");
b.Property<string>("TakeProfitTargets")
.HasColumnType("text");
b.Property<string>("TechnicalRationale")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Timeframe")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("character varying(20)");
b.Property<string>("TradeId")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<int>("TtlMinutes")
.HasColumnType("integer");
b.Property<decimal?>("UserExitPrice")
.HasColumnType("decimal(18,4)");
b.Property<DateTime?>("UserExitTimestamp")
.HasColumnType("timestamp with time zone");
b.Property<int>("VixRegime")
.HasColumnType("integer");
b.Property<decimal>("VixValue")
.HasColumnType("decimal(18,4)");
b.Property<double>("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<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<decimal>("CurrentPrice")
.HasColumnType("decimal(18,4)");
b.Property<string>("Reasoning")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Recommendation")
.IsRequired()
.HasMaxLength(30)
.HasColumnType("character varying(30)");
b.Property<decimal?>("SuggestedStopLoss")
.HasColumnType("decimal(18,4)");
b.Property<decimal?>("SuggestedTakeProfit")
.HasColumnType("decimal(18,4)");
b.Property<DateTime>("Timestamp")
.HasColumnType("timestamp with time zone");
b.Property<Guid>("TradeId")
.HasColumnType("uuid");
b.Property<decimal>("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<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<double>("AtrStopLossMultiplier")
.HasColumnType("double precision");
b.Property<int>("MaxOpenPositions")
.HasColumnType("integer");
b.Property<double>("RiskPerTradePercentage")
.HasColumnType("double precision");
b.Property<DateTime>("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
}
}
}
@@ -1,101 +0,0 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace FinlyticTrades.Migrations
{
/// <inheritdoc />
public partial class ExpandTradeEntity : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<decimal>(
name: "EntryZoneMax",
table: "trades",
type: "numeric(18,4)",
nullable: true);
migrationBuilder.AddColumn<decimal>(
name: "EntryZoneMin",
table: "trades",
type: "numeric(18,4)",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "FundamentalRationale",
table: "trades",
type: "text",
nullable: false,
defaultValue: "");
migrationBuilder.AddColumn<decimal>(
name: "MaxLeverage",
table: "trades",
type: "numeric(18,4)",
nullable: true);
migrationBuilder.AddColumn<decimal>(
name: "RiskRewardRatio",
table: "trades",
type: "numeric(18,4)",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "RiskWarning",
table: "trades",
type: "text",
nullable: false,
defaultValue: "");
migrationBuilder.AddColumn<string>(
name: "TakeProfitTargets",
table: "trades",
type: "text",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "TechnicalRationale",
table: "trades",
type: "text",
nullable: false,
defaultValue: "");
}
/// <inheritdoc />
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");
}
}
}
@@ -1,306 +0,0 @@
// <auto-generated />
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
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "10.0.9")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("FinlyticTrades.Entities.TradeEntity", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<decimal?>("ActualEntryPrice")
.HasColumnType("decimal(18,4)");
b.Property<string>("AnalysisId")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<string>("CloseReason")
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<DateTime?>("ClosedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("CompanyName")
.IsRequired()
.HasMaxLength(150)
.HasColumnType("character varying(150)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<decimal?>("EntryFee")
.HasColumnType("decimal(18,4)");
b.Property<decimal>("EntryPrice")
.HasColumnType("decimal(18,4)");
b.Property<decimal?>("EntryZoneMax")
.HasColumnType("decimal(18,4)");
b.Property<decimal?>("EntryZoneMin")
.HasColumnType("decimal(18,4)");
b.Property<string>("EventId")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<DateTime?>("ExecutionTimestamp")
.HasColumnType("timestamp with time zone");
b.Property<decimal?>("ExitFee")
.HasColumnType("decimal(18,4)");
b.Property<string>("FundamentalRationale")
.IsRequired()
.HasColumnType("text");
b.Property<string>("InstrumentType")
.IsRequired()
.HasMaxLength(30)
.HasColumnType("character varying(30)");
b.Property<bool>("IsGlobalProposal")
.HasColumnType("boolean");
b.Property<bool>("IsRecurring")
.HasColumnType("boolean");
b.Property<bool?>("IsWin")
.HasColumnType("boolean");
b.Property<string>("Isin")
.IsRequired()
.HasMaxLength(30)
.HasColumnType("character varying(30)");
b.Property<decimal?>("KnockoutThreshold")
.HasColumnType("decimal(18,4)");
b.Property<decimal?>("LeverageUsed")
.HasColumnType("decimal(18,4)");
b.Property<decimal?>("MaxLeverage")
.HasColumnType("decimal(18,4)");
b.Property<decimal?>("PnlAbsolute")
.HasColumnType("decimal(18,4)");
b.Property<decimal?>("PnlPercent")
.HasColumnType("decimal(18,4)");
b.Property<decimal?>("PositionSize")
.HasColumnType("decimal(18,4)");
b.Property<decimal?>("Quantity")
.HasColumnType("decimal(18,4)");
b.Property<string>("Reasoning")
.IsRequired()
.HasColumnType("text");
b.Property<decimal?>("RiskRewardRatio")
.HasColumnType("decimal(18,4)");
b.Property<string>("RiskTolerance")
.IsRequired()
.HasMaxLength(30)
.HasColumnType("character varying(30)");
b.Property<string>("RiskWarning")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Sector")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<string>("SignalType")
.IsRequired()
.HasMaxLength(10)
.HasColumnType("character varying(10)");
b.Property<int>("Status")
.HasColumnType("integer");
b.Property<decimal>("StopLoss")
.HasColumnType("decimal(18,4)");
b.Property<string>("Symbol")
.IsRequired()
.HasMaxLength(30)
.HasColumnType("character varying(30)");
b.Property<decimal>("TakeProfit")
.HasColumnType("decimal(18,4)");
b.Property<string>("TakeProfitTargets")
.HasColumnType("text");
b.Property<string>("TechnicalRationale")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Timeframe")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("character varying(20)");
b.Property<string>("TradeId")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<int>("TtlMinutes")
.HasColumnType("integer");
b.Property<decimal?>("UserExitPrice")
.HasColumnType("decimal(18,4)");
b.Property<DateTime?>("UserExitTimestamp")
.HasColumnType("timestamp with time zone");
b.Property<string>("UserId")
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<int>("VixRegime")
.HasColumnType("integer");
b.Property<decimal>("VixValue")
.HasColumnType("decimal(18,4)");
b.Property<double>("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<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<decimal>("CurrentPrice")
.HasColumnType("decimal(18,4)");
b.Property<string>("Reasoning")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Recommendation")
.IsRequired()
.HasMaxLength(30)
.HasColumnType("character varying(30)");
b.Property<decimal?>("SuggestedStopLoss")
.HasColumnType("decimal(18,4)");
b.Property<decimal?>("SuggestedTakeProfit")
.HasColumnType("decimal(18,4)");
b.Property<DateTime>("Timestamp")
.HasColumnType("timestamp with time zone");
b.Property<Guid>("TradeId")
.HasColumnType("uuid");
b.Property<decimal>("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<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<double>("AtrStopLossMultiplier")
.HasColumnType("double precision");
b.Property<int>("MaxOpenPositions")
.HasColumnType("integer");
b.Property<double>("RiskPerTradePercentage")
.HasColumnType("double precision");
b.Property<DateTime>("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
}
}
}
@@ -1,132 +0,0 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace FinlyticTrades.Migrations
{
/// <inheritdoc />
public partial class AddMultiUserTradeExecution : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<decimal>(
name: "ActualEntryPrice",
table: "trades",
type: "numeric(18,4)",
nullable: true);
migrationBuilder.AddColumn<decimal>(
name: "EntryFee",
table: "trades",
type: "numeric(18,4)",
nullable: true);
migrationBuilder.AddColumn<DateTime>(
name: "ExecutionTimestamp",
table: "trades",
type: "timestamp with time zone",
nullable: true);
migrationBuilder.AddColumn<decimal>(
name: "ExitFee",
table: "trades",
type: "numeric(18,4)",
nullable: true);
migrationBuilder.AddColumn<bool>(
name: "IsGlobalProposal",
table: "trades",
type: "boolean",
nullable: false,
defaultValue: false);
migrationBuilder.AddColumn<bool>(
name: "IsRecurring",
table: "trades",
type: "boolean",
nullable: false,
defaultValue: false);
migrationBuilder.AddColumn<decimal>(
name: "KnockoutThreshold",
table: "trades",
type: "numeric(18,4)",
nullable: true);
migrationBuilder.AddColumn<decimal>(
name: "LeverageUsed",
table: "trades",
type: "numeric(18,4)",
nullable: true);
migrationBuilder.AddColumn<decimal>(
name: "PositionSize",
table: "trades",
type: "numeric(18,4)",
nullable: true);
migrationBuilder.AddColumn<decimal>(
name: "Quantity",
table: "trades",
type: "numeric(18,4)",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "UserId",
table: "trades",
type: "character varying(100)",
maxLength: 100,
nullable: true);
}
/// <inheritdoc />
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");
}
}
}
@@ -1,311 +0,0 @@
// <auto-generated />
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
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "10.0.9")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("FinlyticTrades.Entities.TradeEntity", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<decimal?>("ActualEntryPrice")
.HasColumnType("decimal(18,4)");
b.Property<string>("AnalysisId")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<string>("CloseReason")
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<DateTime?>("ClosedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("CompanyName")
.IsRequired()
.HasMaxLength(150)
.HasColumnType("character varying(150)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<decimal?>("EntryFee")
.HasColumnType("decimal(18,4)");
b.Property<decimal>("EntryPrice")
.HasColumnType("decimal(18,4)");
b.Property<decimal?>("EntryZoneMax")
.HasColumnType("decimal(18,4)");
b.Property<decimal?>("EntryZoneMin")
.HasColumnType("decimal(18,4)");
b.Property<string>("EventId")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<DateTime?>("ExecutionTimestamp")
.HasColumnType("timestamp with time zone");
b.Property<decimal?>("ExitFee")
.HasColumnType("decimal(18,4)");
b.Property<string>("FundamentalRationale")
.IsRequired()
.HasColumnType("text");
b.Property<string>("InstrumentType")
.IsRequired()
.HasMaxLength(30)
.HasColumnType("character varying(30)");
b.Property<bool>("IsGlobalProposal")
.HasColumnType("boolean");
b.Property<bool>("IsRecurring")
.HasColumnType("boolean");
b.Property<bool?>("IsWin")
.HasColumnType("boolean");
b.Property<string>("Isin")
.IsRequired()
.HasMaxLength(30)
.HasColumnType("character varying(30)");
b.Property<decimal?>("KnockoutThreshold")
.HasColumnType("decimal(18,4)");
b.Property<decimal?>("LeverageUsed")
.HasColumnType("decimal(18,4)");
b.Property<decimal?>("MaxLeverage")
.HasColumnType("decimal(18,4)");
b.Property<decimal?>("PnlAbsolute")
.HasColumnType("decimal(18,4)");
b.Property<decimal?>("PnlPercent")
.HasColumnType("decimal(18,4)");
b.Property<decimal?>("PositionSize")
.HasColumnType("decimal(18,4)");
b.Property<decimal?>("Quantity")
.HasColumnType("decimal(18,4)");
b.Property<string>("Reasoning")
.IsRequired()
.HasColumnType("text");
b.Property<decimal?>("RiskRewardRatio")
.HasColumnType("decimal(18,4)");
b.Property<string>("RiskTolerance")
.IsRequired()
.HasMaxLength(30)
.HasColumnType("character varying(30)");
b.Property<string>("RiskWarning")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Sector")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<string>("SignalType")
.IsRequired()
.HasMaxLength(10)
.HasColumnType("character varying(10)");
b.Property<int>("Status")
.HasColumnType("integer");
b.Property<decimal>("StopLoss")
.HasColumnType("decimal(18,4)");
b.Property<string>("Symbol")
.IsRequired()
.HasMaxLength(30)
.HasColumnType("character varying(30)");
b.Property<decimal>("TakeProfit")
.HasColumnType("decimal(18,4)");
b.Property<string>("TakeProfitTargets")
.HasColumnType("text");
b.Property<string>("TechnicalRationale")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Timeframe")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("character varying(20)");
b.Property<string>("TradeId")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<int>("TtlMinutes")
.HasColumnType("integer");
b.Property<decimal?>("UserExitPrice")
.HasColumnType("decimal(18,4)");
b.Property<DateTime?>("UserExitTimestamp")
.HasColumnType("timestamp with time zone");
b.Property<string>("UserId")
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<int>("VixRegime")
.HasColumnType("integer");
b.Property<decimal>("VixValue")
.HasColumnType("decimal(18,4)");
b.Property<double>("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<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<decimal>("CurrentPrice")
.HasColumnType("decimal(18,4)");
b.Property<decimal?>("FloatingPnlPercent")
.HasColumnType("decimal(18,4)");
b.Property<string>("Reasoning")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Recommendation")
.IsRequired()
.HasMaxLength(30)
.HasColumnType("character varying(30)");
b.Property<decimal?>("SuggestedStopLoss")
.HasColumnType("decimal(18,4)");
b.Property<decimal?>("SuggestedTakeProfit")
.HasColumnType("decimal(18,4)");
b.Property<DateTime>("Timestamp")
.HasColumnType("timestamp with time zone");
b.Property<Guid>("TradeId")
.HasColumnType("uuid");
b.Property<decimal>("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<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<double>("AtrStopLossMultiplier")
.HasColumnType("double precision");
b.Property<int>("MaxOpenPositions")
.HasColumnType("integer");
b.Property<double>("RiskPerTradePercentage")
.HasColumnType("double precision");
b.Property<DateTime>("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
}
}
}
@@ -1,37 +0,0 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace FinlyticTrades.Migrations
{
/// <inheritdoc />
public partial class AddIndexToTradeHourlyUpdate : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<decimal>(
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" });
}
/// <inheritdoc />
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");
}
}
}
@@ -1,315 +0,0 @@
// <auto-generated />
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
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "10.0.9")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("FinlyticTrades.Entities.TradeEntity", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<decimal?>("ActualEntryPrice")
.HasColumnType("decimal(18,4)");
b.Property<string>("AnalysisId")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<string>("CloseReason")
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<DateTime?>("ClosedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("CompanyName")
.IsRequired()
.HasMaxLength(150)
.HasColumnType("character varying(150)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("DerivativeIsin")
.HasMaxLength(20)
.HasColumnType("character varying(20)");
b.Property<decimal?>("EntryFee")
.HasColumnType("decimal(18,4)");
b.Property<decimal>("EntryPrice")
.HasColumnType("decimal(18,4)");
b.Property<decimal?>("EntryZoneMax")
.HasColumnType("decimal(18,4)");
b.Property<decimal?>("EntryZoneMin")
.HasColumnType("decimal(18,4)");
b.Property<string>("EventId")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<DateTime?>("ExecutionTimestamp")
.HasColumnType("timestamp with time zone");
b.Property<decimal?>("ExitFee")
.HasColumnType("decimal(18,4)");
b.Property<string>("FundamentalRationale")
.IsRequired()
.HasColumnType("text");
b.Property<string>("InstrumentType")
.IsRequired()
.HasMaxLength(30)
.HasColumnType("character varying(30)");
b.Property<bool>("IsGlobalProposal")
.HasColumnType("boolean");
b.Property<bool>("IsRecurring")
.HasColumnType("boolean");
b.Property<bool?>("IsWin")
.HasColumnType("boolean");
b.Property<string>("Isin")
.IsRequired()
.HasMaxLength(30)
.HasColumnType("character varying(30)");
b.Property<decimal?>("KnockoutThreshold")
.HasColumnType("decimal(18,4)");
b.Property<decimal?>("LeverageUsed")
.HasColumnType("decimal(18,4)");
b.Property<decimal?>("MaxLeverage")
.HasColumnType("decimal(18,4)");
b.Property<decimal?>("PnlAbsolute")
.HasColumnType("decimal(18,4)");
b.Property<decimal?>("PnlPercent")
.HasColumnType("decimal(18,4)");
b.Property<decimal?>("PositionSize")
.HasColumnType("decimal(18,4)");
b.Property<decimal?>("Quantity")
.HasColumnType("decimal(18,4)");
b.Property<string>("Reasoning")
.IsRequired()
.HasColumnType("text");
b.Property<decimal?>("RiskRewardRatio")
.HasColumnType("decimal(18,4)");
b.Property<string>("RiskTolerance")
.IsRequired()
.HasMaxLength(30)
.HasColumnType("character varying(30)");
b.Property<string>("RiskWarning")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Sector")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<string>("SignalType")
.IsRequired()
.HasMaxLength(10)
.HasColumnType("character varying(10)");
b.Property<int>("Status")
.HasColumnType("integer");
b.Property<decimal>("StopLoss")
.HasColumnType("decimal(18,4)");
b.Property<string>("Symbol")
.IsRequired()
.HasMaxLength(30)
.HasColumnType("character varying(30)");
b.Property<decimal>("TakeProfit")
.HasColumnType("decimal(18,4)");
b.Property<string>("TakeProfitTargets")
.HasColumnType("text");
b.Property<string>("TechnicalRationale")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Timeframe")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("character varying(20)");
b.Property<string>("TradeId")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<int>("TtlMinutes")
.HasColumnType("integer");
b.Property<decimal?>("UserExitPrice")
.HasColumnType("decimal(18,4)");
b.Property<DateTime?>("UserExitTimestamp")
.HasColumnType("timestamp with time zone");
b.Property<string>("UserId")
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<int>("VixRegime")
.HasColumnType("integer");
b.Property<decimal>("VixValue")
.HasColumnType("decimal(18,4)");
b.Property<double>("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<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<decimal>("CurrentPrice")
.HasColumnType("decimal(18,4)");
b.Property<decimal?>("FloatingPnlPercent")
.HasColumnType("decimal(18,4)");
b.Property<string>("Reasoning")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Recommendation")
.IsRequired()
.HasMaxLength(30)
.HasColumnType("character varying(30)");
b.Property<decimal?>("SuggestedStopLoss")
.HasColumnType("decimal(18,4)");
b.Property<decimal?>("SuggestedTakeProfit")
.HasColumnType("decimal(18,4)");
b.Property<DateTime>("Timestamp")
.HasColumnType("timestamp with time zone");
b.Property<Guid>("TradeId")
.HasColumnType("uuid");
b.Property<decimal>("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<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<double>("AtrStopLossMultiplier")
.HasColumnType("double precision");
b.Property<int>("MaxOpenPositions")
.HasColumnType("integer");
b.Property<double>("RiskPerTradePercentage")
.HasColumnType("double precision");
b.Property<DateTime>("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
}
}
}
@@ -1,29 +0,0 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace FinlyticTrades.Migrations
{
/// <inheritdoc />
public partial class DerivativeIsin : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "DerivativeIsin",
table: "trades",
type: "character varying(20)",
maxLength: 20,
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "DerivativeIsin",
table: "trades");
}
}
}
@@ -1,315 +0,0 @@
// <auto-generated />
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
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "10.0.9")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("FinlyticTrades.Entities.TradeEntity", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<decimal?>("ActualEntryPrice")
.HasColumnType("decimal(18,4)");
b.Property<string>("AnalysisId")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<string>("CloseReason")
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<DateTime?>("ClosedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("CompanyName")
.IsRequired()
.HasMaxLength(150)
.HasColumnType("character varying(150)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("DerivativeIsin")
.HasMaxLength(20)
.HasColumnType("character varying(20)");
b.Property<decimal?>("EntryFee")
.HasColumnType("decimal(18,4)");
b.Property<decimal>("EntryPrice")
.HasColumnType("decimal(18,4)");
b.Property<decimal?>("EntryZoneMax")
.HasColumnType("decimal(18,4)");
b.Property<decimal?>("EntryZoneMin")
.HasColumnType("decimal(18,4)");
b.Property<string>("EventId")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<DateTime?>("ExecutionTimestamp")
.HasColumnType("timestamp with time zone");
b.Property<decimal?>("ExitFee")
.HasColumnType("decimal(18,4)");
b.Property<string>("FundamentalRationale")
.IsRequired()
.HasColumnType("text");
b.Property<string>("InstrumentType")
.IsRequired()
.HasMaxLength(30)
.HasColumnType("character varying(30)");
b.Property<bool>("IsGlobalProposal")
.HasColumnType("boolean");
b.Property<bool>("IsRecurring")
.HasColumnType("boolean");
b.Property<bool?>("IsWin")
.HasColumnType("boolean");
b.Property<string>("Isin")
.IsRequired()
.HasMaxLength(30)
.HasColumnType("character varying(30)");
b.Property<decimal?>("KnockoutThreshold")
.HasColumnType("decimal(18,4)");
b.Property<decimal?>("LeverageUsed")
.HasColumnType("decimal(18,4)");
b.Property<decimal?>("MaxLeverage")
.HasColumnType("decimal(18,4)");
b.Property<decimal?>("PnlAbsolute")
.HasColumnType("decimal(18,4)");
b.Property<decimal?>("PnlPercent")
.HasColumnType("decimal(18,4)");
b.Property<decimal?>("PositionSize")
.HasColumnType("decimal(18,4)");
b.Property<decimal?>("Quantity")
.HasColumnType("decimal(18,4)");
b.Property<string>("Reasoning")
.IsRequired()
.HasColumnType("text");
b.Property<decimal?>("RiskRewardRatio")
.HasColumnType("decimal(18,4)");
b.Property<string>("RiskTolerance")
.IsRequired()
.HasMaxLength(30)
.HasColumnType("character varying(30)");
b.Property<string>("RiskWarning")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Sector")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<string>("SignalType")
.IsRequired()
.HasMaxLength(10)
.HasColumnType("character varying(10)");
b.Property<int>("Status")
.HasColumnType("integer");
b.Property<decimal>("StopLoss")
.HasColumnType("decimal(18,4)");
b.Property<string>("Symbol")
.IsRequired()
.HasMaxLength(30)
.HasColumnType("character varying(30)");
b.Property<decimal>("TakeProfit")
.HasColumnType("decimal(18,4)");
b.Property<string>("TakeProfitTargets")
.HasColumnType("text");
b.Property<string>("TechnicalRationale")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Timeframe")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("character varying(20)");
b.Property<string>("TradeId")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<int>("TtlMinutes")
.HasColumnType("integer");
b.Property<decimal?>("UserExitPrice")
.HasColumnType("decimal(18,4)");
b.Property<DateTime?>("UserExitTimestamp")
.HasColumnType("timestamp with time zone");
b.Property<string>("UserId")
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<int>("VixRegime")
.HasColumnType("integer");
b.Property<decimal>("VixValue")
.HasColumnType("decimal(18,4)");
b.Property<double>("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<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<decimal>("CurrentPrice")
.HasColumnType("decimal(18,4)");
b.Property<decimal?>("FloatingPnlPercent")
.HasColumnType("decimal(18,4)");
b.Property<string>("Reasoning")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Recommendation")
.IsRequired()
.HasMaxLength(30)
.HasColumnType("character varying(30)");
b.Property<decimal?>("SuggestedStopLoss")
.HasColumnType("decimal(18,4)");
b.Property<decimal?>("SuggestedTakeProfit")
.HasColumnType("decimal(18,4)");
b.Property<DateTime>("Timestamp")
.HasColumnType("timestamp with time zone");
b.Property<Guid>("TradeId")
.HasColumnType("uuid");
b.Property<decimal>("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<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<double>("AtrStopLossMultiplier")
.HasColumnType("double precision");
b.Property<int>("MaxOpenPositions")
.HasColumnType("integer");
b.Property<double>("RiskPerTradePercentage")
.HasColumnType("double precision");
b.Property<DateTime>("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
}
}
}
@@ -1,22 +0,0 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace FinlyticTrades.Migrations
{
/// <inheritdoc />
public partial class CheckPendingTrades : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
}
}
}
@@ -1,357 +0,0 @@
// <auto-generated />
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
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "10.0.9")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("FinlyticCore.Entities.Settings.SettingEntity", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("Key")
.IsRequired()
.HasMaxLength(150)
.HasColumnType("character varying(150)");
b.Property<DateTime>("LastUpdatedUtc")
.HasColumnType("timestamp with time zone");
b.Property<string>("ServiceIdentifier")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<string>("ValueJson")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("Key");
b.ToTable("DynamicSettings");
});
modelBuilder.Entity("FinlyticTrades.Entities.TradeEntity", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<decimal?>("ActualEntryPrice")
.HasColumnType("decimal(18,4)");
b.Property<string>("AnalysisId")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<string>("AssetType")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<string>("CloseReason")
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<DateTime?>("ClosedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("CompanyName")
.IsRequired()
.HasMaxLength(150)
.HasColumnType("character varying(150)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("DerivativeIsin")
.HasMaxLength(20)
.HasColumnType("character varying(20)");
b.Property<string>("DerivativeProductCategories")
.IsRequired()
.HasColumnType("text");
b.Property<decimal?>("EntryFee")
.HasColumnType("decimal(18,4)");
b.Property<decimal>("EntryPrice")
.HasColumnType("decimal(18,4)");
b.Property<decimal?>("EntryZoneMax")
.HasColumnType("decimal(18,4)");
b.Property<decimal?>("EntryZoneMin")
.HasColumnType("decimal(18,4)");
b.Property<string>("EventId")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<DateTime?>("ExecutionTimestamp")
.HasColumnType("timestamp with time zone");
b.Property<decimal?>("ExitFee")
.HasColumnType("decimal(18,4)");
b.Property<string>("FundamentalRationale")
.IsRequired()
.HasColumnType("text");
b.Property<bool>("HasCfd")
.HasColumnType("boolean");
b.Property<string>("InstrumentType")
.IsRequired()
.HasMaxLength(30)
.HasColumnType("character varying(30)");
b.Property<bool>("IsGlobalProposal")
.HasColumnType("boolean");
b.Property<bool>("IsRecurring")
.HasColumnType("boolean");
b.Property<bool?>("IsWin")
.HasColumnType("boolean");
b.Property<string>("Isin")
.IsRequired()
.HasMaxLength(30)
.HasColumnType("character varying(30)");
b.Property<decimal?>("KnockoutThreshold")
.HasColumnType("decimal(18,4)");
b.Property<decimal?>("LeverageUsed")
.HasColumnType("decimal(18,4)");
b.Property<decimal?>("MaxLeverage")
.HasColumnType("decimal(18,4)");
b.Property<decimal?>("PnlAbsolute")
.HasColumnType("decimal(18,4)");
b.Property<decimal?>("PnlPercent")
.HasColumnType("decimal(18,4)");
b.Property<decimal?>("PositionSize")
.HasColumnType("decimal(18,4)");
b.Property<decimal?>("Quantity")
.HasColumnType("decimal(18,4)");
b.Property<string>("Reasoning")
.IsRequired()
.HasColumnType("text");
b.Property<decimal?>("RiskRewardRatio")
.HasColumnType("decimal(18,4)");
b.Property<string>("RiskTolerance")
.IsRequired()
.HasMaxLength(30)
.HasColumnType("character varying(30)");
b.Property<string>("RiskWarning")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Sector")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<string>("SignalType")
.IsRequired()
.HasMaxLength(10)
.HasColumnType("character varying(10)");
b.Property<int>("Status")
.HasColumnType("integer");
b.Property<decimal>("StopLoss")
.HasColumnType("decimal(18,4)");
b.Property<string>("Symbol")
.IsRequired()
.HasMaxLength(30)
.HasColumnType("character varying(30)");
b.Property<decimal>("TakeProfit")
.HasColumnType("decimal(18,4)");
b.Property<string>("TakeProfitTargets")
.HasColumnType("text");
b.Property<string>("TechnicalRationale")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Timeframe")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("character varying(20)");
b.Property<string>("TradeId")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<int>("TtlMinutes")
.HasColumnType("integer");
b.Property<decimal?>("UserExitPrice")
.HasColumnType("decimal(18,4)");
b.Property<DateTime?>("UserExitTimestamp")
.HasColumnType("timestamp with time zone");
b.Property<string>("UserId")
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<int>("VixRegime")
.HasColumnType("integer");
b.Property<decimal>("VixValue")
.HasColumnType("decimal(18,4)");
b.Property<double>("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<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<decimal>("CurrentPrice")
.HasColumnType("decimal(18,4)");
b.Property<decimal?>("FloatingPnlPercent")
.HasColumnType("decimal(18,4)");
b.Property<string>("Reasoning")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Recommendation")
.IsRequired()
.HasMaxLength(30)
.HasColumnType("character varying(30)");
b.Property<decimal?>("SuggestedStopLoss")
.HasColumnType("decimal(18,4)");
b.Property<decimal?>("SuggestedTakeProfit")
.HasColumnType("decimal(18,4)");
b.Property<DateTime>("Timestamp")
.HasColumnType("timestamp with time zone");
b.Property<Guid>("TradeId")
.HasColumnType("uuid");
b.Property<decimal>("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<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<double>("AtrStopLossMultiplier")
.HasColumnType("double precision");
b.Property<int>("MaxOpenPositions")
.HasColumnType("integer");
b.Property<double>("RiskPerTradePercentage")
.HasColumnType("double precision");
b.Property<DateTime>("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
}
}
}
@@ -1,76 +0,0 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace FinlyticTrades.Migrations
{
/// <inheritdoc />
public partial class AddAssetTypeAndDerivativeCategoriesToTrades : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "AssetType",
table: "trades",
type: "character varying(50)",
maxLength: 50,
nullable: false,
defaultValue: "");
migrationBuilder.AddColumn<string>(
name: "DerivativeProductCategories",
table: "trades",
type: "text",
nullable: false,
defaultValue: "");
migrationBuilder.AddColumn<bool>(
name: "HasCfd",
table: "trades",
type: "boolean",
nullable: false,
defaultValue: false);
migrationBuilder.CreateTable(
name: "DynamicSettings",
columns: table => new
{
Id = table.Column<Guid>(type: "uuid", nullable: false),
Key = table.Column<string>(type: "character varying(150)", maxLength: 150, nullable: false),
ValueJson = table.Column<string>(type: "text", nullable: false),
ServiceIdentifier = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false),
LastUpdatedUtc = table.Column<DateTime>(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");
}
/// <inheritdoc />
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");
}
}
}
@@ -1,358 +0,0 @@
// <auto-generated />
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
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "10.0.9")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("FinlyticCore.Entities.Settings.SettingEntity", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("Key")
.IsRequired()
.HasMaxLength(150)
.HasColumnType("character varying(150)");
b.Property<DateTime>("LastUpdatedUtc")
.HasColumnType("timestamp with time zone");
b.Property<string>("ServiceIdentifier")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<string>("ValueJson")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("Key")
.IsUnique();
b.ToTable("DynamicSettings");
});
modelBuilder.Entity("FinlyticTrades.Entities.TradeEntity", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<decimal?>("ActualEntryPrice")
.HasColumnType("decimal(18,4)");
b.Property<string>("AnalysisId")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<string>("AssetType")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<string>("CloseReason")
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<DateTime?>("ClosedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("CompanyName")
.IsRequired()
.HasMaxLength(150)
.HasColumnType("character varying(150)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("DerivativeIsin")
.HasMaxLength(20)
.HasColumnType("character varying(20)");
b.Property<string>("DerivativeProductCategories")
.IsRequired()
.HasColumnType("text");
b.Property<decimal?>("EntryFee")
.HasColumnType("decimal(18,4)");
b.Property<decimal>("EntryPrice")
.HasColumnType("decimal(18,4)");
b.Property<decimal?>("EntryZoneMax")
.HasColumnType("decimal(18,4)");
b.Property<decimal?>("EntryZoneMin")
.HasColumnType("decimal(18,4)");
b.Property<string>("EventId")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<DateTime?>("ExecutionTimestamp")
.HasColumnType("timestamp with time zone");
b.Property<decimal?>("ExitFee")
.HasColumnType("decimal(18,4)");
b.Property<string>("FundamentalRationale")
.IsRequired()
.HasColumnType("text");
b.Property<bool>("HasCfd")
.HasColumnType("boolean");
b.Property<string>("InstrumentType")
.IsRequired()
.HasMaxLength(30)
.HasColumnType("character varying(30)");
b.Property<bool>("IsGlobalProposal")
.HasColumnType("boolean");
b.Property<bool>("IsRecurring")
.HasColumnType("boolean");
b.Property<bool?>("IsWin")
.HasColumnType("boolean");
b.Property<string>("Isin")
.IsRequired()
.HasMaxLength(30)
.HasColumnType("character varying(30)");
b.Property<decimal?>("KnockoutThreshold")
.HasColumnType("decimal(18,4)");
b.Property<decimal?>("LeverageUsed")
.HasColumnType("decimal(18,4)");
b.Property<decimal?>("MaxLeverage")
.HasColumnType("decimal(18,4)");
b.Property<decimal?>("PnlAbsolute")
.HasColumnType("decimal(18,4)");
b.Property<decimal?>("PnlPercent")
.HasColumnType("decimal(18,4)");
b.Property<decimal?>("PositionSize")
.HasColumnType("decimal(18,4)");
b.Property<decimal?>("Quantity")
.HasColumnType("decimal(18,4)");
b.Property<string>("Reasoning")
.IsRequired()
.HasColumnType("text");
b.Property<decimal?>("RiskRewardRatio")
.HasColumnType("decimal(18,4)");
b.Property<string>("RiskTolerance")
.IsRequired()
.HasMaxLength(30)
.HasColumnType("character varying(30)");
b.Property<string>("RiskWarning")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Sector")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<string>("SignalType")
.IsRequired()
.HasMaxLength(10)
.HasColumnType("character varying(10)");
b.Property<int>("Status")
.HasColumnType("integer");
b.Property<decimal>("StopLoss")
.HasColumnType("decimal(18,4)");
b.Property<string>("Symbol")
.IsRequired()
.HasMaxLength(30)
.HasColumnType("character varying(30)");
b.Property<decimal>("TakeProfit")
.HasColumnType("decimal(18,4)");
b.Property<string>("TakeProfitTargets")
.HasColumnType("text");
b.Property<string>("TechnicalRationale")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Timeframe")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("character varying(20)");
b.Property<string>("TradeId")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<int>("TtlMinutes")
.HasColumnType("integer");
b.Property<decimal?>("UserExitPrice")
.HasColumnType("decimal(18,4)");
b.Property<DateTime?>("UserExitTimestamp")
.HasColumnType("timestamp with time zone");
b.Property<string>("UserId")
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<int>("VixRegime")
.HasColumnType("integer");
b.Property<decimal>("VixValue")
.HasColumnType("decimal(18,4)");
b.Property<double>("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<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<decimal>("CurrentPrice")
.HasColumnType("decimal(18,4)");
b.Property<decimal?>("FloatingPnlPercent")
.HasColumnType("decimal(18,4)");
b.Property<string>("Reasoning")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Recommendation")
.IsRequired()
.HasMaxLength(30)
.HasColumnType("character varying(30)");
b.Property<decimal?>("SuggestedStopLoss")
.HasColumnType("decimal(18,4)");
b.Property<decimal?>("SuggestedTakeProfit")
.HasColumnType("decimal(18,4)");
b.Property<DateTime>("Timestamp")
.HasColumnType("timestamp with time zone");
b.Property<Guid>("TradeId")
.HasColumnType("uuid");
b.Property<decimal>("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<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<double>("AtrStopLossMultiplier")
.HasColumnType("double precision");
b.Property<int>("MaxOpenPositions")
.HasColumnType("integer");
b.Property<double>("RiskPerTradePercentage")
.HasColumnType("double precision");
b.Property<DateTime>("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
}
}
}
@@ -1,37 +0,0 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace FinlyticTrades.Migrations
{
/// <inheritdoc />
public partial class AddDynamicSettings : Migration
{
/// <inheritdoc />
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);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
name: "IX_DynamicSettings_Key",
table: "DynamicSettings");
migrationBuilder.CreateIndex(
name: "IX_DynamicSettings_Key",
table: "DynamicSettings",
column: "Key");
}
}
}
@@ -1,355 +0,0 @@
// <auto-generated />
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<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("Key")
.IsRequired()
.HasMaxLength(150)
.HasColumnType("character varying(150)");
b.Property<DateTime>("LastUpdatedUtc")
.HasColumnType("timestamp with time zone");
b.Property<string>("ServiceIdentifier")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<string>("ValueJson")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("Key")
.IsUnique();
b.ToTable("DynamicSettings");
});
modelBuilder.Entity("FinlyticTrades.Entities.TradeEntity", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<decimal?>("ActualEntryPrice")
.HasColumnType("decimal(18,4)");
b.Property<string>("AnalysisId")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<string>("AssetType")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<string>("CloseReason")
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<DateTime?>("ClosedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("CompanyName")
.IsRequired()
.HasMaxLength(150)
.HasColumnType("character varying(150)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("DerivativeIsin")
.HasMaxLength(20)
.HasColumnType("character varying(20)");
b.Property<string>("DerivativeProductCategories")
.IsRequired()
.HasColumnType("text");
b.Property<decimal?>("EntryFee")
.HasColumnType("decimal(18,4)");
b.Property<decimal>("EntryPrice")
.HasColumnType("decimal(18,4)");
b.Property<decimal?>("EntryZoneMax")
.HasColumnType("decimal(18,4)");
b.Property<decimal?>("EntryZoneMin")
.HasColumnType("decimal(18,4)");
b.Property<string>("EventId")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<DateTime?>("ExecutionTimestamp")
.HasColumnType("timestamp with time zone");
b.Property<decimal?>("ExitFee")
.HasColumnType("decimal(18,4)");
b.Property<string>("FundamentalRationale")
.IsRequired()
.HasColumnType("text");
b.Property<bool>("HasCfd")
.HasColumnType("boolean");
b.Property<string>("InstrumentType")
.IsRequired()
.HasMaxLength(30)
.HasColumnType("character varying(30)");
b.Property<bool>("IsGlobalProposal")
.HasColumnType("boolean");
b.Property<bool>("IsRecurring")
.HasColumnType("boolean");
b.Property<bool?>("IsWin")
.HasColumnType("boolean");
b.Property<string>("Isin")
.IsRequired()
.HasMaxLength(30)
.HasColumnType("character varying(30)");
b.Property<decimal?>("KnockoutThreshold")
.HasColumnType("decimal(18,4)");
b.Property<decimal?>("LeverageUsed")
.HasColumnType("decimal(18,4)");
b.Property<decimal?>("MaxLeverage")
.HasColumnType("decimal(18,4)");
b.Property<decimal?>("PnlAbsolute")
.HasColumnType("decimal(18,4)");
b.Property<decimal?>("PnlPercent")
.HasColumnType("decimal(18,4)");
b.Property<decimal?>("PositionSize")
.HasColumnType("decimal(18,4)");
b.Property<decimal?>("Quantity")
.HasColumnType("decimal(18,4)");
b.Property<string>("Reasoning")
.IsRequired()
.HasColumnType("text");
b.Property<decimal?>("RiskRewardRatio")
.HasColumnType("decimal(18,4)");
b.Property<string>("RiskTolerance")
.IsRequired()
.HasMaxLength(30)
.HasColumnType("character varying(30)");
b.Property<string>("RiskWarning")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Sector")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<string>("SignalType")
.IsRequired()
.HasMaxLength(10)
.HasColumnType("character varying(10)");
b.Property<int>("Status")
.HasColumnType("integer");
b.Property<decimal>("StopLoss")
.HasColumnType("decimal(18,4)");
b.Property<string>("Symbol")
.IsRequired()
.HasMaxLength(30)
.HasColumnType("character varying(30)");
b.Property<decimal>("TakeProfit")
.HasColumnType("decimal(18,4)");
b.Property<string>("TakeProfitTargets")
.HasColumnType("text");
b.Property<string>("TechnicalRationale")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Timeframe")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("character varying(20)");
b.Property<string>("TradeId")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<int>("TtlMinutes")
.HasColumnType("integer");
b.Property<decimal?>("UserExitPrice")
.HasColumnType("decimal(18,4)");
b.Property<DateTime?>("UserExitTimestamp")
.HasColumnType("timestamp with time zone");
b.Property<string>("UserId")
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<int>("VixRegime")
.HasColumnType("integer");
b.Property<decimal>("VixValue")
.HasColumnType("decimal(18,4)");
b.Property<double>("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<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<decimal>("CurrentPrice")
.HasColumnType("decimal(18,4)");
b.Property<decimal?>("FloatingPnlPercent")
.HasColumnType("decimal(18,4)");
b.Property<string>("Reasoning")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Recommendation")
.IsRequired()
.HasMaxLength(30)
.HasColumnType("character varying(30)");
b.Property<decimal?>("SuggestedStopLoss")
.HasColumnType("decimal(18,4)");
b.Property<decimal?>("SuggestedTakeProfit")
.HasColumnType("decimal(18,4)");
b.Property<DateTime>("Timestamp")
.HasColumnType("timestamp with time zone");
b.Property<Guid>("TradeId")
.HasColumnType("uuid");
b.Property<decimal>("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<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<double>("AtrStopLossMultiplier")
.HasColumnType("double precision");
b.Property<int>("MaxOpenPositions")
.HasColumnType("integer");
b.Property<double>("RiskPerTradePercentage")
.HasColumnType("double precision");
b.Property<DateTime>("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
}
}
}
-49
View File
@@ -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<TradesDbContext>(options =>
options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection")));
builder.Services.AddScoped<ISettingsDbContext>(sp => sp.GetRequiredService<TradesDbContext>());
// 2. Core Services
builder.Services.AddSingleton<ISettingsService, SettingsService>();
builder.Services.AddSingleton(typeof(IFinlyticLogger<>), typeof(FinlyticLogger<>));
// 3. Domain Services (Scoped)
builder.Services.AddScoped<ITradeLifecycleService, TradeLifecycleService>();
builder.Services.AddScoped<ISettingsDbService, SettingsDbService>();
// 4. Hosted Services / Singletons
builder.Services.AddSingleton<TradesMqttClient>();
builder.Services.AddHostedService(sp => sp.GetRequiredService<TradesMqttClient>());
builder.Services.AddHostedService<FeedbackExporterEngine>();
var host = builder.Build();
// DB Migrations ausführen
using (var scope = host.Services.CreateScope())
{
try
{
var context = scope.ServiceProvider.GetRequiredService<TradesDbContext>();
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();
-36
View File
@@ -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).
@@ -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
{
/// <summary>
/// Exports feedback data for closed trades.
/// </summary>
Task ExportFeedbackDataAsync(CancellationToken cancellationToken = default);
}
public class FeedbackExporterEngine : BackgroundService, IFeedbackExporterEngine
{
private readonly IServiceScopeFactory _scopeFactory;
private readonly IFinlyticLogger<FeedbackExporterEngine> _finlyticLogger;
private readonly string _feedbackDir;
public FeedbackExporterEngine(IServiceScopeFactory scopeFactory, IFinlyticLogger<FeedbackExporterEngine> 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.");
}
/// <summary>
/// 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.
/// </summary>
public async Task ExportFeedbackDataAsync(CancellationToken cancellationToken = default)
{
using var scope = _scopeFactory.CreateScope();
var dbContext = scope.ServiceProvider.GetRequiredService<TradesDbContext>();
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<TradeFeedbackRecord>();
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;
}
}
@@ -1,94 +0,0 @@
using FinlyticTrades.Database;
using FinlyticTrades.Entities;
using Microsoft.EntityFrameworkCore;
namespace FinlyticTrades.Services;
public interface ISettingsDbService
{
/// <summary>
/// Gets the current settings.
/// </summary>
Task<TradesSettingsEntity> GetSettingsAsync();
/// <summary>
/// Saves the provided settings.
/// </summary>
Task<TradesSettingsEntity> SaveSettingsAsync(TradesSettingsEntity settings);
/// <summary>
/// Updates settings from a dictionary of key-value pairs.
/// </summary>
Task UpdateSettingsFromDictionaryAsync(Dictionary<string, string> dictionary);
}
public class SettingsDbService : ISettingsDbService
{
private readonly TradesDbContext _context;
/// <summary>
/// Initializes a new instance of the SettingsDbService class.
/// </summary>
public SettingsDbService(TradesDbContext context)
{
_context = context;
}
/// <summary>
/// Gets the current settings.
/// </summary>
public async Task<TradesSettingsEntity> 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;
}
/// <summary>
/// Saves the provided settings.
/// </summary>
public async Task<TradesSettingsEntity> 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;
}
/// <summary>
/// Updates settings from a dictionary of key-value pairs.
/// </summary>
public async Task UpdateSettingsFromDictionaryAsync(Dictionary<string, string> 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);
}
}
@@ -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<bool> ProcessProposedTradeAsync(TradeProposalDto proposal, CancellationToken cancellationToken = default);
Task<bool> ProcessManualAnalysisResponseAsync(ManualAnalysisResponseDto response, string userId, CancellationToken cancellationToken = default);
Task<TradeEntity?> AcceptTradeAsync(TradeAcceptanceDto request, CancellationToken cancellationToken = default);
Task AddHourlyUpdateAsync(TradeHourlyUpdateDto update, CancellationToken cancellationToken = default);
Task<List<TradeEntity>> GetActiveTradesAsync(string? userId = null, CancellationToken cancellationToken = default);
Task<List<TradeEntity>> GetTradesAsync(string? isin, string? status, string? userId = null, CancellationToken cancellationToken = default);
Task<TradeEntity?> CloseTradeAsync(string tradeId, CloseTradeRequest request, CancellationToken cancellationToken = default);
Task<TradeEntity?> 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<TradeLifecycleService> _finlyticLogger;
public TradeLifecycleService(TradesDbContext dbContext, IFinlyticLogger<TradeLifecycleService> finlyticLogger)
{
_dbContext = dbContext;
_finlyticLogger = finlyticLogger;
}
public async Task<bool> 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<bool> 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<TradeEntity?> 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<string>(proposal.DerivativeProductCategories) : new List<string>()
};
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<List<TradeEntity>> 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<List<TradeEntity>> 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<TradeStatus>(status, true, out var parsedStatus))
{
query = query.Where(t => t.Status == parsedStatus);
}
return await query.OrderByDescending(t => t.CreatedAt).ToListAsync(cancellationToken);
}
public async Task<TradeEntity?> 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<TradeEntity?> 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;
}
}
-22
View File
@@ -1,22 +0,0 @@
using FinlyticCore.Models.Settings;
namespace FinlyticTrades.Util;
public static class SettingKeys
{
// --- Logging-Kanäle ---
public static readonly SettingKey<bool> TradesChannel = new("Logging.Channel.Trades", true);
public static readonly SettingKey<bool> MqttChannel = new("Logging.Channel.MQTT", true);
public static readonly SettingKey<bool> HealthPingChannel = new("Logging.Channel.Health", true);
// --- Trade Management & Limits ---
public static readonly SettingKey<int> MaxActiveTradesCount = new("Trades.MaxActiveTradesCount", 20);
public static readonly SettingKey<int> AutoArchiveClosedTradesDays = new("Trades.AutoArchiveClosedTradesDays", 30);
public static readonly SettingKey<double> DefaultSlippageTolerancePercent = new("Trades.DefaultSlippageTolerancePercent", 0.5);
public static readonly SettingKey<int> ProposedTradeExpirationHours = new("Trades.ProposedTradeExpirationHours", 24);
// --- Parquet / Data Export ---
public static readonly SettingKey<bool> EnableParquetExport = new("Export.EnableParquetExport", true);
public static readonly SettingKey<int> ParquetExportIntervalHours = new("Export.ParquetExportIntervalHours", 6);
public static readonly SettingKey<string> ParquetExportDirectory = new("Export.ParquetExportDirectory", "data/exports/trades");
}
-456
View File
@@ -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<TradesMqttClient> _logger;
public TradesMqttClient(
IConfiguration configuration,
IServiceScopeFactory scopeFactory,
ILogger<TradesMqttClient> 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<IFinlyticLogger<TradesMqttClient>>();
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<ISettingsService>();
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<ITradeLifecycleService>();
var finlyticLoggerInstance = msgScope.ServiceProvider.GetRequiredService<IFinlyticLogger<TradesMqttClient>>();
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<IFinlyticLogger<TradesMqttClient>>();
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<IFinlyticLogger<TradesMqttClient>>();
var settingsService = scope.ServiceProvider.GetRequiredService<ISettingsService>();
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<IFinlyticLogger<TradesMqttClient>>();
var settingsService = scope.ServiceProvider.GetRequiredService<ISettingsService>();
await finlyticLogger.LogInfoAsync(SettingKeys.MqttChannel, "[FinlyticTrades] [Settings_Update] Processing settings update RPC [CorrelationId: {CorrelationId}]", correlationId);
try
{
Dictionary<string, object?>? updates = null;
try
{
updates = JsonSerializer.Deserialize<Dictionary<string, object?>>(payload);
}
catch
{
var list = JsonSerializer.Deserialize<List<DynamicSettingDto>>(payload);
if (list != null)
{
updates = new Dictionary<string, object?>();
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<LivePriceDto?> FetchLivePriceAsync(string isin)
{
if (string.IsNullOrWhiteSpace(isin)) return null;
try
{
return await SendRpcRequestAsync<LivePriceDto, IsinRequest>(
"tr_GetLivePrice",
new IsinRequest(isin),
TimeSpan.FromMilliseconds(1200));
}
catch
{
return null;
}
}
private static TradeProposalDto MapToDto(TradeEntity t)
{
List<decimal>? parseTakeProfitTargets()
{
if (string.IsNullOrWhiteSpace(t.TakeProfitTargets)) return null;
var list = new List<decimal>();
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<string>(),
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()
};
}
}
-78
View File
@@ -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.
+217 -62
View File
@@ -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
+15 -3
View File
@@ -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:<ver>`:
# * 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 ""
-29
View File
@@ -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)