Implement A1, A6, A2: Add TraderPosition entity, PositionPnLEngine (Average-Cost-Method), Market-level WinRate, and integrate with TraderAnalyticsWorker

This commit is contained in:
Richard
2026-07-03 11:16:42 +02:00
parent 5fe7ab63ac
commit e994e1ce72
10 changed files with 1227 additions and 78 deletions
@@ -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
}
}