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:
Richard
2026-07-01 19:53:29 +02:00
commit afb251acfc
107 changed files with 9613 additions and 0 deletions
@@ -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
}
}