Initial commit: Predictalytics solution
Clean Architecture .NET 8 solution (Domain/Application/Infrastructure/Api/Worker/WinFormsHost) for analyzing Polymarket traders for copytrading/strategy-replication candidates. Includes EF Core InitialBaseline migration and DB secrets removed from source/config in preparation for version control.
This commit is contained in:
@@ -0,0 +1,138 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Predictalytics.Infrastructure.Data;
|
||||
using Predictalytics.Domain.Entities;
|
||||
|
||||
namespace Predictalytics.Worker.Services;
|
||||
|
||||
public class TraderAnalyticsWorker : BackgroundService
|
||||
{
|
||||
private readonly IServiceProvider _services;
|
||||
private readonly ILogger<TraderAnalyticsWorker> _logger;
|
||||
|
||||
public TraderAnalyticsWorker(IServiceProvider services, ILogger<TraderAnalyticsWorker> logger)
|
||||
{
|
||||
_services = services;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
protected override async Task ExecuteAsync(CancellationToken ct)
|
||||
{
|
||||
_logger.LogInformation("TraderAnalyticsWorker starting...");
|
||||
|
||||
while (!ct.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
await RunAnalyticsAsync(ct);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error in TraderAnalyticsWorker");
|
||||
}
|
||||
|
||||
_logger.LogInformation("TraderAnalyticsWorker sleeping for 12 hours...");
|
||||
await Task.Delay(TimeSpan.FromHours(12), ct);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RunAnalyticsAsync(CancellationToken ct)
|
||||
{
|
||||
using var scope = _services.CreateScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
|
||||
var cutoff30d = DateTime.UtcNow.AddDays(-30);
|
||||
|
||||
// Find traders active in the last 30 days
|
||||
var traderIds = await db.Trades
|
||||
.Where(t => t.ExecutedAt >= cutoff30d)
|
||||
.Select(t => t.TraderId)
|
||||
.Distinct()
|
||||
.ToListAsync(ct);
|
||||
|
||||
_logger.LogInformation("Found {Count} active traders to analyze", traderIds.Count);
|
||||
|
||||
foreach (var id in traderIds)
|
||||
{
|
||||
await UpdateTraderAnalyticsAsync(db, id, ct);
|
||||
}
|
||||
|
||||
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