feat(fundamentals): multi-ticker DB caching, parallel html scraper, and frontend mappings
This commit is contained in:
@@ -154,58 +154,63 @@ public class TradeLifecycleService : ITradeLifecycleService
|
||||
{
|
||||
string targetUserId = !string.IsNullOrWhiteSpace(request.UserId) ? request.UserId : "default_user";
|
||||
|
||||
var existingTrade = await _dbContext.Trades
|
||||
// 1. Prüfen, ob DIESER spezifische Nutzer diesen Trade/AnalysisId bereits als aktiven Trade angenommen hat
|
||||
var userExistingTrade = await _dbContext.Trades
|
||||
.FirstOrDefaultAsync(t =>
|
||||
(!string.IsNullOrEmpty(request.TradeId) && t.TradeId == request.TradeId) ||
|
||||
(!string.IsNullOrEmpty(request.AnalysisId) && t.AnalysisId == request.AnalysisId), cancellationToken);
|
||||
!t.IsGlobalProposal &&
|
||||
t.UserId == targetUserId &&
|
||||
((!string.IsNullOrEmpty(request.TradeId) && t.TradeId == request.TradeId) ||
|
||||
(!string.IsNullOrEmpty(request.AnalysisId) && t.AnalysisId == request.AnalysisId)),
|
||||
cancellationToken);
|
||||
|
||||
if (existingTrade != null)
|
||||
if (userExistingTrade != null)
|
||||
{
|
||||
if (existingTrade.Status == TradeStatus.Closed)
|
||||
if (userExistingTrade.Status == TradeStatus.Closed)
|
||||
{
|
||||
await _finlyticLogger.LogWarningAsync(SettingKeys.TradesChannel, "[TradeLifecycleService] Refused to accept trade {TradeId} because its status is CLOSED", existingTrade.TradeId);
|
||||
await _finlyticLogger.LogWarningAsync(SettingKeys.TradesChannel, "[TradeLifecycleService] Refused to accept trade {TradeId} because user's trade is already CLOSED", userExistingTrade.TradeId);
|
||||
return null;
|
||||
}
|
||||
|
||||
existingTrade.Status = TradeStatus.Active;
|
||||
existingTrade.IsGlobalProposal = false;
|
||||
existingTrade.UserId = targetUserId;
|
||||
|
||||
if (request.ActualEntryPrice > 0) existingTrade.ActualEntryPrice = request.ActualEntryPrice;
|
||||
if (request.EntryPrice > 0) existingTrade.EntryPrice = request.EntryPrice.Value;
|
||||
if (request.PositionSize > 0) existingTrade.PositionSize = request.PositionSize;
|
||||
if (request.LeverageUsed > 0) existingTrade.LeverageUsed = request.LeverageUsed;
|
||||
if (request.Quantity > 0) existingTrade.Quantity = request.Quantity;
|
||||
if (request.EntryFee.HasValue) existingTrade.EntryFee = request.EntryFee;
|
||||
if (request.ExitFee.HasValue) existingTrade.ExitFee = request.ExitFee;
|
||||
if (request.StopLoss > 0) existingTrade.StopLoss = request.StopLoss.Value;
|
||||
if (request.TakeProfit > 0) existingTrade.TakeProfit = request.TakeProfit.Value;
|
||||
if (request.KnockoutThreshold > 0) existingTrade.KnockoutThreshold = request.KnockoutThreshold;
|
||||
if (!string.IsNullOrWhiteSpace(request.Timeframe)) existingTrade.Timeframe = request.Timeframe;
|
||||
if (!string.IsNullOrWhiteSpace(request.DerivativeIsin)) existingTrade.DerivativeIsin = request.DerivativeIsin;
|
||||
if (!string.IsNullOrWhiteSpace(request.Reasoning)) existingTrade.Reasoning = request.Reasoning;
|
||||
// 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;
|
||||
|
||||
existingTrade.ExecutionTimestamp = request.ExecutionTimestamp?.ToUniversalTime() ?? DateTime.UtcNow;
|
||||
userExistingTrade.ExecutionTimestamp = request.ExecutionTimestamp?.ToUniversalTime() ?? DateTime.UtcNow;
|
||||
|
||||
existingTrade.PnlAbsolute = -(existingTrade.EntryFee ?? 0m) - (existingTrade.ExitFee ?? 0m);
|
||||
if (existingTrade.PositionSize > 0)
|
||||
userExistingTrade.PnlAbsolute = -(userExistingTrade.EntryFee ?? 0m) - (userExistingTrade.ExitFee ?? 0m);
|
||||
if (userExistingTrade.PositionSize > 0)
|
||||
{
|
||||
existingTrade.PnlPercent = (existingTrade.PnlAbsolute / existingTrade.PositionSize) * 100m;
|
||||
userExistingTrade.PnlPercent = (userExistingTrade.PnlAbsolute / userExistingTrade.PositionSize) * 100m;
|
||||
}
|
||||
|
||||
_dbContext.Trades.Update(existingTrade);
|
||||
_dbContext.Trades.Update(userExistingTrade);
|
||||
await _dbContext.SaveChangesAsync(cancellationToken);
|
||||
|
||||
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;
|
||||
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 &&
|
||||
(!string.IsNullOrEmpty(request.AnalysisId) ? t.AnalysisId == request.AnalysisId : t.Isin == request.Isin),
|
||||
.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 = !string.IsNullOrWhiteSpace(request.TradeId) ? request.TradeId : ("TRD-" + Guid.NewGuid().ToString("N")[..10].ToUpperInvariant());
|
||||
var targetTradeId = "TRD-" + Guid.NewGuid().ToString("N")[..10].ToUpperInvariant();
|
||||
|
||||
var newTrade = new TradeEntity
|
||||
{
|
||||
@@ -248,9 +253,10 @@ public class TradeLifecycleService : ITradeLifecycleService
|
||||
EntryFee = request.EntryFee,
|
||||
ExitFee = request.ExitFee,
|
||||
ExecutionTimestamp = request.ExecutionTimestamp?.ToUniversalTime() ?? DateTime.UtcNow,
|
||||
Quantity = request.Quantity,
|
||||
Quantity = request.Quantity > 0 ? request.Quantity : 1m,
|
||||
KnockoutThreshold = request.KnockoutThreshold,
|
||||
IsRecurring = request.IsRecurring
|
||||
IsRecurring = request.IsRecurring,
|
||||
DerivativeProductCategories = proposal?.DerivativeProductCategories != null ? new List<string>(proposal.DerivativeProductCategories) : new List<string>()
|
||||
};
|
||||
|
||||
newTrade.PnlAbsolute = -(newTrade.EntryFee ?? 0m) - (newTrade.ExitFee ?? 0m);
|
||||
@@ -262,58 +268,69 @@ public class TradeLifecycleService : ITradeLifecycleService
|
||||
_dbContext.Trades.Add(newTrade);
|
||||
await _dbContext.SaveChangesAsync(cancellationToken);
|
||||
|
||||
await _finlyticLogger.LogInfoAsync(SettingKeys.TradesChannel, "[TradeLifecycleService] Successfully created active trade {TradeId} for ISIN {Isin}, UserId: {UserId}", newTrade.TradeId, request.Isin, newTrade.UserId);
|
||||
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 trade = await _dbContext.Trades
|
||||
.FirstOrDefaultAsync(t => t.TradeId == update.TradeId || t.Id.ToString() == update.TradeId, cancellationToken);
|
||||
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 (trade == null || (trade.Status != TradeStatus.Active && trade.Status != TradeStatus.Proposed))
|
||||
if (matchedTrades.Count == 0)
|
||||
{
|
||||
await _finlyticLogger.LogWarningAsync(SettingKeys.TradesChannel, "[TradeLifecycleService] Cannot add hourly update: Trade {TradeId} not found or not active/proposed.", update.TradeId);
|
||||
await _finlyticLogger.LogWarningAsync(SettingKeys.TradesChannel, "[TradeLifecycleService] Cannot add hourly update: No active or proposed trades found for identifier {TradeId}.", update.TradeId);
|
||||
return;
|
||||
}
|
||||
|
||||
var updateEntity = new TradeHourlyUpdateEntity
|
||||
foreach (var trade in matchedTrades)
|
||||
{
|
||||
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)
|
||||
if (trade.Status != TradeStatus.Active && trade.Status != TradeStatus.Proposed)
|
||||
{
|
||||
trade.Status = TradeStatus.Invalidated;
|
||||
trade.CloseReason = "ProposalInvalidated";
|
||||
trade.ClosedAt = DateTime.UtcNow;
|
||||
continue;
|
||||
}
|
||||
else
|
||||
|
||||
var updateEntity = new TradeHourlyUpdateEntity
|
||||
{
|
||||
await _finlyticLogger.LogInfoAsync(SettingKeys.TradesChannel, "[TradeLifecycleService] Active trade {TradeId} received Close recommendation ({Reasoning}). Trade kept Active for user action.",
|
||||
trade.TradeId, update.Reasoning);
|
||||
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 for Trade {TradeId}. Recommendation: {Rec}, Price: {Price}",
|
||||
update.TradeId, update.Recommendation, update.CurrentPrice);
|
||||
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)
|
||||
|
||||
Reference in New Issue
Block a user