feat(trades): dynamic settings, IFinlyticLogger, live log streaming, and EF migration
This commit is contained in:
@@ -7,12 +7,13 @@ 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 Microsoft.Extensions.Logging;
|
||||
using Parquet.Serialization;
|
||||
|
||||
namespace FinlyticTrades.Services;
|
||||
@@ -25,17 +26,16 @@ public interface IFeedbackExporterEngine
|
||||
Task ExportFeedbackDataAsync(CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
|
||||
public class FeedbackExporterEngine : BackgroundService, IFeedbackExporterEngine
|
||||
{
|
||||
private readonly IServiceScopeFactory _scopeFactory;
|
||||
private readonly ILogger<FeedbackExporterEngine> _logger;
|
||||
private readonly IFinlyticLogger<FeedbackExporterEngine> _finlyticLogger;
|
||||
private readonly string _feedbackDir;
|
||||
|
||||
public FeedbackExporterEngine(IServiceScopeFactory scopeFactory, ILogger<FeedbackExporterEngine> logger)
|
||||
public FeedbackExporterEngine(IServiceScopeFactory scopeFactory, IFinlyticLogger<FeedbackExporterEngine> finlyticLogger)
|
||||
{
|
||||
_scopeFactory = scopeFactory;
|
||||
_logger = logger;
|
||||
_finlyticLogger = finlyticLogger;
|
||||
_feedbackDir = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "data", "feedback");
|
||||
|
||||
if (!Directory.Exists(_feedbackDir))
|
||||
@@ -46,7 +46,7 @@ public class FeedbackExporterEngine : BackgroundService, IFeedbackExporterEngine
|
||||
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
_logger.LogInformation("[{Channel}] Feedback Exporter Engine background service started.", "TradesChannel");
|
||||
await _finlyticLogger.LogInfoAsync(SettingKeys.TradesChannel, "[FeedbackExporterEngine] Feedback Exporter Engine background service started.");
|
||||
|
||||
try
|
||||
{
|
||||
@@ -69,7 +69,7 @@ public class FeedbackExporterEngine : BackgroundService, IFeedbackExporterEngine
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "[{Channel}] Error executing feedback exporter job.", "TradesChannel");
|
||||
await _finlyticLogger.LogErrorAsync(SettingKeys.TradesChannel, ex, "[FeedbackExporterEngine] Error executing feedback exporter job.");
|
||||
}
|
||||
|
||||
try
|
||||
@@ -82,7 +82,7 @@ public class FeedbackExporterEngine : BackgroundService, IFeedbackExporterEngine
|
||||
}
|
||||
}
|
||||
|
||||
_logger.LogInformation("[{Channel}] Feedback Exporter Engine background service stopped.", "TradesChannel");
|
||||
await _finlyticLogger.LogInfoAsync(SettingKeys.TradesChannel, "[FeedbackExporterEngine] Feedback Exporter Engine background service stopped.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -101,7 +101,7 @@ public class FeedbackExporterEngine : BackgroundService, IFeedbackExporterEngine
|
||||
|
||||
if (closedTrades.Count == 0)
|
||||
{
|
||||
_logger.LogInformation("[{Channel}] No closed trades available for export.", "TradesChannel");
|
||||
await _finlyticLogger.LogInfoAsync(SettingKeys.TradesChannel, "[FeedbackExporterEngine] No closed trades available for export.");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -184,16 +184,16 @@ public class FeedbackExporterEngine : BackgroundService, IFeedbackExporterEngine
|
||||
|
||||
File.Move(parquetTmpPath, parquetPath, overwrite: true);
|
||||
|
||||
_logger.LogInformation("[{Channel}] Exported Parquet feedback file for sector '{Sector}' to {ParquetPath}", "TradesChannel", sectorName, parquetPath);
|
||||
await _finlyticLogger.LogInfoAsync(SettingKeys.TradesChannel, "[FeedbackExporterEngine] Exported Parquet feedback file for sector '{Sector}' to {ParquetPath}", sectorName, parquetPath);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "[{Channel}] Failed to write Parquet file for sector '{Sector}'. JSON file was written successfully.", "TradesChannel", sectorName);
|
||||
await _finlyticLogger.LogWarningAsync(SettingKeys.TradesChannel, ex, "[FeedbackExporterEngine] Failed to write Parquet file for sector '{Sector}'. JSON file was written successfully.", sectorName);
|
||||
}
|
||||
}
|
||||
|
||||
_logger.LogInformation("[{Channel}] Successfully exported feedback data for {Count} closed trades across {Sectors} sectors.",
|
||||
"TradesChannel", closedTrades.Count, groups.Count());
|
||||
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)
|
||||
|
||||
@@ -5,10 +5,11 @@ 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;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace FinlyticTrades.Services;
|
||||
|
||||
@@ -28,19 +29,19 @@ public interface ITradeLifecycleService
|
||||
public class TradeLifecycleService : ITradeLifecycleService
|
||||
{
|
||||
private readonly TradesDbContext _dbContext;
|
||||
private readonly ILogger<TradeLifecycleService> _logger;
|
||||
private readonly IFinlyticLogger<TradeLifecycleService> _finlyticLogger;
|
||||
|
||||
public TradeLifecycleService(TradesDbContext dbContext, ILogger<TradeLifecycleService> logger)
|
||||
public TradeLifecycleService(TradesDbContext dbContext, IFinlyticLogger<TradeLifecycleService> finlyticLogger)
|
||||
{
|
||||
_dbContext = dbContext;
|
||||
_logger = logger;
|
||||
_finlyticLogger = finlyticLogger;
|
||||
}
|
||||
|
||||
public async Task<bool> ProcessManualAnalysisResponseAsync(ManualAnalysisResponseDto response, string userId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (response == null || !response.IsTradeProposed)
|
||||
{
|
||||
_logger.LogInformation("[{Channel}] Manual analysis response indicated NO trade proposed (AnalysisId: {AnalysisId}). Skipping.", "TradesChannel", response?.AnalysisId);
|
||||
await _finlyticLogger.LogInfoAsync(SettingKeys.TradesChannel, "[TradeLifecycleService] Manual analysis response indicated NO trade proposed (AnalysisId: {AnalysisId}). Skipping.", response?.AnalysisId);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -90,7 +91,7 @@ public class TradeLifecycleService : ITradeLifecycleService
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(proposal.Symbol) && string.IsNullOrWhiteSpace(proposal.Isin))
|
||||
{
|
||||
_logger.LogWarning("[{Channel}] ProcessProposedTradeAsync: Received proposal with missing Symbol and ISIN. Skipping.", "TradesChannel");
|
||||
await _finlyticLogger.LogWarningAsync(SettingKeys.TradesChannel, "[TradeLifecycleService] ProcessProposedTradeAsync: Received proposal with missing Symbol and ISIN. Skipping.");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -109,8 +110,8 @@ public class TradeLifecycleService : ITradeLifecycleService
|
||||
{
|
||||
if (existingTrade.Status == TradeStatus.Active)
|
||||
{
|
||||
_logger.LogInformation("[{Channel}] An ACTIVE trade {TradeId} already exists for {Symbol} ({Isin}). Skipping duplicate proposed trade creation.",
|
||||
"TradesChannel", existingTrade.TradeId, proposal.Symbol, proposal.Isin);
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -123,8 +124,8 @@ public class TradeLifecycleService : ITradeLifecycleService
|
||||
_dbContext.Trades.Update(existingTrade);
|
||||
await _dbContext.SaveChangesAsync(cancellationToken);
|
||||
|
||||
_logger.LogInformation("[{Channel}] Successfully UPDATED existing trade proposal {TradeId} for Symbol {Symbol} (ISIN: {Isin}) with status {Status}",
|
||||
"TradesChannel", existingTrade.TradeId, proposal.Symbol, proposal.Isin, existingTrade.Status);
|
||||
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;
|
||||
}
|
||||
@@ -143,8 +144,8 @@ public class TradeLifecycleService : ITradeLifecycleService
|
||||
_dbContext.Trades.Add(tradeEntity);
|
||||
await _dbContext.SaveChangesAsync(cancellationToken);
|
||||
|
||||
_logger.LogInformation("[{Channel}] Successfully ingested NEW trade proposal {TradeId} for Symbol {Symbol} (ISIN: {Isin}) with status {Status}",
|
||||
"TradesChannel", tradeId, proposal.Symbol, proposal.Isin, targetStatus);
|
||||
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;
|
||||
}
|
||||
@@ -162,7 +163,7 @@ public class TradeLifecycleService : ITradeLifecycleService
|
||||
{
|
||||
if (existingTrade.Status == TradeStatus.Closed)
|
||||
{
|
||||
_logger.LogWarning("[{Channel}] Refused to accept trade {TradeId} because its status is CLOSED", "TradesChannel", existingTrade.TradeId);
|
||||
await _finlyticLogger.LogWarningAsync(SettingKeys.TradesChannel, "[TradeLifecycleService] Refused to accept trade {TradeId} because its status is CLOSED", existingTrade.TradeId);
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -195,7 +196,7 @@ public class TradeLifecycleService : ITradeLifecycleService
|
||||
_dbContext.Trades.Update(existingTrade);
|
||||
await _dbContext.SaveChangesAsync(cancellationToken);
|
||||
|
||||
_logger.LogInformation("[{Channel}] Successfully ACCEPTED and UPDATED trade {TradeId} for ISIN {Isin}, UserId: {UserId}", "TradesChannel", existingTrade.TradeId, existingTrade.Isin, existingTrade.UserId);
|
||||
await _finlyticLogger.LogInfoAsync(SettingKeys.TradesChannel, "[TradeLifecycleService] Successfully ACCEPTED and UPDATED trade {TradeId} for ISIN {Isin}, UserId: {UserId}", existingTrade.TradeId, existingTrade.Isin, existingTrade.UserId);
|
||||
return existingTrade;
|
||||
}
|
||||
|
||||
@@ -261,7 +262,7 @@ public class TradeLifecycleService : ITradeLifecycleService
|
||||
_dbContext.Trades.Add(newTrade);
|
||||
await _dbContext.SaveChangesAsync(cancellationToken);
|
||||
|
||||
_logger.LogInformation("[{Channel}] Successfully created active trade {TradeId} for ISIN {Isin}, UserId: {UserId}", "TradesChannel", newTrade.TradeId, request.Isin, newTrade.UserId);
|
||||
await _finlyticLogger.LogInfoAsync(SettingKeys.TradesChannel, "[TradeLifecycleService] Successfully created active trade {TradeId} for ISIN {Isin}, UserId: {UserId}", newTrade.TradeId, request.Isin, newTrade.UserId);
|
||||
return newTrade;
|
||||
}
|
||||
|
||||
@@ -272,7 +273,7 @@ public class TradeLifecycleService : ITradeLifecycleService
|
||||
|
||||
if (trade == null || (trade.Status != TradeStatus.Active && trade.Status != TradeStatus.Proposed))
|
||||
{
|
||||
_logger.LogWarning("[{Channel}] Cannot add hourly update: Trade {TradeId} not found or not active/proposed.", "TradesChannel", update.TradeId);
|
||||
await _finlyticLogger.LogWarningAsync(SettingKeys.TradesChannel, "[TradeLifecycleService] Cannot add hourly update: Trade {TradeId} not found or not active/proposed.", update.TradeId);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -305,16 +306,14 @@ public class TradeLifecycleService : ITradeLifecycleService
|
||||
}
|
||||
else
|
||||
{
|
||||
// NO AUTO CLOSE for active user trades!
|
||||
// Trade remains Active, alert is stored in HourlyUpdates and surfaced in UI for manual confirmation.
|
||||
_logger.LogInformation("[{Channel}] Active trade {TradeId} received Close recommendation ({Reasoning}). Trade kept Active for user action.",
|
||||
"TradesChannel", trade.TradeId, update.Reasoning);
|
||||
await _finlyticLogger.LogInfoAsync(SettingKeys.TradesChannel, "[TradeLifecycleService] Active trade {TradeId} received Close recommendation ({Reasoning}). Trade kept Active for user action.",
|
||||
trade.TradeId, update.Reasoning);
|
||||
}
|
||||
}
|
||||
|
||||
await _dbContext.SaveChangesAsync(cancellationToken);
|
||||
_logger.LogInformation("[{Channel}] Added hourly update for Trade {TradeId}. Recommendation: {Rec}, Price: {Price}",
|
||||
"TradesChannel", update.TradeId, update.Recommendation, update.CurrentPrice);
|
||||
await _finlyticLogger.LogInfoAsync(SettingKeys.TradesChannel, "[TradeLifecycleService] Added hourly update for Trade {TradeId}. Recommendation: {Rec}, Price: {Price}",
|
||||
update.TradeId, update.Recommendation, update.CurrentPrice);
|
||||
}
|
||||
|
||||
public async Task<List<TradeEntity>> GetActiveTradesAsync(string? userId = null, CancellationToken cancellationToken = default)
|
||||
@@ -374,8 +373,8 @@ public class TradeLifecycleService : ITradeLifecycleService
|
||||
CalculatePnL(trade);
|
||||
|
||||
await _dbContext.SaveChangesAsync(cancellationToken);
|
||||
_logger.LogInformation("[{Channel}] Trade {TradeId} manually closed at price {ExitPrice}. PnL: {PnlAbs} ({PnlPct:F2}%)",
|
||||
"TradesChannel", trade.TradeId, trade.UserExitPrice, trade.PnlAbsolute, trade.PnlPercent);
|
||||
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;
|
||||
}
|
||||
@@ -392,7 +391,7 @@ public class TradeLifecycleService : ITradeLifecycleService
|
||||
trade.ClosedAt = DateTime.UtcNow;
|
||||
|
||||
await _dbContext.SaveChangesAsync(cancellationToken);
|
||||
_logger.LogInformation("[{Channel}] Trade {TradeId} rejected by user.", "TradesChannel", trade.TradeId);
|
||||
await _finlyticLogger.LogInfoAsync(SettingKeys.TradesChannel, "[TradeLifecycleService] Trade {TradeId} rejected by user.", trade.TradeId);
|
||||
|
||||
return trade;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user