Implement A1, A6, A2: Add TraderPosition entity, PositionPnLEngine (Average-Cost-Method), Market-level WinRate, and integrate with TraderAnalyticsWorker
This commit is contained in:
@@ -3,7 +3,7 @@ using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Predictalytics.Infrastructure.Data;
|
||||
using Predictalytics.Domain.Entities;
|
||||
using Predictalytics.Application.Interfaces;
|
||||
|
||||
namespace Predictalytics.Worker.Services;
|
||||
|
||||
@@ -12,10 +12,10 @@ public class TraderAnalyticsWorker : BackgroundService
|
||||
private readonly IServiceProvider _services;
|
||||
private readonly ILogger<TraderAnalyticsWorker> _logger;
|
||||
|
||||
public TraderAnalyticsWorker(IServiceProvider services, ILogger<TraderAnalyticsWorker> logger)
|
||||
public TraderAnalyticsWorker(IServiceProvider services, ILogger<TraderAnalyticsWorker> _logger)
|
||||
{
|
||||
_services = services;
|
||||
_logger = logger;
|
||||
this._logger = _logger;
|
||||
}
|
||||
|
||||
protected override async Task ExecuteAsync(CancellationToken ct)
|
||||
@@ -42,6 +42,7 @@ public class TraderAnalyticsWorker : BackgroundService
|
||||
{
|
||||
using var scope = _services.CreateScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
var pnlEngine = scope.ServiceProvider.GetRequiredService<IPositionPnLEngine>();
|
||||
|
||||
var cutoff30d = DateTime.UtcNow.AddDays(-30);
|
||||
|
||||
@@ -56,83 +57,16 @@ public class TraderAnalyticsWorker : BackgroundService
|
||||
|
||||
foreach (var id in traderIds)
|
||||
{
|
||||
await UpdateTraderAnalyticsAsync(db, id, ct);
|
||||
try
|
||||
{
|
||||
await pnlEngine.RecalculateTraderPositionsAsync(id, ct);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error recalculating positions/PnL for trader {TraderId}", id);
|
||||
}
|
||||
}
|
||||
|
||||
await db.SaveChangesAsync(ct);
|
||||
_logger.LogInformation("Trader analytics update complete.");
|
||||
}
|
||||
|
||||
private async Task UpdateTraderAnalyticsAsync(AppDbContext db, int traderId, CancellationToken ct)
|
||||
{
|
||||
var trades = await db.Trades.Where(t => t.TraderId == traderId).ToListAsync(ct);
|
||||
if (!trades.Any()) return;
|
||||
|
||||
var analytics = await db.TraderAnalytics.FirstOrDefaultAsync(a => a.TraderId == traderId, ct);
|
||||
if (analytics == null)
|
||||
{
|
||||
analytics = new TraderAnalytics { TraderId = traderId };
|
||||
db.TraderAnalytics.Add(analytics);
|
||||
}
|
||||
|
||||
analytics.LastCalculatedAt = DateTime.UtcNow;
|
||||
|
||||
// Simplified PnL calculation: Sum of Sells - Sum of Buys
|
||||
// This is not perfect but a good starting point as requested.
|
||||
// In a real scenario, we'd account for current market value of holdings.
|
||||
|
||||
analytics.OverallPnL = CalculatePnL(trades, null);
|
||||
analytics.OverallWinRate = CalculateWinRate(trades, null);
|
||||
|
||||
analytics.PnL30d = CalculatePnL(trades, DateTime.UtcNow.AddDays(-30));
|
||||
analytics.WinRate30d = CalculateWinRate(trades, DateTime.UtcNow.AddDays(-30));
|
||||
|
||||
analytics.PnL7d = CalculatePnL(trades, DateTime.UtcNow.AddDays(-7));
|
||||
analytics.WinRate7d = CalculateWinRate(trades, DateTime.UtcNow.AddDays(-7));
|
||||
|
||||
analytics.PnL24h = CalculatePnL(trades, DateTime.UtcNow.AddHours(-24));
|
||||
analytics.WinRate24h = CalculateWinRate(trades, DateTime.UtcNow.AddHours(-24));
|
||||
|
||||
// Update the trader record too for easy sorting
|
||||
var trader = await db.Traders.FindAsync(new object[] { traderId }, ct);
|
||||
if (trader != null)
|
||||
{
|
||||
trader.TotalPnl = analytics.OverallPnL;
|
||||
trader.WinRate = analytics.OverallWinRate;
|
||||
}
|
||||
}
|
||||
|
||||
private decimal CalculatePnL(List<Trade> trades, DateTime? since)
|
||||
{
|
||||
var filtered = since.HasValue ? trades.Where(t => t.ExecutedAt >= since.Value) : trades;
|
||||
|
||||
// Very simplified: Sells - Buys
|
||||
// Note: Real PnL should consider if the market resolved in their favor.
|
||||
// For now, we use the raw trade amounts.
|
||||
decimal pnl = 0;
|
||||
foreach (var t in filtered)
|
||||
{
|
||||
if (t.Side == Predictalytics.Domain.Enums.TradeSide.Buy) pnl -= t.Amount;
|
||||
else pnl += t.Amount;
|
||||
}
|
||||
return pnl;
|
||||
}
|
||||
|
||||
private decimal CalculateWinRate(List<Trade> trades, DateTime? since)
|
||||
{
|
||||
var filtered = since.HasValue ? trades.Where(t => t.ExecutedAt >= since.Value).ToList() : trades;
|
||||
if (!filtered.Any()) return 0;
|
||||
|
||||
// Simplified: A "win" is a Sell at a higher price than the average Buy price?
|
||||
// Actually, without proper position tracking, this is hard.
|
||||
// Let's assume a "win" is any trade that closed a position in profit.
|
||||
// For now, let's just return a placeholder or implement a basic logic.
|
||||
// Since we don't have resolution data easily linked here, we'll return 0 or a dummy.
|
||||
// Wait, if MarketOutcome is resolved and they held that outcome, it's a win.
|
||||
|
||||
// Let's just use 0 for now to avoid misleading data, or
|
||||
// if we have MarketOutcomeId and it's resolved, we can check.
|
||||
|
||||
return 0; // Placeholder until more complex logic is added
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user