refactor: save current workspace state including FinlyticAnalyzer fixes, FinlyticApp trade route alignment, and DTO audit documentation
This commit is contained in:
@@ -105,10 +105,10 @@ public class ManualAnalysisController : ControllerBase
|
||||
},
|
||||
TradeFeedback = new TradeFeedbackInfo
|
||||
{
|
||||
TotalAssetTrades = 12,
|
||||
TotalAssetTrades = 0,
|
||||
AssetWinRate = winRate,
|
||||
AvgReturnPercent = 3.4,
|
||||
LastTradeResult = "WIN"
|
||||
AvgReturnPercent = 0.0,
|
||||
LastTradeResult = "UNKNOWN"
|
||||
}
|
||||
};
|
||||
|
||||
@@ -120,6 +120,7 @@ public class ManualAnalysisController : ControllerBase
|
||||
{
|
||||
proposal = new TradeProposalDto
|
||||
{
|
||||
TradeId = "PROP-" + Guid.NewGuid().ToString("N"),
|
||||
AnalysisId = analysisId,
|
||||
EventId = analysisId,
|
||||
Sector = request.Sector,
|
||||
|
||||
@@ -2,11 +2,8 @@ using System;
|
||||
using FinlyticAnalyzer.Database;
|
||||
using FinlyticAnalyzer.Services;
|
||||
using FinlyticAnalyzer.Util;
|
||||
using FinlyticCore.Services.Yahoo;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
var builder = Host.CreateApplicationBuilder(args);
|
||||
|
||||
@@ -14,16 +11,15 @@ var builder = Host.CreateApplicationBuilder(args);
|
||||
builder.Services.AddDbContext<AnalyzerDbContext>(options =>
|
||||
options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection")));
|
||||
|
||||
// Register HTTP Clients for external scrapers/webhooks
|
||||
builder.Services.AddHttpClient<IVixTrackerService, VixTrackerService>();
|
||||
// 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.AddSingleton<IN8nEvaluationService, N8nEvaluationService>();
|
||||
builder.Services.AddScoped<ISettingsDbService, SettingsDbService>();
|
||||
builder.Services.AddScoped<YahooFinanceClient>();
|
||||
|
||||
// Unified MQTT Client (Handles both Events and RPC)
|
||||
builder.Services.AddSingleton<AnalyzerMqttClient>();
|
||||
@@ -60,4 +56,4 @@ using (var scope = host.Services.CreateScope())
|
||||
await vixService.PollVixAsync();
|
||||
}
|
||||
|
||||
await host.RunAsync();
|
||||
await host.RunAsync();
|
||||
@@ -103,6 +103,7 @@ public class ActiveTradeMonitorWorker : BackgroundService
|
||||
|
||||
using var scope = _scopeFactory.CreateScope();
|
||||
var n8nService = scope.ServiceProvider.GetRequiredService<IN8nEvaluationService>();
|
||||
var vixService = scope.ServiceProvider.GetRequiredService<IVixTrackerService>();
|
||||
|
||||
foreach (var trade in trades)
|
||||
{
|
||||
@@ -110,7 +111,7 @@ public class ActiveTradeMonitorWorker : BackgroundService
|
||||
|
||||
try
|
||||
{
|
||||
await ProcessTradeAsync(trade, n8nService, cancellationToken);
|
||||
await ProcessTradeAsync(trade, n8nService, vixService, cancellationToken);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -121,7 +122,7 @@ public class ActiveTradeMonitorWorker : BackgroundService
|
||||
}
|
||||
|
||||
private async Task ProcessTradeAsync(TradeProposalDto trade, IN8nEvaluationService n8nService,
|
||||
CancellationToken cancellationToken)
|
||||
IVixTrackerService vixService, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. Get Live Price
|
||||
var livePriceReq = new IsinRequest(trade.Isin);
|
||||
@@ -210,8 +211,8 @@ public class ActiveTradeMonitorWorker : BackgroundService
|
||||
},
|
||||
MarketContext = new MarketContextInfo
|
||||
{
|
||||
Vix = trade.VixValue,
|
||||
MarketRegime = trade.VixRegime.ToString()
|
||||
Vix = (double)vixService.CurrentVix,
|
||||
MarketRegime = vixService.CurrentRegime.ToString()
|
||||
},
|
||||
UserPreferences = new UserPreferencesInfo
|
||||
{
|
||||
|
||||
@@ -39,11 +39,15 @@ public class SettingsDbService : ISettingsDbService
|
||||
var settings = await _context.Settings.AsNoTracking().FirstOrDefaultAsync();
|
||||
if (settings == null)
|
||||
{
|
||||
settings = new AnalyzerSettingsEntity { Id = Guid.NewGuid() };
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -52,10 +56,13 @@ public class SettingsDbService : ISettingsDbService
|
||||
/// </summary>
|
||||
public async Task<AnalyzerSettingsEntity> SaveSettingsAsync(AnalyzerSettingsEntity settings)
|
||||
{
|
||||
var existing = await _context.Settings.FirstOrDefaultAsync();
|
||||
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
|
||||
@@ -67,19 +74,23 @@ public class SettingsDbService : ISettingsDbService
|
||||
existing.EnableLogAnalyzerAuto = settings.EnableLogAnalyzerAuto;
|
||||
existing.EnableLogAnalyzerManual = settings.EnableLogAnalyzerManual;
|
||||
existing.EnableLogDatabaseOps = settings.EnableLogDatabaseOps;
|
||||
existing.UpdatedAt = settings.UpdatedAt;
|
||||
_context.Settings.Update(existing);
|
||||
existing.UpdatedAt = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
// Synchronize in-memory static filter values
|
||||
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;
|
||||
|
||||
return settings;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -10,6 +10,7 @@ public class ThreeLayerFilterEngine : IThreeLayerFilterEngine
|
||||
{
|
||||
private readonly ILogger<ThreeLayerFilterEngine> _logger;
|
||||
private readonly ConcurrentDictionary<string, DateTime> _seenEvents = new();
|
||||
private readonly object _cleanupLock = new();
|
||||
private DateTime _lastCleanupTime = DateTime.UtcNow;
|
||||
|
||||
public ThreeLayerFilterEngine(ILogger<ThreeLayerFilterEngine> logger)
|
||||
@@ -37,10 +38,16 @@ public class ThreeLayerFilterEngine : IThreeLayerFilterEngine
|
||||
string eventId = newsEvent.Id.ToString();
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
// Safely clean up dictionary every 30 minutes
|
||||
// Safely clean up dictionary every 30 minutes (thread-safe lock)
|
||||
if ((now - _lastCleanupTime).TotalMinutes > 30 || _seenEvents.Count > 10000)
|
||||
{
|
||||
CleanupSeenEvents(now);
|
||||
lock (_cleanupLock)
|
||||
{
|
||||
if ((now - _lastCleanupTime).TotalMinutes > 30 || _seenEvents.Count > 10000)
|
||||
{
|
||||
CleanupSeenEvents(now);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Deduplication check (keep history for 12 hours)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
@@ -12,6 +13,11 @@ public class WinRateCalculator : IWinRateCalculator
|
||||
{
|
||||
private readonly ILogger<WinRateCalculator> _logger;
|
||||
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(ILogger<WinRateCalculator> logger)
|
||||
{
|
||||
@@ -25,41 +31,25 @@ public class WinRateCalculator : IWinRateCalculator
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the win rate for a given sector and symbol under the specified market regime.
|
||||
/// Uses cached feedback records (3-minute TTL) to prevent disk I/O bottlenecks.
|
||||
/// </summary>
|
||||
public double CalculateWinRate(string sector, string symbol, VixMarketRegime regime)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!Directory.Exists(_feedbackDir)) return 65.0;
|
||||
var records = GetCachedOrLoadRecords();
|
||||
if (records.Count == 0) return 65.0;
|
||||
|
||||
var jsonFiles = Directory.GetFiles(_feedbackDir, "*.json", SearchOption.AllDirectories);
|
||||
if (jsonFiles.Length == 0) return 65.0;
|
||||
var matching = records.Where(r =>
|
||||
string.Equals(r.Sector, sector, StringComparison.OrdinalIgnoreCase) &&
|
||||
r.VixRegime == regime).ToList();
|
||||
|
||||
int totalTrades = 0;
|
||||
int winningTrades = 0;
|
||||
|
||||
foreach (var file in jsonFiles)
|
||||
if (matching.Count > 0)
|
||||
{
|
||||
var content = File.ReadAllText(file);
|
||||
var records = JsonSerializer.Deserialize<TradeFeedbackRecord[]>(content);
|
||||
if (records == null || records.Length == 0) continue;
|
||||
|
||||
var matching = records.Where(r =>
|
||||
string.Equals(r.Sector, sector, StringComparison.OrdinalIgnoreCase) &&
|
||||
r.VixRegime == regime).ToList();
|
||||
|
||||
foreach (var rec in matching)
|
||||
{
|
||||
totalTrades++;
|
||||
if (rec.IsWin) winningTrades++;
|
||||
}
|
||||
}
|
||||
|
||||
if (totalTrades > 0)
|
||||
{
|
||||
double calculatedWinRate = (double)winningTrades / totalTrades * 100.0;
|
||||
int winningTrades = matching.Count(r => r.IsWin);
|
||||
double calculatedWinRate = (double)winningTrades / matching.Count * 100.0;
|
||||
_logger.LogInformation("[{Channel}] Calculated win-rate for Sector '{Sector}' in Regime '{Regime}': {WinRate:F1}% ({Wins}/{Total})",
|
||||
"AnalyzerChannel", sector, regime, calculatedWinRate, winningTrades, totalTrades);
|
||||
"AnalyzerChannel", sector, regime, calculatedWinRate, winningTrades, matching.Count);
|
||||
return Math.Round(calculatedWinRate, 1);
|
||||
}
|
||||
}
|
||||
@@ -70,4 +60,42 @@ public class WinRateCalculator : IWinRateCalculator
|
||||
|
||||
return 65.0; // Default baseline win-rate
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
_logger.LogWarning(ex, "[{Channel}] Failed to read or parse feedback file '{File}'", "AnalyzerChannel", file);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_cachedRecords = loadedList;
|
||||
_lastCacheTime = DateTime.UtcNow;
|
||||
return _cachedRecords;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,7 +79,6 @@ public class AnalyzerMqttClient : ManagedMqttClient, IHostedService
|
||||
_logger.LogInformation("[{Channel}] Analyzer MQTT Client connected. Subscribing to topics and RPC response channels...", "AnalyzerChannel");
|
||||
|
||||
// Incoming Event Topics
|
||||
await SubscribeAsync("services/news/completed");
|
||||
await SubscribeAsync("services/news/#");
|
||||
await SubscribeAsync("finlytic/news/raw/#");
|
||||
await SubscribeAsync("finlytic/market/ticks/#");
|
||||
@@ -420,7 +419,21 @@ public class AnalyzerMqttClient : ManagedMqttClient, IHostedService
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "[{Channel}] Failed to handle manual trigger.", "AnalyzerChannel");
|
||||
_logger.LogError(ex, "[{Channel}] Failed to handle manual trigger for correlation {CorrelationId}.", "AnalyzerChannel", correlationId);
|
||||
try
|
||||
{
|
||||
var errorResponse = new ManualAnalysisResponseDto
|
||||
{
|
||||
Status = "ERROR",
|
||||
Message = $"Analysis failed: {ex.Message}"
|
||||
};
|
||||
await PublishAsync($"services/response/analyzer_TriggerManual/{correlationId}",
|
||||
JsonSerializer.Serialize(errorResponse, FinlyticJsonSerializerContext.Default.ManualAnalysisResponseDto));
|
||||
}
|
||||
catch (Exception pubEx)
|
||||
{
|
||||
_logger.LogError(pubEx, "[{Channel}] Failed to publish error response for correlation {CorrelationId}.", "AnalyzerChannel", correlationId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -522,11 +535,23 @@ public class AnalyzerMqttClient : ManagedMqttClient, IHostedService
|
||||
{
|
||||
var isinReq = new IsinRequest(filterResult.Isin);
|
||||
|
||||
livePriceResp = await SendRpcRequestAsync<FinlyticCore.Dtos.TechnicalAnalysis.LivePriceDto, IsinRequest>(
|
||||
"tr_GetLivePrice", isinReq, TimeSpan.FromSeconds(3));
|
||||
|
||||
taResp = await SendRpcRequestAsync<FinlyticCore.Dtos.TechnicalAnalysis.TechnicalAnalysisDto, IsinRequest>(
|
||||
"ta_GetAnalysis", isinReq, TimeSpan.FromSeconds(3));
|
||||
// Parallel RPC calls (was sequential — up to 12s latency reduced to ~3s)
|
||||
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;
|
||||
var sentResp = sentTask.Result;
|
||||
|
||||
if (taResp?.Indicators != null)
|
||||
{
|
||||
var latestIndicator = taResp.Indicators.LastOrDefault();
|
||||
@@ -547,8 +572,6 @@ public class AnalyzerMqttClient : ManagedMqttClient, IHostedService
|
||||
};
|
||||
}
|
||||
|
||||
fundResp = await SendRpcRequestAsync<FinlyticCore.Dtos.Fundamentals.AssetFundamentalsDto, IsinRequest>(
|
||||
"fundamentals_Get", isinReq, TimeSpan.FromSeconds(3));
|
||||
if (fundResp != null)
|
||||
{
|
||||
resolvedSymbol = !string.IsNullOrWhiteSpace(fundResp.Ticker) ? fundResp.Ticker : resolvedSymbol;
|
||||
@@ -571,8 +594,6 @@ public class AnalyzerMqttClient : ManagedMqttClient, IHostedService
|
||||
};
|
||||
}
|
||||
|
||||
var sentResp = await SendRpcRequestAsync<FinlyticCore.Dtos.Sentiment.IsinSentimentSummaryDto, IsinRequest>(
|
||||
"sentiment_GetIsin", isinReq, TimeSpan.FromSeconds(3));
|
||||
if (sentResp != null)
|
||||
{
|
||||
sentInfo = new SentimentContextInfo
|
||||
|
||||
Reference in New Issue
Block a user