chore: remove deprecated microservices, update solution and docker compose configurations
This commit is contained in:
@@ -23,3 +23,16 @@
|
|||||||
**/values.dev.yaml
|
**/values.dev.yaml
|
||||||
LICENSE
|
LICENSE
|
||||||
README.md
|
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/
|
||||||
@@ -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
@@ -15,15 +15,19 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FinlyticFundamentals", "Fin
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FinlyticSentiment", "FinlyticSentiment\FinlyticSentiment.csproj", "{9C3BB705-86AD-4A89-AA0F-A52C87A4950B}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FinlyticSentiment", "FinlyticSentiment\FinlyticSentiment.csproj", "{9C3BB705-86AD-4A89-AA0F-A52C87A4950B}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FinlyticTechnicalAnalysis", "FinlyticTechnicalAnalysis\FinlyticTechnicalAnalysis.csproj", "{A1C82F63-4482-4E99-9231-1184FA2E001F}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FinlyticTechnicals", "FinlyticTechnicals\FinlyticTechnicals.csproj", "{A1C82F63-4482-4E99-9231-1184FA2E001F}"
|
||||||
EndProject
|
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FinlyticAnalyzer", "FinlyticAnalyzer\FinlyticAnalyzer.csproj", "{E9F7C091-62C4-417A-B981-8977DF82A1B0}"
|
|
||||||
EndProject
|
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FinlyticTrades", "FinlyticTrades\FinlyticTrades.csproj", "{57D84C2E-73E1-4231-A91B-6B620FCE5289}"
|
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FinlyticBackend", "FinlyticBackend\FinlyticBackend.csproj", "{C1A924B8-904E-436D-B07E-4E621F51C1AA}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FinlyticBackend", "FinlyticBackend\FinlyticBackend.csproj", "{C1A924B8-904E-436D-B07E-4E621F51C1AA}"
|
||||||
EndProject
|
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
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
@@ -97,30 +101,80 @@ Global
|
|||||||
{9C3BB705-86AD-4A89-AA0F-A52C87A4950B}.Release|x86.Build.0 = Release|Any CPU
|
{9C3BB705-86AD-4A89-AA0F-A52C87A4950B}.Release|x86.Build.0 = Release|Any CPU
|
||||||
{A1C82F63-4482-4E99-9231-1184FA2E001F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{A1C82F63-4482-4E99-9231-1184FA2E001F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{A1C82F63-4482-4E99-9231-1184FA2E001F}.Debug|Any CPU.Build.0 = 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
|
{A1C82F63-4482-4E99-9231-1184FA2E001F}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
{E9F7C091-62C4-417A-B981-8977DF82A1B0}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{A1C82F63-4482-4E99-9231-1184FA2E001F}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
{E9F7C091-62C4-417A-B981-8977DF82A1B0}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{A1C82F63-4482-4E99-9231-1184FA2E001F}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
{E9F7C091-62C4-417A-B981-8977DF82A1B0}.Release|Any CPU.Build.0 = Release|Any CPU
|
{A1C82F63-4482-4E99-9231-1184FA2E001F}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
{57D84C2E-73E1-4231-A91B-6B620FCE5289}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{A1C82F63-4482-4E99-9231-1184FA2E001F}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{57D84C2E-73E1-4231-A91B-6B620FCE5289}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{A1C82F63-4482-4E99-9231-1184FA2E001F}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
{57D84C2E-73E1-4231-A91B-6B620FCE5289}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{A1C82F63-4482-4E99-9231-1184FA2E001F}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
{57D84C2E-73E1-4231-A91B-6B620FCE5289}.Release|Any CPU.Build.0 = 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.ActiveCfg = Debug|Any CPU
|
||||||
{C1A924B8-904E-436D-B07E-4E621F51C1AA}.Debug|Any CPU.Build.0 = 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.ActiveCfg = Release|Any CPU
|
||||||
{C1A924B8-904E-436D-B07E-4E621F51C1AA}.Release|Any CPU.Build.0 = 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
|
{8112DE84-695D-489B-9568-C531B34C63F8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{B39B0AD9-FB8A-4F5F-8652-73C3BD8E75F3}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{8112DE84-695D-489B-9568-C531B34C63F8}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{B39B0AD9-FB8A-4F5F-8652-73C3BD8E75F3}.Debug|x64.ActiveCfg = Debug|Any CPU
|
{8112DE84-695D-489B-9568-C531B34C63F8}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
{B39B0AD9-FB8A-4F5F-8652-73C3BD8E75F3}.Debug|x64.Build.0 = Debug|Any CPU
|
{8112DE84-695D-489B-9568-C531B34C63F8}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
{B39B0AD9-FB8A-4F5F-8652-73C3BD8E75F3}.Debug|x86.ActiveCfg = Debug|Any CPU
|
{8112DE84-695D-489B-9568-C531B34C63F8}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
{B39B0AD9-FB8A-4F5F-8652-73C3BD8E75F3}.Debug|x86.Build.0 = Debug|Any CPU
|
{8112DE84-695D-489B-9568-C531B34C63F8}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
{B39B0AD9-FB8A-4F5F-8652-73C3BD8E75F3}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{8112DE84-695D-489B-9568-C531B34C63F8}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{B39B0AD9-FB8A-4F5F-8652-73C3BD8E75F3}.Release|Any CPU.Build.0 = Release|Any CPU
|
{8112DE84-695D-489B-9568-C531B34C63F8}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
{B39B0AD9-FB8A-4F5F-8652-73C3BD8E75F3}.Release|x64.ActiveCfg = Release|Any CPU
|
{8112DE84-695D-489B-9568-C531B34C63F8}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
{B39B0AD9-FB8A-4F5F-8652-73C3BD8E75F3}.Release|x64.Build.0 = Release|Any CPU
|
{8112DE84-695D-489B-9568-C531B34C63F8}.Release|x64.Build.0 = Release|Any CPU
|
||||||
{B39B0AD9-FB8A-4F5F-8652-73C3BD8E75F3}.Release|x86.ActiveCfg = Release|Any CPU
|
{8112DE84-695D-489B-9568-C531B34C63F8}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
{B39B0AD9-FB8A-4F5F-8652-73C3BD8E75F3}.Release|x86.Build.0 = 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
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
@@ -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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-151
@@ -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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-151
@@ -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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-277
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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();
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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");
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Generated
-162
@@ -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)
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-193
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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();
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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"]
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
@@ -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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-306
@@ -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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-311
@@ -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)
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Generated
-357
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-76
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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();
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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");
|
|
||||||
}
|
|
||||||
@@ -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
@@ -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
@@ -1,53 +1,129 @@
|
|||||||
services:
|
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:
|
finlyticassets:
|
||||||
image: finlyticassets
|
image: finlyticassets
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: FinlyticAssets/Dockerfile
|
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:
|
networks:
|
||||||
- postgres-network
|
- postgres-network
|
||||||
environment:
|
environment:
|
||||||
- ConnectionStrings__DefaultConnection=Host=OmniDB;Database=finlytic_assets;Username=admin;Password=${DB_PASSWORD}
|
- ConnectionStrings__DefaultConnection=Host=${DB_HOST:-OmniDB};Port=${DB_PORT:-5432};Database=finlytic_assets;Username=admin;Password=${DB_PASSWORD}
|
||||||
- MQTT__Host=host.docker.internal
|
- MQTT__Host=${MQTT_HOST:-host.docker.internal}
|
||||||
- MQTT__Port=4545
|
- MQTT__Port=${MQTT_PORT:-4545}
|
||||||
#- MQTT__Username=admin
|
# 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__Password=${MQTT_PASSWORD}
|
||||||
- MQTT__ClientId=finlytic_assets
|
- MQTT__ClientId=finlytic_assets
|
||||||
volumes:
|
volumes:
|
||||||
- C:\Users\larsh\Documents\docker\finlytic\assets\index:/app/assets/index
|
- ${FINLYTIC_DATA_ROOT:-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/logos:/app/assets/logos
|
||||||
|
|
||||||
finlyticnews:
|
finlyticnews:
|
||||||
image: finlyticnews
|
image: finlyticnews
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: FinlyticNews/Dockerfile
|
dockerfile: FinlyticNews/Dockerfile
|
||||||
|
restart: unless-stopped
|
||||||
networks:
|
networks:
|
||||||
- postgres-network
|
- postgres-network
|
||||||
environment:
|
environment:
|
||||||
- ConnectionStrings__DefaultConnection=Host=OmniDB;Database=finlytic_news;Username=admin;Password=${DB_PASSWORD}
|
- ConnectionStrings__DefaultConnection=Host=${DB_HOST:-OmniDB};Port=${DB_PORT:-5432};Database=finlytic_news;Username=admin;Password=${DB_PASSWORD}
|
||||||
- MQTT__Host=host.docker.internal
|
- MQTT__Host=${MQTT_HOST:-host.docker.internal}
|
||||||
- MQTT__Port=4545
|
- MQTT__Port=${MQTT_PORT:-4545}
|
||||||
#- MQTT__Username=admin
|
# See finlyticassets above: broker has no auth configured yet, do not enable.
|
||||||
|
#- MQTT__Username=${MQTT_USERNAME:-admin}
|
||||||
#- MQTT__Password=${MQTT_PASSWORD}
|
#- MQTT__Password=${MQTT_PASSWORD}
|
||||||
- MQTT__ClientId=finlytic_news
|
- 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:
|
volumes:
|
||||||
- C:\Users\larsh\Documents\docker\finlytic\assets\index:/app/assets/index:ro
|
- ${FINLYTIC_DATA_ROOT:-C:/Users/larsh/Documents/docker/finlytic}/assets/index:/app/assets/index:ro
|
||||||
- C:\Users\larsh\Documents\docker\finlytic\data\summaries:/app/data/summaries:ro
|
|
||||||
|
|
||||||
|
|
||||||
finlyticfundamentals:
|
finlyticfundamentals:
|
||||||
image: finlyticfundamentals
|
image: finlyticfundamentals
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: FinlyticFundamentals/Dockerfile
|
dockerfile: FinlyticFundamentals/Dockerfile
|
||||||
|
restart: unless-stopped
|
||||||
networks:
|
networks:
|
||||||
- postgres-network
|
- postgres-network
|
||||||
environment:
|
environment:
|
||||||
- ConnectionStrings__DefaultConnection=Host=OmniDB;Database=finlytic_fundamentals;Username=admin;Password=${DB_PASSWORD}
|
- ConnectionStrings__DefaultConnection=Host=${DB_HOST:-OmniDB};Port=${DB_PORT:-5432};Database=finlytic_fundamentals;Username=admin;Password=${DB_PASSWORD}
|
||||||
- MQTT__Host=host.docker.internal
|
- MQTT__Host=${MQTT_HOST:-host.docker.internal}
|
||||||
- MQTT__Port=4545
|
- 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
|
- MQTT__ClientId=finlytic_fundamentals
|
||||||
|
|
||||||
finlyticsentiment:
|
finlyticsentiment:
|
||||||
@@ -55,76 +131,95 @@ services:
|
|||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: FinlyticSentiment/Dockerfile
|
dockerfile: FinlyticSentiment/Dockerfile
|
||||||
|
restart: unless-stopped
|
||||||
networks:
|
networks:
|
||||||
- postgres-network
|
- postgres-network
|
||||||
environment:
|
environment:
|
||||||
- ConnectionStrings__DefaultConnection=Host=OmniDB;Database=finlytic_sentimental;Username=admin;Password=${DB_PASSWORD}
|
- ConnectionStrings__DefaultConnection=Host=${DB_HOST:-OmniDB};Port=${DB_PORT:-5432};Database=finlytic_sentimental;Username=admin;Password=${DB_PASSWORD}
|
||||||
- MQTT__Host=host.docker.internal
|
- MQTT__Host=${MQTT_HOST:-host.docker.internal}
|
||||||
- MQTT__Port=4545
|
- 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
|
- MQTT__ClientId=finlytic_sentiment
|
||||||
- Webhooks__German=https://n8n.kleidukos.me/webhook/sentiment/de
|
- Webhooks__German=https://n8n.kleidukos.me/webhook/sentiment/de
|
||||||
- Webhooks__English=https://n8n.kleidukos.me/webhook/sentiment/en
|
- Webhooks__English=https://n8n.kleidukos.me/webhook/sentiment/en
|
||||||
volumes:
|
# No volumes: FinlyticSentiment persists to PostgreSQL only. The
|
||||||
- C:\Users\larsh\Documents\docker\finlytic\data\summaries:/app/data/summaries
|
# 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:
|
finlytictechnicals:
|
||||||
image: finlytictechnicalanalysis
|
image: finlytictechnicals
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: FinlyticTechnicalAnalysis/Dockerfile
|
dockerfile: FinlyticTechnicals/Dockerfile
|
||||||
|
restart: unless-stopped
|
||||||
networks:
|
networks:
|
||||||
- postgres-network
|
- postgres-network
|
||||||
environment:
|
environment:
|
||||||
- ConnectionStrings__DefaultConnection=Host=OmniDB;Database=finlytic_ta;Username=admin;Password=${DB_PASSWORD}
|
- ConnectionStrings__DefaultConnection=Host=${DB_HOST:-OmniDB};Port=${DB_PORT:-5432};Database=finlytic_ta;Username=admin;Password=${DB_PASSWORD}
|
||||||
- MQTT__Host=host.docker.internal
|
- MQTT__Host=${MQTT_HOST:-host.docker.internal}
|
||||||
- MQTT__Port=4545
|
- MQTT__Port=${MQTT_PORT:-4545}
|
||||||
- MQTT__ClientId=finlytic_ta
|
# 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:
|
finlyticengine:
|
||||||
image: finlyticanalyzer
|
image: finlyticengine
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: FinlyticAnalyzer/Dockerfile
|
dockerfile: FinlyticEngine/Dockerfile
|
||||||
|
restart: unless-stopped
|
||||||
networks:
|
networks:
|
||||||
- postgres-network
|
- postgres-network
|
||||||
environment:
|
environment:
|
||||||
- ConnectionStrings__DefaultConnection=Host=OmniDB;Database=finlytic_analyzer;Username=admin;Password=${DB_PASSWORD}
|
- ConnectionStrings__DefaultConnection=Host=${DB_HOST:-OmniDB};Port=${DB_PORT:-5432};Database=finlytic_engine;Username=admin;Password=${DB_PASSWORD}
|
||||||
- MQTT__Host=host.docker.internal
|
- MQTT__Host=${MQTT_HOST:-host.docker.internal}
|
||||||
- MQTT__Port=4545
|
- MQTT__Port=${MQTT_PORT:-4545}
|
||||||
- MQTT__ClientId=finlytic_analyzer
|
# See finlyticassets above: broker has no auth configured yet, do not enable.
|
||||||
- N8N__WebhookUrl=https://n8n.kleidukos.me/webhook/gemini/analysis/auto
|
#- MQTT__Username=${MQTT_USERNAME:-admin}
|
||||||
volumes:
|
#- MQTT__Password=${MQTT_PASSWORD}
|
||||||
- C:\Users\larsh\Documents\docker\finlytic\data\feedback:/app/data/feedback:ro
|
- MQTT__ClientId=finlytic_engine
|
||||||
|
- Ai__N8nValidationWebhookUrl=https://n8n.kleidukos.me/webhook/trade-validation
|
||||||
|
|
||||||
finlytictrades:
|
finlyticsimulation:
|
||||||
image: finlytictrades
|
image: finlyticsimulation
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: FinlyticTrades/Dockerfile
|
dockerfile: FinlyticSimulation/Dockerfile
|
||||||
|
restart: unless-stopped
|
||||||
networks:
|
networks:
|
||||||
- postgres-network
|
- postgres-network
|
||||||
environment:
|
environment:
|
||||||
- ConnectionStrings__DefaultConnection=Host=OmniDB;Database=finlytic_trades;Username=admin;Password=${DB_PASSWORD}
|
- ConnectionStrings__DefaultConnection=Host=${DB_HOST:-OmniDB};Port=${DB_PORT:-5432};Database=finlytic_simulation;Username=admin;Password=${DB_PASSWORD}
|
||||||
- MQTT__Host=host.docker.internal
|
- MQTT__Host=${MQTT_HOST:-host.docker.internal}
|
||||||
- MQTT__Port=4545
|
- MQTT__Port=${MQTT_PORT:-4545}
|
||||||
- MQTT__ClientId=finlytic_trades
|
# See finlyticassets above: broker has no auth configured yet, do not enable.
|
||||||
volumes:
|
#- MQTT__Username=${MQTT_USERNAME:-admin}
|
||||||
- C:\Users\larsh\Documents\docker\finlytic\data\feedback:/app/data/feedback
|
#- MQTT__Password=${MQTT_PASSWORD}
|
||||||
|
- MQTT__ClientId=finlytic_simulation
|
||||||
|
|
||||||
finlyticbot:
|
finlyticbot:
|
||||||
image: finlyticbot
|
image: finlyticbot
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: FinlyticBot/Dockerfile
|
dockerfile: FinlyticBot/Dockerfile
|
||||||
|
restart: unless-stopped
|
||||||
networks:
|
networks:
|
||||||
- postgres-network
|
- postgres-network
|
||||||
environment:
|
environment:
|
||||||
- ConnectionStrings__DefaultConnection=Host=OmniDB;Database=finlytic_bot;Username=admin;Password=${DB_PASSWORD}
|
- ConnectionStrings__DefaultConnection=Host=${DB_HOST:-OmniDB};Port=${DB_PORT:-5432};Database=finlytic_bot;Username=admin;Password=${DB_PASSWORD}
|
||||||
- MQTT__Host=host.docker.internal
|
- MQTT__Host=${MQTT_HOST:-host.docker.internal}
|
||||||
- MQTT__Port=4545
|
- 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
|
- MQTT__ClientId=finlytic_bot
|
||||||
- Alpaca__KeyId=${ALPACA_KEY_ID:-}
|
- Alpaca__KeyId=${ALPACA_KEY_ID:-PK_PAPER_PLACEHOLDER_KEY}
|
||||||
- Alpaca__SecretKey=${ALPACA_SECRET_KEY:-}
|
- Alpaca__SecretKey=${ALPACA_SECRET_KEY:-SK_PAPER_PLACEHOLDER_SECRET}
|
||||||
- Alpaca__IsPaper=true
|
- Alpaca__IsPaper=true
|
||||||
|
|
||||||
finlyticbackend:
|
finlyticbackend:
|
||||||
@@ -132,22 +227,82 @@ services:
|
|||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: FinlyticBackend/Dockerfile
|
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:
|
ports:
|
||||||
- "5000:8080"
|
- "5000:8080"
|
||||||
networks:
|
networks:
|
||||||
- postgres-network
|
- postgres-network
|
||||||
environment:
|
environment:
|
||||||
- ConnectionStrings__DefaultConnection=Host=OmniDB;Database=finlytic_backend;Username=admin;Password=${DB_PASSWORD}
|
- ConnectionStrings__DefaultConnection=Host=${DB_HOST:-OmniDB};Port=${DB_PORT:-5432};Database=finlytic_backend;Username=admin;Password=${DB_PASSWORD}
|
||||||
- MQTT__Host=host.docker.internal
|
- MQTT__Host=${MQTT_HOST:-host.docker.internal}
|
||||||
- MQTT__Port=4545
|
- 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
|
- MQTT__ClientId=finlytic_backend
|
||||||
- JWT__SecretKey=${JWT_SECRET_KEY:-FinlyticEnterpriseUltraSecureJwtSecretKey_2026_AtLeast32Chars!}
|
- JWT__SecretKey=${JWT_SECRET_KEY}
|
||||||
- ADMIN__DefaultPassword=${ADMIN_DEFAULT_PASSWORD:-AdminDefaultPassword2026!}
|
- ADMIN__DefaultPassword=${ADMIN_DEFAULT_PASSWORD}
|
||||||
- Services__TradesServiceUrl=http://finlytictrades:8080/api/v1/trades/active
|
|
||||||
volumes:
|
volumes:
|
||||||
- C:\Users\larsh\Documents\docker\finlytic\assets\index:/app/assets/index
|
- ${FINLYTIC_DATA_ROOT:-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/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:
|
networks:
|
||||||
postgres-network:
|
postgres-network:
|
||||||
external: true
|
external: true
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,20 @@
|
|||||||
# rebuild-playwright-base.ps1
|
# 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.
|
# CONSUMERS — both reference this image as `FROM finlytic-playwright-base:<ver>`:
|
||||||
# After running this, a normal `docker compose build` will be fast again.
|
# * 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:
|
# Usage:
|
||||||
# .\rebuild-playwright-base.ps1
|
# .\rebuild-playwright-base.ps1
|
||||||
@@ -34,6 +45,7 @@ docker build `
|
|||||||
if ($LASTEXITCODE -eq 0) {
|
if ($LASTEXITCODE -eq 0) {
|
||||||
Write-Host ""
|
Write-Host ""
|
||||||
Write-Host "✅ Base image '$ImageName' built and cached locally." -ForegroundColor Green
|
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 " You can now run 'docker compose build' as usual." -ForegroundColor Green
|
||||||
Write-Host ""
|
Write-Host ""
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
|
||||||
Reference in New Issue
Block a user