feat(trades): dynamic settings, IFinlyticLogger, live log streaming, and EF migration

This commit is contained in:
2026-08-15 21:30:19 +02:00
parent 0d370d09e7
commit 57554a9582
9 changed files with 588 additions and 63 deletions
@@ -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;
}