feat: Add category performance, update WebUI and OpenRouter integration, fix bugs

This commit is contained in:
Richard
2026-07-05 14:13:16 +02:00
parent d102af2965
commit e9e9ce0d5a
30 changed files with 2302 additions and 54 deletions
@@ -1,4 +1,5 @@
using Predictalytics.Application.Interfaces; using Predictalytics.Application.Interfaces;
using Predictalytics.Application.Services;
namespace Predictalytics.Api.Endpoints; namespace Predictalytics.Api.Endpoints;
@@ -35,6 +36,18 @@ public static class TraderEndpoints
return Results.Ok(); return Results.Ok();
}); });
group.MapPost("/{id:int}/watchlist", async (int id, WatchlistService svc, CancellationToken ct) =>
{
await svc.AddAsync(id, "Watched via UI", null, ct);
return Results.Ok();
});
group.MapDelete("/{id:int}/watchlist", async (int id, WatchlistService svc, CancellationToken ct) =>
{
await svc.RemoveByTraderIdAsync(id, ct);
return Results.Ok();
});
group.MapPost("/{id:int}/ai-analysis", async (int id, bool manual, IAiStrategyAnalysisService aiSvc, CancellationToken ct) => group.MapPost("/{id:int}/ai-analysis", async (int id, bool manual, IAiStrategyAnalysisService aiSvc, CancellationToken ct) =>
{ {
var summary = await aiSvc.AnalyzeTraderStrategyAsync(id, manual, ct); var summary = await aiSvc.AnalyzeTraderStrategyAsync(id, manual, ct);
+4
View File
@@ -6,5 +6,9 @@
"LogLevel": { "LogLevel": {
"Default": "Information" "Default": "Information"
} }
},
"OpenRouter": {
"BaseUrl": "https://openrouter.ai/api/v1",
"ApiKey": "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"
} }
} }
+13
View File
@@ -184,9 +184,13 @@
<div style="display:flex; align-items:center; gap:16px"> <div style="display:flex; align-items:center; gap:16px">
<button class="btn-back" onclick="navigateBack()">← Back</button> <button class="btn-back" onclick="navigateBack()">← Back</button>
<h1 class="page-title" id="td-name" style="margin-bottom:0">Trader Name</h1> <h1 class="page-title" id="td-name" style="margin-bottom:0">Trader Name</h1>
<button class="btn-sm" id="btn-toggle-watchlist" style="padding:4px 8px; margin-left:10px; background:var(--bg-input);">☆ Watchlist</button>
</div> </div>
<div>
<button class="btn-sm" id="btn-open-platform" style="padding:8px 16px; background:var(--primary); display:none; margin-right:10px;">Open Platform</button>
<button class="btn-sm" id="btn-refresh-trader" style="padding:8px 16px; background:var(--bg-input)">Sync History</button> <button class="btn-sm" id="btn-refresh-trader" style="padding:8px 16px; background:var(--bg-input)">Sync History</button>
</div> </div>
</div>
<div class="detail-grid"> <div class="detail-grid">
<div class="detail-sidebar"> <div class="detail-sidebar">
<div class="card"> <div class="card">
@@ -224,6 +228,15 @@
<div class="metric-card"><div class="metric-label">Total Trades</div><div class="metric-value" id="td-trades"></div></div> <div class="metric-card"><div class="metric-label">Total Trades</div><div class="metric-value" id="td-trades"></div></div>
<div class="metric-card accent"><div class="metric-label">Score</div><div class="metric-value" id="td-score"></div></div> <div class="metric-card accent"><div class="metric-label">Score</div><div class="metric-value" id="td-score"></div></div>
</div> </div>
<div class="card" style="margin-bottom: 24px;">
<div class="card-header"><h2>Category Specialization</h2></div>
<div class="table-wrap">
<table class="data-table">
<thead><tr><th>Category</th><th>Win Rate</th><th>PnL</th><th>Volume</th><th>Trades</th></tr></thead>
<tbody id="td-categoryBody"></tbody>
</table>
</div>
</div>
<div class="card"> <div class="card">
<div class="card-header"><h2>Recent Trades</h2></div> <div class="card-header"><h2>Recent Trades</h2></div>
<div class="table-wrap"> <div class="table-wrap">
+37 -5
View File
@@ -214,7 +214,7 @@ async function loadDashboard() {
tbody.innerHTML = data.topTraders.map((t, i) => ` tbody.innerHTML = data.topTraders.map((t, i) => `
<tr> <tr>
<td onclick="viewTrader(${t.id})" style="cursor:pointer">${i + 1}</td> <td onclick="viewTrader(${t.id})" style="cursor:pointer">${i + 1}</td>
<td><strong>${t.platform === 'Polymarket' ? `<a href="https://polymarket.com/profile/${t.platformUserId}" target="_blank" style="color:var(--text);text-decoration:underline;">${t.displayName}</a>` : t.displayName}</strong></td> <td><strong><a href="#" onclick="viewTrader(${t.id}); return false;" style="color:var(--primary);text-decoration:none;">${t.displayName}</a></strong></td>
<td onclick="viewTrader(${t.id})" style="cursor:pointer">${t.platform}</td> <td onclick="viewTrader(${t.id})" style="cursor:pointer">${t.platform}</td>
<td onclick="viewTrader(${t.id})" style="cursor:pointer"><strong>${Number(t.combinedScore).toFixed(1)}</strong></td> <td onclick="viewTrader(${t.id})" style="cursor:pointer"><strong>${Number(t.combinedScore).toFixed(1)}</strong></td>
<td onclick="viewTrader(${t.id})" style="cursor:pointer">${fmt.pct(t.winRate)}</td> <td onclick="viewTrader(${t.id})" style="cursor:pointer">${fmt.pct(t.winRate)}</td>
@@ -231,7 +231,7 @@ async function loadDashboard() {
<td>${fmt.time(t.executedAt)}</td> <td>${fmt.time(t.executedAt)}</td>
<td onclick="viewTrader(${t.traderId})" style="cursor:pointer; color:var(--primary)">${t.traderName}</td> <td onclick="viewTrader(${t.traderId})" style="cursor:pointer; color:var(--primary)">${t.traderName}</td>
<td onclick="${t.dbMarketId ? `viewMarket(${t.dbMarketId})` : `''`}" style="cursor:${t.dbMarketId ? 'pointer' : 'default'}" title="Market ID: ${t.marketId}"> <td onclick="${t.dbMarketId ? `viewMarket(${t.dbMarketId})` : `''`}" style="cursor:${t.dbMarketId ? 'pointer' : 'default'}" title="Market ID: ${t.marketId}">
${t.dbMarketId ? `<span style="color:var(--primary)">Market #${t.dbMarketId}</span>` : (t.marketId ? t.marketId.substring(0, 12) + '...' : '—')} ${t.marketName || (t.marketId.length > 20 ? t.marketId.substring(0,20)+'...' : t.marketId)}
</td> </td>
<td>${fmt.side(t.side)}</td> <td>${fmt.side(t.side)}</td>
<td>${Number(t.price).toFixed(2)}</td> <td>${Number(t.price).toFixed(2)}</td>
@@ -287,7 +287,7 @@ async function loadTraders() {
tbody.innerHTML = data.map((t, i) => ` tbody.innerHTML = data.map((t, i) => `
<tr> <tr>
<td>${i + 1}</td> <td>${i + 1}</td>
<td><strong>${t.platform === 'Polymarket' ? `<a href="https://polymarket.com/profile/${t.platformUserId}" target="_blank" style="color:var(--text);text-decoration:underline;">${t.displayName}</a>` : t.displayName}</strong></td> <td><strong><a href="#" onclick="viewTrader(${t.id}); return false;" style="color:var(--primary);text-decoration:none;">${t.displayName}</a></strong></td>
<td>${t.platform}</td> <td>${t.platform}</td>
<td><strong>${Number(t.combinedScore).toFixed(1)}</strong></td> <td><strong>${Number(t.combinedScore).toFixed(1)}</strong></td>
<td>${fmt.pct(t.winRate)}</td> <td>${fmt.pct(t.winRate)}</td>
@@ -351,7 +351,7 @@ async function viewTrader(id) {
document.getElementById('td-platformId').textContent = t.platformUserId; document.getElementById('td-platformId').textContent = t.platformUserId;
document.getElementById('td-tier').innerHTML = fmt.tier(t.tier); document.getElementById('td-tier').innerHTML = fmt.tier(t.tier);
document.getElementById('td-strategy').textContent = t.strategy; document.getElementById('td-strategy').textContent = t.strategy;
document.getElementById('td-winrate').textContent = fmt.pct(t.winRate); document.getElementById('td-winrate').innerHTML = fmt.pct(t.winRate);
document.getElementById('td-pnl').innerHTML = fmt.pnl(t.totalPnl); document.getElementById('td-pnl').innerHTML = fmt.pnl(t.totalPnl);
document.getElementById('td-trades').textContent = fmt.num(t.totalTrades); document.getElementById('td-trades').textContent = fmt.num(t.totalTrades);
document.getElementById('td-score').textContent = Number(t.combinedScore).toFixed(1); document.getElementById('td-score').textContent = Number(t.combinedScore).toFixed(1);
@@ -363,12 +363,44 @@ async function viewTrader(id) {
const aiBtn = document.getElementById('btn-ai-analysis'); const aiBtn = document.getElementById('btn-ai-analysis');
aiBtn.onclick = () => triggerAiAnalysis(id, true); aiBtn.onclick = () => triggerAiAnalysis(id, true);
const wlBtn = document.getElementById('btn-toggle-watchlist');
wlBtn.textContent = t.isOnWatchlist ? '★ Watchlist (Remove)' : '☆ Watchlist (Add)';
wlBtn.onclick = async () => {
const method = t.isOnWatchlist ? 'DELETE' : 'POST';
await api(`/api/traders/${id}/watchlist`, { method });
viewTrader(id); // Reload to update UI
};
const openBtn = document.getElementById('btn-open-platform');
if (t.platform === 'Polymarket') {
openBtn.style.display = 'inline-block';
openBtn.textContent = 'Open on Polymarket';
openBtn.onclick = () => window.open(`https://polymarket.com/profile/${t.platformUserId}`, '_blank');
} else {
openBtn.style.display = 'none';
}
const catBody = document.getElementById('td-categoryBody');
if (t.categoryPerformances && t.categoryPerformances.length > 0) {
catBody.innerHTML = t.categoryPerformances.map(c => `
<tr>
<td>${c.category}</td>
<td>${fmt.pct(c.winRate)}</td>
<td>${fmt.pnl(c.totalPnL)}</td>
<td>${fmt.usd(c.totalVolume)}</td>
<td>${fmt.num(c.totalTrades)}</td>
</tr>
`).join('');
} else {
catBody.innerHTML = '<tr><td colspan="5" style="text-align:center; padding:16px; color:var(--text-muted)">No category data available</td></tr>';
}
const tbody = document.getElementById('td-tradesBody'); const tbody = document.getElementById('td-tradesBody');
tbody.innerHTML = t.recentTrades.map(tr => ` tbody.innerHTML = t.recentTrades.map(tr => `
<tr> <tr>
<td>${fmt.time(tr.executedAt)}</td> <td>${fmt.time(tr.executedAt)}</td>
<td onclick="${tr.dbMarketId ? `viewMarket(${tr.dbMarketId})` : `''`}" style="cursor:${tr.dbMarketId ? 'pointer' : 'default'}; color:${tr.dbMarketId ? 'var(--primary)' : 'inherit'}" title="Market ID: ${tr.marketId}"> <td onclick="${tr.dbMarketId ? `viewMarket(${tr.dbMarketId})` : `''`}" style="cursor:${tr.dbMarketId ? 'pointer' : 'default'}; color:${tr.dbMarketId ? 'var(--primary)' : 'inherit'}" title="Market ID: ${tr.marketId}">
${tr.dbMarketId ? `Market #${tr.dbMarketId}` : (tr.marketId ? tr.marketId.substring(0, 16) + '...' : '—')} ${tr.marketName || (tr.marketId.length > 20 ? tr.marketId.substring(0,20)+'...' : tr.marketId)}
</td> </td>
<td>${fmt.side(tr.side)}</td> <td>${fmt.side(tr.side)}</td>
<td>${Number(tr.price).toFixed(2)}</td> <td>${Number(tr.price).toFixed(2)}</td>
@@ -97,8 +97,14 @@ public class AnalyticsServiceTests
private class MockDiscoveryService : IDiscoveryService private class MockDiscoveryService : IDiscoveryService
{ {
public Task<int> ImportTraderAsync(PlatformType platform, string platformUserId, string displayName, CancellationToken ct) => Task.FromResult(0); public Task<int> ImportTraderAsync(PlatformType platform, string platformUserId, string displayName, bool isAutoDiscovered = true, CancellationToken ct = default)
{
return Task.FromResult(1);
}
public Task ScanTopHoldersAsync(CancellationToken ct) => Task.CompletedTask; public Task ScanTopHoldersAsync(CancellationToken ct) => Task.CompletedTask;
public Task<IReadOnlyList<DiscoveredTrader>> RunDiscoveryAsync(PlatformType platform, CancellationToken ct) => Task.FromResult<IReadOnlyList<DiscoveredTrader>>(new List<DiscoveredTrader>()); public Task<IReadOnlyList<DiscoveredTrader>> RunDiscoveryAsync(PlatformType platform, CancellationToken ct = default)
{
return Task.FromResult<IReadOnlyList<DiscoveredTrader>>(new List<DiscoveredTrader>());
}
} }
} }
@@ -8,6 +8,7 @@ public class MarketDetailDto
public string Question { get; set; } = ""; public string Question { get; set; } = "";
public string? Description { get; set; } public string? Description { get; set; }
public string Category { get; set; } = ""; public string Category { get; set; } = "";
public string Subcategory { get; set; } = "";
public double Volume { get; set; } public double Volume { get; set; }
public double Liquidity { get; set; } public double Liquidity { get; set; }
public DateTime? EndDate { get; set; } public DateTime? EndDate { get; set; }
@@ -9,6 +9,7 @@ public record TradeDto(
string Platform, string Platform,
int? DbMarketId, int? DbMarketId,
string MarketId, string MarketId,
string MarketName,
string Outcome, string Outcome,
string Side, string Side,
decimal Price, decimal Price,
@@ -43,5 +43,15 @@ public record TraderDetailDto(
DateTime CreatedAt, DateTime CreatedAt,
DateTime? LastPolledAt, DateTime? LastPolledAt,
string? AiStrategySummary, string? AiStrategySummary,
IReadOnlyList<TradeDto> RecentTrades IReadOnlyList<TradeDto> RecentTrades,
IReadOnlyList<TraderCategoryPerformanceDto> CategoryPerformances
);
public record TraderCategoryPerformanceDto(
string Category,
decimal TotalVolume,
decimal TotalPnL,
int TotalTrades,
int WinningTrades,
decimal WinRate
); );
@@ -1,4 +1,4 @@
using Predictalytics.Domain.Enums; using Predictalytics.Domain.Enums;
using Predictalytics.Domain.Interfaces; using Predictalytics.Domain.Interfaces;
namespace Predictalytics.Application.Interfaces; namespace Predictalytics.Application.Interfaces;
@@ -9,5 +9,5 @@ public interface IDiscoveryService
Task<IReadOnlyList<DiscoveredTrader>> RunDiscoveryAsync(PlatformType platform, CancellationToken ct = default); Task<IReadOnlyList<DiscoveredTrader>> RunDiscoveryAsync(PlatformType platform, CancellationToken ct = default);
/// <summary>Import a discovered trader into the tracking system.</summary> /// <summary>Import a discovered trader into the tracking system.</summary>
Task<int> ImportTraderAsync(PlatformType platform, string platformUserId, string displayName, CancellationToken ct = default); Task<int> ImportTraderAsync(PlatformType platform, string platformUserId, string displayName, bool isAutoDiscovered = true, CancellationToken ct = default);
} }
@@ -186,13 +186,22 @@ public class AnalyticsService : IAnalyticsService
var trades = await _tradeRepo.GetByTraderIdAsync(traderId, 0, 50, ct); var trades = await _tradeRepo.GetByTraderIdAsync(traderId, 0, 50, ct);
var wl = await _watchlistRepo.GetByTraderIdAsync(traderId, ct); var wl = await _watchlistRepo.GetByTraderIdAsync(traderId, ct);
var s = trader.CurrentScore; var s = trader.CurrentScore;
var perfs = trader.CategoryPerformances.Select(p => new TraderCategoryPerformanceDto(
p.Category.ToString(),
p.TotalVolume,
p.TotalPnL,
p.TotalTrades,
p.WinningTrades,
p.WinRate)).ToList();
return new TraderDetailDto(trader.Id, trader.Platform.ToString(), trader.PlatformUserId, trader.DisplayName, return new TraderDetailDto(trader.Id, trader.Platform.ToString(), trader.PlatformUserId, trader.DisplayName,
trader.Notes, trader.Tier.ToString(), trader.Strategy.ToString(), trader.IsSuspectedBot, trader.ManualPriorityOverride, trader.Notes, trader.Tier.ToString(), trader.Strategy.ToString(), trader.IsSuspectedBot, trader.ManualPriorityOverride,
trader.WinRate, trader.TotalPnl, trader.TotalTrades, trader.WinRate, trader.TotalPnl, trader.TotalTrades,
s?.ActivityScore ?? 0, s?.QualityScore ?? 0, s?.VolumeScore ?? 0, s?.TimingScore ?? 0, s?.ActivityScore ?? 0, s?.QualityScore ?? 0, s?.VolumeScore ?? 0, s?.TimingScore ?? 0,
s?.CombinedScore ?? 0, s?.CopytradingScore ?? 0, s?.Rank ?? 0, wl != null, trader.CreatedAt, trader.LastPolledAt, s?.CombinedScore ?? 0, s?.CopytradingScore ?? 0, s?.Rank ?? 0, wl != null, trader.CreatedAt, trader.LastPolledAt,
trader.AiStrategySummary, trader.AiStrategySummary,
trades.Select(MapTradeDto).ToList()); trades.Select(MapTradeDto).ToList(),
perfs);
} }
public async Task<MarketDetailDto?> GetMarketDetailAsync(int marketId, CancellationToken ct = default) public async Task<MarketDetailDto?> GetMarketDetailAsync(int marketId, CancellationToken ct = default)
@@ -215,7 +224,8 @@ public class AnalyticsService : IAnalyticsService
PlatformMarketId = market.ConditionId, PlatformMarketId = market.ConditionId,
Question = market.Question, Question = market.Question,
Description = market.Description, Description = market.Description,
Category = market.Category, Category = market.Category.ToString(),
Subcategory = market.Subcategory,
Volume = (double)market.Volume, Volume = (double)market.Volume,
Liquidity = (double)market.Liquidity, Liquidity = (double)market.Liquidity,
EndDate = market.EndDate, EndDate = market.EndDate,
@@ -415,7 +425,7 @@ public class AnalyticsService : IAnalyticsService
throw new ArgumentException($"Invalid platform: {platform}"); throw new ArgumentException($"Invalid platform: {platform}");
_logger.LogInformation("Manually adding trader {Wallet} for platform {Platform}", walletAddress, platform); _logger.LogInformation("Manually adding trader {Wallet} for platform {Platform}", walletAddress, platform);
return await _discovery.ImportTraderAsync(pType, walletAddress, walletAddress[..Math.Min(10, walletAddress.Length)] + "...", ct); return await _discovery.ImportTraderAsync(pType, walletAddress, walletAddress[..Math.Min(10, walletAddress.Length)] + "...", false, ct);
} }
private static TraderDto MapTraderDto(Trader t, HashSet<int> wIds) => new( private static TraderDto MapTraderDto(Trader t, HashSet<int> wIds) => new(
@@ -424,6 +434,6 @@ public class AnalyticsService : IAnalyticsService
wIds.Contains(t.Id), t.IsSuspectedBot, t.LastPolledAt); wIds.Contains(t.Id), t.IsSuspectedBot, t.LastPolledAt);
private static TradeDto MapTradeDto(Trade t) => new( private static TradeDto MapTradeDto(Trade t) => new(
t.Id, t.TraderId, t.Trader?.DisplayName ?? "", t.Platform.ToString(), t.Id, t.TraderId, t.Trader?.DisplayName ?? "?", t.Platform.ToString(),
t.DbMarketId, t.MarketId, t.Outcome, t.Side.ToString(), t.Price, t.Size, t.Amount, t.ExecutedAt); t.DbMarketId, t.MarketId, t.DbMarket?.Question ?? t.MarketId, t.Outcome, t.Side.ToString(), t.Price, t.Size, t.Amount, t.ExecutedAt);
} }
@@ -48,7 +48,7 @@ public class DiscoveryService : IDiscoveryService
var existing = await _traderRepo.GetByPlatformIdAsync(platform, d.PlatformUserId, ct); var existing = await _traderRepo.GetByPlatformIdAsync(platform, d.PlatformUserId, ct);
if (existing == null) if (existing == null)
{ {
await ImportTraderAsync(platform, d.PlatformUserId, d.DisplayName, ct); await ImportTraderAsync(platform, d.PlatformUserId, d.DisplayName, true, ct);
newCount++; newCount++;
} }
} }
@@ -59,7 +59,7 @@ public class DiscoveryService : IDiscoveryService
return discovered; return discovered;
} }
public async Task<int> ImportTraderAsync(PlatformType platform, string platformUserId, string displayName, CancellationToken ct = default) public async Task<int> ImportTraderAsync(PlatformType platform, string platformUserId, string displayName, bool isAutoDiscovered = true, CancellationToken ct = default)
{ {
var existing = await _traderRepo.GetByPlatformIdAsync(platform, platformUserId, ct); var existing = await _traderRepo.GetByPlatformIdAsync(platform, platformUserId, ct);
if (existing != null) return existing.Id; if (existing != null) return existing.Id;
@@ -69,7 +69,7 @@ public class DiscoveryService : IDiscoveryService
Platform = platform, Platform = platform,
PlatformUserId = platformUserId, PlatformUserId = platformUserId,
DisplayName = string.IsNullOrEmpty(displayName) ? platformUserId[..8] + "..." : displayName, DisplayName = string.IsNullOrEmpty(displayName) ? platformUserId[..8] + "..." : displayName,
IsAutoDiscovered = true, IsAutoDiscovered = isAutoDiscovered,
CreatedAt = DateTime.UtcNow CreatedAt = DateTime.UtcNow
}; };
@@ -1,4 +1,4 @@
using Predictalytics.Domain.Entities; using Predictalytics.Domain.Entities;
using Predictalytics.Domain.Interfaces; using Predictalytics.Domain.Interfaces;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@@ -29,4 +29,14 @@ public class WatchlistService
await _repo.RemoveAsync(id, ct); await _repo.RemoveAsync(id, ct);
_logger.LogInformation("Removed watchlist entry {Id}", id); _logger.LogInformation("Removed watchlist entry {Id}", id);
} }
public async Task RemoveByTraderIdAsync(int traderId, CancellationToken ct = default)
{
var existing = await _repo.GetByTraderIdAsync(traderId, ct);
if (existing != null)
{
await _repo.RemoveAsync(existing.Id, ct);
_logger.LogInformation("Removed trader {TraderId} from watchlist", traderId);
}
}
} }
+4 -1
View File
@@ -38,7 +38,10 @@ public class Market
public string Question { get; set; } = string.Empty; public string Question { get; set; } = string.Empty;
/// <summary>Category / tag (e.g. "Politics", "Crypto", "Sports").</summary> /// <summary>Category / tag (e.g. "Politics", "Crypto", "Sports").</summary>
public string Category { get; set; } = string.Empty; public MarketCategory Category { get; set; }
/// <summary>Subcategory (e.g. "Basketball", "Elections", "Bitcoin").</summary>
public string Subcategory { get; set; } = string.Empty;
/// <summary>Current total volume traded.</summary> /// <summary>Current total volume traded.</summary>
public decimal Volume { get; set; } public decimal Volume { get; set; }
@@ -73,4 +73,5 @@ public class Trader
public virtual TraderAnalytics? Analytics { get; set; } public virtual TraderAnalytics? Analytics { get; set; }
public ICollection<WatchlistEntry> WatchlistEntries { get; set; } = new List<WatchlistEntry>(); public ICollection<WatchlistEntry> WatchlistEntries { get; set; } = new List<WatchlistEntry>();
public ICollection<TraderPosition> Positions { get; set; } = new List<TraderPosition>(); public ICollection<TraderPosition> Positions { get; set; } = new List<TraderPosition>();
public ICollection<TraderCategoryPerformance> CategoryPerformances { get; set; } = new List<TraderCategoryPerformance>();
} }
@@ -0,0 +1,28 @@
using Predictalytics.Domain.Enums;
namespace Predictalytics.Domain.Entities;
public class TraderCategoryPerformance
{
public int Id { get; set; }
public int TraderId { get; set; }
public Trader Trader { get; set; } = null!;
public MarketCategory Category { get; set; }
/// <summary>Total volume traded in this category (USD).</summary>
public decimal TotalVolume { get; set; }
/// <summary>Total Profit/Loss in this category (USD).</summary>
public decimal TotalPnL { get; set; }
/// <summary>Number of trades in this category.</summary>
public int TotalTrades { get; set; }
/// <summary>Number of profitable trades in this category.</summary>
public int WinningTrades { get; set; }
/// <summary>Calculated win rate for this category (0.0 - 1.0).</summary>
public decimal WinRate => TotalTrades > 0 ? (decimal)WinningTrades / TotalTrades : 0;
}
@@ -0,0 +1,13 @@
namespace Predictalytics.Domain.Enums;
public enum MarketCategory
{
Other = 0,
Politics = 1,
Crypto = 2,
Sports = 3,
PopCulture = 4,
Science = 5,
GlobalNews = 6,
Economy = 7
}
@@ -18,6 +18,7 @@ public class AppDbContext : DbContext
public DbSet<MarketAnalytics> MarketAnalytics => Set<MarketAnalytics>(); public DbSet<MarketAnalytics> MarketAnalytics => Set<MarketAnalytics>();
public DbSet<TraderPosition> TraderPositions => Set<TraderPosition>(); public DbSet<TraderPosition> TraderPositions => Set<TraderPosition>();
public DbSet<MarketOutcomePriceSnapshot> MarketOutcomePriceSnapshots => Set<MarketOutcomePriceSnapshot>(); public DbSet<MarketOutcomePriceSnapshot> MarketOutcomePriceSnapshots => Set<MarketOutcomePriceSnapshot>();
public DbSet<TraderCategoryPerformance> TraderCategoryPerformances => Set<TraderCategoryPerformance>();
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { } public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }
@@ -54,7 +55,7 @@ public class AppDbContext : DbContext
// Outcome: labels can be long (e.g. anime titles or sports match descriptions) // Outcome: labels can be long (e.g. anime titles or sports match descriptions)
e.Property(t => t.Outcome).HasMaxLength(128); e.Property(t => t.Outcome).HasMaxLength(128);
// Price: 0.001.00 on prediction markets, 6 decimals sufficient // Price: 0.001.00 on prediction markets, 6 decimals sufficient
e.Property(t => t.Price).HasPrecision(10, 6); e.Property(t => t.Price).HasPrecision(18, 6);
// Size: number of shares, needs more integer digits // Size: number of shares, needs more integer digits
e.Property(t => t.Size).HasPrecision(14, 6); e.Property(t => t.Size).HasPrecision(14, 6);
e.Property(t => t.Amount).HasPrecision(18, 4); e.Property(t => t.Amount).HasPrecision(18, 4);
@@ -92,7 +93,8 @@ public class AppDbContext : DbContext
e.Property(m => m.Question).HasMaxLength(1024); e.Property(m => m.Question).HasMaxLength(1024);
e.Property(m => m.Description).HasMaxLength(4096); e.Property(m => m.Description).HasMaxLength(4096);
e.Property(m => m.ImageUrl).HasMaxLength(1024); e.Property(m => m.ImageUrl).HasMaxLength(1024);
e.Property(m => m.Category).HasMaxLength(128); e.Property(m => m.Category).HasConversion<string>().HasMaxLength(64);
e.Property(m => m.Subcategory).HasMaxLength(128);
e.Property(m => m.Volume).HasPrecision(18, 4); e.Property(m => m.Volume).HasPrecision(18, 4);
e.Property(m => m.Volume24h).HasPrecision(18, 4); e.Property(m => m.Volume24h).HasPrecision(18, 4);
e.Property(m => m.Liquidity).HasPrecision(18, 4); e.Property(m => m.Liquidity).HasPrecision(18, 4);
@@ -142,6 +144,17 @@ public class AppDbContext : DbContext
e.HasOne(a => a.Trader).WithMany().HasForeignKey(a => a.TraderId).OnDelete(DeleteBehavior.SetNull); e.HasOne(a => a.Trader).WithMany().HasForeignKey(a => a.TraderId).OnDelete(DeleteBehavior.SetNull);
}); });
// TraderCategoryPerformance
mb.Entity<TraderCategoryPerformance>(e =>
{
e.HasKey(tcp => tcp.Id);
e.HasOne(tcp => tcp.Trader).WithMany().HasForeignKey(tcp => tcp.TraderId).OnDelete(DeleteBehavior.Cascade);
e.Property(tcp => tcp.Category).HasConversion<string>().HasMaxLength(64);
e.Property(tcp => tcp.TotalVolume).HasPrecision(18, 4);
e.Property(tcp => tcp.TotalPnL).HasPrecision(18, 4);
e.HasIndex(tcp => new { tcp.TraderId, tcp.Category }).IsUnique();
});
// PlatformConfig // PlatformConfig
mb.Entity<PlatformConfig>(e => mb.Entity<PlatformConfig>(e =>
{ {
@@ -192,8 +192,8 @@ public class MarketRepository : IMarketRepository
existing.PlatformMarketId = updated.PlatformMarketId; existing.PlatformMarketId = updated.PlatformMarketId;
existing.QuestionId = updated.QuestionId; existing.QuestionId = updated.QuestionId;
existing.Description = updated.Description; existing.Description = updated.Description;
existing.ImageUrl = updated.ImageUrl;
existing.Category = updated.Category; existing.Category = updated.Category;
existing.Subcategory = updated.Subcategory;
existing.Volume = updated.Volume; existing.Volume = updated.Volume;
existing.Volume24h = updated.Volume24h; existing.Volume24h = updated.Volume24h;
existing.Liquidity = updated.Liquidity; existing.Liquidity = updated.Liquidity;
@@ -230,7 +230,7 @@ public class MarketRepository : IMarketRepository
market.Description = StringHelper.Truncate(market.Description, 4096); market.Description = StringHelper.Truncate(market.Description, 4096);
market.MarketSlug = StringHelper.Truncate(market.MarketSlug, 512) ?? ""; market.MarketSlug = StringHelper.Truncate(market.MarketSlug, 512) ?? "";
market.ImageUrl = StringHelper.Truncate(market.ImageUrl, 1024); market.ImageUrl = StringHelper.Truncate(market.ImageUrl, 1024);
market.Category = StringHelper.Truncate(market.Category, 128) ?? ""; market.Subcategory = StringHelper.Truncate(market.Subcategory, 128) ?? "";
foreach (var o in market.Outcomes) foreach (var o in market.Outcomes)
{ {
@@ -14,27 +14,27 @@ public class TradeRepository : ITradeRepository
=> await _db.Trades.FirstOrDefaultAsync(t => t.Platform == platform && t.PlatformTradeId == platformTradeId, ct); => await _db.Trades.FirstOrDefaultAsync(t => t.Platform == platform && t.PlatformTradeId == platformTradeId, ct);
public async Task<IReadOnlyList<Trade>> GetByTraderIdAsync(int traderId, int skip = 0, int take = 50, CancellationToken ct = default) public async Task<IReadOnlyList<Trade>> GetByTraderIdAsync(int traderId, int skip = 0, int take = 50, CancellationToken ct = default)
=> await _db.Trades.Include(t => t.Trader).Where(t => t.TraderId == traderId) => await _db.Trades.Include(t => t.Trader).Include(t => t.DbMarket).Where(t => t.TraderId == traderId)
.OrderByDescending(t => t.ExecutedAt).Skip(skip).Take(take).ToListAsync(ct); .OrderByDescending(t => t.ExecutedAt).Skip(skip).Take(take).ToListAsync(ct);
public async Task<IReadOnlyList<Trade>> GetByDbMarketIdAsync(int dbMarketId, int skip = 0, int take = 50, CancellationToken ct = default) public async Task<IReadOnlyList<Trade>> GetByDbMarketIdAsync(int dbMarketId, int skip = 0, int take = 50, CancellationToken ct = default)
=> await _db.Trades.Include(t => t.Trader).Where(t => t.DbMarketId == dbMarketId) => await _db.Trades.Include(t => t.Trader).Include(t => t.DbMarket).Where(t => t.DbMarketId == dbMarketId)
.OrderByDescending(t => t.ExecutedAt).Skip(skip).Take(take).ToListAsync(ct); .OrderByDescending(t => t.ExecutedAt).Skip(skip).Take(take).ToListAsync(ct);
public async Task<IReadOnlyList<Trade>> GetByMarketIdAsync(string platformMarketId, int skip = 0, int take = 50, CancellationToken ct = default) public async Task<IReadOnlyList<Trade>> GetByMarketIdAsync(string platformMarketId, int skip = 0, int take = 50, CancellationToken ct = default)
=> await _db.Trades.Include(t => t.Trader).Where(t => t.MarketId == platformMarketId) => await _db.Trades.Include(t => t.Trader).Include(t => t.DbMarket).Where(t => t.MarketId == platformMarketId)
.OrderByDescending(t => t.ExecutedAt).Skip(skip).Take(take).ToListAsync(ct); .OrderByDescending(t => t.ExecutedAt).Skip(skip).Take(take).ToListAsync(ct);
public async Task<IReadOnlyList<Trade>> GetRecentAsync(int count = 50, PlatformType? platform = null, CancellationToken ct = default) public async Task<IReadOnlyList<Trade>> GetRecentAsync(int count = 50, PlatformType? platform = null, CancellationToken ct = default)
{ {
var q = _db.Trades.Include(t => t.Trader).AsQueryable(); var q = _db.Trades.Include(t => t.Trader).Include(t => t.DbMarket).AsQueryable();
if (platform.HasValue) q = q.Where(t => t.Platform == platform.Value); if (platform.HasValue) q = q.Where(t => t.Platform == platform.Value);
return await q.OrderByDescending(t => t.ExecutedAt).Take(count).ToListAsync(ct); return await q.OrderByDescending(t => t.ExecutedAt).Take(count).ToListAsync(ct);
} }
public async Task<IReadOnlyList<Trade>> GetLargestAsync(int count = 5, DateTime? since = null, CancellationToken ct = default) public async Task<IReadOnlyList<Trade>> GetLargestAsync(int count = 5, DateTime? since = null, CancellationToken ct = default)
{ {
var q = _db.Trades.Include(t => t.Trader).AsQueryable(); var q = _db.Trades.Include(t => t.Trader).Include(t => t.DbMarket).AsQueryable();
if (since.HasValue) q = q.Where(t => t.ExecutedAt >= since.Value); if (since.HasValue) q = q.Where(t => t.ExecutedAt >= since.Value);
return await q.OrderByDescending(t => t.Amount).Take(count).ToListAsync(ct); return await q.OrderByDescending(t => t.Amount).Take(count).ToListAsync(ct);
} }
@@ -11,10 +11,10 @@ public class TraderRepository : ITraderRepository
public TraderRepository(AppDbContext db) => _db = db; public TraderRepository(AppDbContext db) => _db = db;
public async Task<Trader?> GetByIdAsync(int id, CancellationToken ct = default) public async Task<Trader?> GetByIdAsync(int id, CancellationToken ct = default)
=> await _db.Traders.Include(t => t.CurrentScore).FirstOrDefaultAsync(t => t.Id == id, ct); => await _db.Traders.Include(t => t.CurrentScore).Include(t => t.CategoryPerformances).FirstOrDefaultAsync(t => t.Id == id, ct);
public async Task<Trader?> GetByPlatformIdAsync(PlatformType platform, string platformUserId, CancellationToken ct = default) public async Task<Trader?> GetByPlatformIdAsync(PlatformType platform, string platformUserId, CancellationToken ct = default)
=> await _db.Traders.Include(t => t.CurrentScore) => await _db.Traders.Include(t => t.CurrentScore).Include(t => t.CategoryPerformances)
.FirstOrDefaultAsync(t => t.Platform == platform && t.PlatformUserId == platformUserId, ct); .FirstOrDefaultAsync(t => t.Platform == platform && t.PlatformUserId == platformUserId, ct);
public async Task<IReadOnlyList<Trader>> GetAllAsync(PlatformType? platform = null, int skip = 0, int take = 50, CancellationToken ct = default) public async Task<IReadOnlyList<Trader>> GetAllAsync(PlatformType? platform = null, int skip = 0, int take = 50, CancellationToken ct = default)
@@ -75,15 +75,18 @@ public class TraderRepository : ITraderRepository
public async Task<IReadOnlyList<Trader>> GetTradersDueForTradeUpdateAsync(int cooldownHours = 12, int take = 20, CancellationToken ct = default) public async Task<IReadOnlyList<Trader>> GetTradersDueForTradeUpdateAsync(int cooldownHours = 12, int take = 20, CancellationToken ct = default)
{ {
// Prioritize: var normalCutoff = DateTime.UtcNow.AddHours(-cooldownHours);
// 1. Traders needing initial import (IsInitialImportComplete == false) var priorityCutoff = DateTime.UtcNow.AddHours(-1); // Sync priority traders more often, but not continuously
// 2. Traders where LastTradesUpdatedAt < cutoff (cooldownHours)
var cutoff = DateTime.UtcNow.AddHours(-cooldownHours);
return await _db.Traders return await _db.Traders
.Where(t => !t.IsInitialImportComplete || t.LastTradesUpdatedAt == null || t.LastTradesUpdatedAt < cutoff) .Include(t => t.WatchlistEntries)
.OrderBy(t => t.IsInitialImportComplete) // false (0) comes before true (1) .Where(t => t.LastTradesUpdatedAt == null ||
(!t.IsInitialImportComplete) ||
((!t.IsAutoDiscovered || t.WatchlistEntries.Any()) && t.LastTradesUpdatedAt < priorityCutoff) ||
(t.IsAutoDiscovered && t.LastTradesUpdatedAt < normalCutoff))
.OrderBy(t => t.IsAutoDiscovered) // Manual first (false = 0)
.ThenByDescending(t => t.WatchlistEntries.Any()) // Watchlisted next (true = 1)
.ThenBy(t => t.IsInitialImportComplete) // New ones next (false = 0)
.ThenBy(t => t.LastTradesUpdatedAt ?? DateTime.MinValue) // Oldest first .ThenBy(t => t.LastTradesUpdatedAt ?? DateTime.MinValue) // Oldest first
.Take(take) .Take(take)
.ToListAsync(ct); .ToListAsync(ct);
@@ -0,0 +1,46 @@
using Predictalytics.Domain.Enums;
namespace Predictalytics.Infrastructure.Helpers;
public static class MarketCategoryMapper
{
public static (MarketCategory Category, string Subcategory) Map(string rawCategory, string tags)
{
var searchString = $"{rawCategory} {tags}".ToLowerInvariant();
if (searchString.Contains("politic") || searchString.Contains("election") || searchString.Contains("trump") || searchString.Contains("biden"))
return (MarketCategory.Politics, GetSubcategory(rawCategory, tags, "Elections"));
if (searchString.Contains("crypto") || searchString.Contains("bitcoin") || searchString.Contains("eth") || searchString.Contains("solana"))
return (MarketCategory.Crypto, GetSubcategory(rawCategory, tags, "Crypto"));
if (searchString.Contains("sport") || searchString.Contains("nfl") || searchString.Contains("nba") || searchString.Contains("soccer") || searchString.Contains("tennis"))
return (MarketCategory.Sports, GetSubcategory(rawCategory, tags, "Sports"));
if (searchString.Contains("pop") || searchString.Contains("culture") || searchString.Contains("movie") || searchString.Contains("oscars") || searchString.Contains("music"))
return (MarketCategory.PopCulture, GetSubcategory(rawCategory, tags, "Pop Culture"));
if (searchString.Contains("science") || searchString.Contains("space") || searchString.Contains("weather") || searchString.Contains("climate"))
return (MarketCategory.Science, GetSubcategory(rawCategory, tags, "Science"));
if (searchString.Contains("news") || searchString.Contains("global") || searchString.Contains("world"))
return (MarketCategory.GlobalNews, GetSubcategory(rawCategory, tags, "Global News"));
if (searchString.Contains("economy") || searchString.Contains("finance") || searchString.Contains("business") || searchString.Contains("fed"))
return (MarketCategory.Economy, GetSubcategory(rawCategory, tags, "Economy"));
return (MarketCategory.Other, GetSubcategory(rawCategory, tags, "Other"));
}
private static string GetSubcategory(string rawCategory, string tags, string fallback)
{
if (!string.IsNullOrWhiteSpace(rawCategory) && !rawCategory.Equals("OVERALL", StringComparison.OrdinalIgnoreCase))
return rawCategory;
var firstTag = tags.Split(',', StringSplitOptions.RemoveEmptyEntries).FirstOrDefault()?.Trim();
if (!string.IsNullOrWhiteSpace(firstTag))
return firstTag;
return fallback;
}
}
@@ -0,0 +1,855 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Predictalytics.Infrastructure.Data;
#nullable disable
namespace Predictalytics.Infrastructure.Migrations
{
[DbContext(typeof(AppDbContext))]
[Migration("20260705114852_AddMarketCategoryAndSubcategory")]
partial class AddMarketCategoryAndSubcategory
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "8.0.11")
.HasAnnotation("Relational:MaxIdentifierLength", 64);
MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder);
modelBuilder.Entity("Predictalytics.Domain.Entities.Alert", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<int>("Id"));
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<bool>("IsRead")
.HasColumnType("tinyint(1)");
b.Property<string>("Message")
.IsRequired()
.HasMaxLength(4096)
.HasColumnType("varchar(4096)");
b.Property<int>("Platform")
.HasColumnType("int");
b.Property<int>("Severity")
.HasColumnType("int");
b.Property<string>("Title")
.IsRequired()
.HasMaxLength(512)
.HasColumnType("varchar(512)");
b.Property<int?>("TraderId")
.HasColumnType("int");
b.Property<int>("Type")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("CreatedAt");
b.HasIndex("TraderId");
b.ToTable("Alerts");
});
modelBuilder.Entity("Predictalytics.Domain.Entities.Event", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<int>("Id"));
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<DateTime>("DbCreatedAt")
.HasColumnType("datetime(6)");
b.Property<string>("Description")
.HasMaxLength(4096)
.HasColumnType("varchar(4096)");
b.Property<DateTime?>("EndDate")
.HasColumnType("datetime(6)");
b.Property<string>("ImageUrl")
.HasMaxLength(1024)
.HasColumnType("varchar(1024)");
b.Property<bool>("IsActive")
.HasColumnType("tinyint(1)");
b.Property<bool>("IsClosed")
.HasColumnType("tinyint(1)");
b.Property<DateTime?>("LastUpdatedAt")
.HasColumnType("datetime(6)");
b.Property<int>("Platform")
.HasColumnType("int");
b.Property<long>("PlatformEventId")
.HasColumnType("bigint");
b.Property<string>("Slug")
.IsRequired()
.HasMaxLength(512)
.HasColumnType("varchar(512)");
b.Property<DateTime?>("StartDate")
.HasColumnType("datetime(6)");
b.Property<string>("Tags")
.IsRequired()
.HasMaxLength(1024)
.HasColumnType("varchar(1024)");
b.Property<string>("Title")
.IsRequired()
.HasMaxLength(1024)
.HasColumnType("varchar(1024)");
b.HasKey("Id");
b.HasIndex("Platform", "PlatformEventId")
.IsUnique();
b.ToTable("Events");
});
modelBuilder.Entity("Predictalytics.Domain.Entities.Market", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<int>("Id"));
b.Property<string>("Category")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("varchar(64)");
b.Property<string>("ConditionId")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("varchar(256)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<DateTime>("DbCreatedAt")
.HasColumnType("datetime(6)");
b.Property<string>("Description")
.HasMaxLength(4096)
.HasColumnType("varchar(4096)");
b.Property<DateTime?>("EndDate")
.HasColumnType("datetime(6)");
b.Property<int>("EventId")
.HasColumnType("int");
b.Property<string>("ImageUrl")
.HasMaxLength(1024)
.HasColumnType("varchar(1024)");
b.Property<bool>("IsResolved")
.HasColumnType("tinyint(1)");
b.Property<DateTime?>("LastTradesUpdatedAt")
.HasColumnType("datetime(6)");
b.Property<DateTime?>("LastUpdatedAt")
.HasColumnType("datetime(6)");
b.Property<decimal>("Liquidity")
.HasPrecision(18, 4)
.HasColumnType("decimal(18,4)");
b.Property<string>("MarketSlug")
.IsRequired()
.HasMaxLength(512)
.HasColumnType("varchar(512)");
b.Property<int>("Platform")
.HasColumnType("int");
b.Property<long>("PlatformMarketId")
.HasColumnType("bigint");
b.Property<string>("Question")
.IsRequired()
.HasMaxLength(1024)
.HasColumnType("varchar(1024)");
b.Property<string>("QuestionId")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("varchar(256)");
b.Property<string>("ResolutionOutcome")
.HasColumnType("longtext");
b.Property<DateTime?>("StartDate")
.HasColumnType("datetime(6)");
b.Property<string>("Subcategory")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("varchar(128)");
b.Property<decimal>("Volume")
.HasPrecision(18, 4)
.HasColumnType("decimal(18,4)");
b.Property<decimal>("Volume24h")
.HasPrecision(18, 4)
.HasColumnType("decimal(18,4)");
b.HasKey("Id");
b.HasIndex("EventId");
b.HasIndex("Platform", "PlatformMarketId")
.IsUnique();
b.ToTable("Markets");
});
modelBuilder.Entity("Predictalytics.Domain.Entities.MarketAnalytics", b =>
{
b.Property<int>("MarketId")
.HasColumnType("int");
b.Property<decimal>("AverageTradeSize")
.HasPrecision(18, 4)
.HasColumnType("decimal(18,4)");
b.Property<decimal>("BotActivityScore")
.HasPrecision(8, 4)
.HasColumnType("decimal(8,4)");
b.Property<DateTime>("LastCalculatedAt")
.HasColumnType("datetime(6)");
b.Property<int>("UniqueTradersCount")
.HasColumnType("int");
b.HasKey("MarketId");
b.ToTable("MarketAnalytics");
});
modelBuilder.Entity("Predictalytics.Domain.Entities.MarketOutcome", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<int>("Id"));
b.Property<decimal>("CurrentPrice")
.HasPrecision(18, 8)
.HasColumnType("decimal(18,8)");
b.Property<string>("Label")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("varchar(256)");
b.Property<int>("MarketId")
.HasColumnType("int");
b.Property<int>("OutcomeIndex")
.HasColumnType("int");
b.Property<string>("TokenId")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("varchar(256)");
b.HasKey("Id");
b.HasIndex("TokenId");
b.HasIndex("MarketId", "OutcomeIndex")
.IsUnique();
b.ToTable("MarketOutcomes");
});
modelBuilder.Entity("Predictalytics.Domain.Entities.MarketOutcomePriceSnapshot", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<int>("Id"));
b.Property<int>("MarketOutcomeId")
.HasColumnType("int");
b.Property<decimal>("Price")
.HasPrecision(10, 6)
.HasColumnType("decimal(10,6)");
b.Property<DateTime>("Timestamp")
.HasColumnType("datetime(6)");
b.HasKey("Id");
b.HasIndex("MarketOutcomeId", "Timestamp");
b.ToTable("MarketOutcomePriceSnapshots");
});
modelBuilder.Entity("Predictalytics.Domain.Entities.PlatformConfig", b =>
{
b.Property<int>("Id")
.HasColumnType("int");
b.Property<string>("BaseUrl")
.HasMaxLength(1024)
.HasColumnType("varchar(1024)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<string>("DisplayName")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("varchar(256)");
b.Property<bool>("IsActive")
.HasColumnType("tinyint(1)");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("varchar(128)");
b.Property<string>("SettingsJson")
.HasColumnType("longtext");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("datetime(6)");
b.HasKey("Id");
b.ToTable("PlatformConfigs");
});
modelBuilder.Entity("Predictalytics.Domain.Entities.Trade", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<long>("Id"));
b.Property<decimal>("Amount")
.HasPrecision(18, 4)
.HasColumnType("decimal(18,4)");
b.Property<string>("AssetId")
.IsRequired()
.HasMaxLength(80)
.HasColumnType("varchar(80)");
b.Property<int?>("DbMarketId")
.HasColumnType("int");
b.Property<DateTime>("ExecutedAt")
.HasColumnType("datetime(6)");
b.Property<bool>("IsContextEnriched")
.HasColumnType("tinyint(1)");
b.Property<string>("MarketId")
.IsRequired()
.HasMaxLength(66)
.HasColumnType("varchar(66)");
b.Property<int?>("MarketOutcomeId")
.HasColumnType("int");
b.Property<string>("Outcome")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("varchar(128)");
b.Property<int>("Platform")
.HasColumnType("int");
b.Property<string>("PlatformTradeId")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("varchar(256)");
b.Property<decimal?>("PostTradePrice1m")
.HasColumnType("decimal(18,4)");
b.Property<decimal?>("PreTradePrice1m")
.HasColumnType("decimal(18,4)");
b.Property<decimal>("Price")
.HasPrecision(10, 6)
.HasColumnType("decimal(10,6)");
b.Property<int>("Side")
.HasColumnType("int");
b.Property<decimal>("Size")
.HasPrecision(14, 6)
.HasColumnType("decimal(14,6)");
b.Property<int>("TraderId")
.HasColumnType("int");
b.Property<string>("TransactionHash")
.HasMaxLength(66)
.HasColumnType("varchar(66)");
b.HasKey("Id");
b.HasIndex("AssetId");
b.HasIndex("DbMarketId");
b.HasIndex("ExecutedAt");
b.HasIndex("MarketOutcomeId");
b.HasIndex("TraderId");
b.HasIndex("Platform", "PlatformTradeId")
.IsUnique();
b.ToTable("Trades");
});
modelBuilder.Entity("Predictalytics.Domain.Entities.Trader", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<int>("Id"));
b.Property<string>("AiStrategySummary")
.HasColumnType("longtext");
b.Property<DateTime?>("AiStrategyUpdatedAt")
.HasColumnType("datetime(6)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<string>("DisplayName")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("varchar(256)");
b.Property<bool>("IsAutoDiscovered")
.HasColumnType("tinyint(1)");
b.Property<bool>("IsInitialImportComplete")
.HasColumnType("tinyint(1)");
b.Property<bool>("IsSuspectedBot")
.HasColumnType("tinyint(1)");
b.Property<DateTime?>("LastApiErrorAt")
.HasColumnType("datetime(6)");
b.Property<DateTime?>("LastPolledAt")
.HasColumnType("datetime(6)");
b.Property<DateTime?>("LastTradesUpdatedAt")
.HasColumnType("datetime(6)");
b.Property<int?>("ManualPriorityOverride")
.HasColumnType("int");
b.Property<string>("Notes")
.HasColumnType("longtext");
b.Property<int>("Platform")
.HasColumnType("int");
b.Property<string>("PlatformUserId")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("varchar(128)");
b.Property<int>("Strategy")
.HasColumnType("int");
b.Property<int>("Tier")
.HasColumnType("int");
b.Property<decimal>("TotalPnl")
.HasPrecision(18, 4)
.HasColumnType("decimal(18,4)");
b.Property<int>("TotalTrades")
.HasColumnType("int");
b.Property<decimal>("WinRate")
.HasPrecision(8, 4)
.HasColumnType("decimal(8,4)");
b.HasKey("Id");
b.HasIndex("Platform", "PlatformUserId")
.IsUnique();
b.ToTable("Traders");
});
modelBuilder.Entity("Predictalytics.Domain.Entities.TraderAnalytics", b =>
{
b.Property<int>("TraderId")
.HasColumnType("int");
b.Property<DateTime>("LastCalculatedAt")
.HasColumnType("datetime(6)");
b.Property<decimal>("OverallPnL")
.HasPrecision(18, 4)
.HasColumnType("decimal(18,4)");
b.Property<decimal>("OverallWinRate")
.HasPrecision(8, 4)
.HasColumnType("decimal(8,4)");
b.Property<decimal>("PnL24h")
.HasPrecision(18, 4)
.HasColumnType("decimal(18,4)");
b.Property<decimal>("PnL30d")
.HasPrecision(18, 4)
.HasColumnType("decimal(18,4)");
b.Property<decimal>("PnL7d")
.HasPrecision(18, 4)
.HasColumnType("decimal(18,4)");
b.Property<decimal>("WinRate24h")
.HasPrecision(8, 4)
.HasColumnType("decimal(8,4)");
b.Property<decimal>("WinRate30d")
.HasPrecision(8, 4)
.HasColumnType("decimal(8,4)");
b.Property<decimal>("WinRate7d")
.HasPrecision(8, 4)
.HasColumnType("decimal(8,4)");
b.HasKey("TraderId");
b.ToTable("TraderAnalytics");
});
modelBuilder.Entity("Predictalytics.Domain.Entities.TraderPosition", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<int>("Id"));
b.Property<decimal>("AvgCost")
.HasPrecision(10, 6)
.HasColumnType("decimal(10,6)");
b.Property<DateTime>("LastUpdatedAt")
.HasColumnType("datetime(6)");
b.Property<int>("MarketOutcomeId")
.HasColumnType("int");
b.Property<decimal>("RealizedPnl")
.HasPrecision(18, 4)
.HasColumnType("decimal(18,4)");
b.Property<decimal>("SharesHeld")
.HasPrecision(14, 6)
.HasColumnType("decimal(14,6)");
b.Property<int>("TraderId")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("MarketOutcomeId");
b.HasIndex("TraderId", "MarketOutcomeId")
.IsUnique();
b.ToTable("TraderPositions");
});
modelBuilder.Entity("Predictalytics.Domain.Entities.TraderScore", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<int>("Id"));
b.Property<decimal>("ActivityScore")
.HasPrecision(8, 4)
.HasColumnType("decimal(8,4)");
b.Property<DateTime>("CalculatedAt")
.HasColumnType("datetime(6)");
b.Property<decimal>("CombinedScore")
.HasPrecision(8, 4)
.HasColumnType("decimal(8,4)");
b.Property<decimal>("CopytradingScore")
.HasPrecision(8, 4)
.HasColumnType("decimal(8,4)");
b.Property<decimal>("QualityScore")
.HasPrecision(8, 4)
.HasColumnType("decimal(8,4)");
b.Property<int>("Rank")
.HasColumnType("int");
b.Property<decimal>("TimingScore")
.HasPrecision(8, 4)
.HasColumnType("decimal(8,4)");
b.Property<int>("TraderId")
.HasColumnType("int");
b.Property<decimal>("VolumeScore")
.HasPrecision(8, 4)
.HasColumnType("decimal(8,4)");
b.HasKey("Id");
b.HasIndex("TraderId")
.IsUnique();
b.ToTable("TraderScores");
});
modelBuilder.Entity("Predictalytics.Domain.Entities.WatchlistEntry", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<int>("Id"));
b.Property<DateTime>("AddedAt")
.HasColumnType("datetime(6)");
b.Property<bool>("AlertsEnabled")
.HasColumnType("tinyint(1)");
b.Property<string>("Label")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("varchar(256)");
b.Property<string>("Notes")
.HasColumnType("longtext");
b.Property<int>("TraderId")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("TraderId")
.IsUnique();
b.ToTable("WatchlistEntries");
});
modelBuilder.Entity("Predictalytics.Domain.Entities.Alert", b =>
{
b.HasOne("Predictalytics.Domain.Entities.Trader", "Trader")
.WithMany()
.HasForeignKey("TraderId")
.OnDelete(DeleteBehavior.SetNull);
b.Navigation("Trader");
});
modelBuilder.Entity("Predictalytics.Domain.Entities.Market", b =>
{
b.HasOne("Predictalytics.Domain.Entities.Event", "Event")
.WithMany("Markets")
.HasForeignKey("EventId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Event");
});
modelBuilder.Entity("Predictalytics.Domain.Entities.MarketAnalytics", b =>
{
b.HasOne("Predictalytics.Domain.Entities.Market", "Market")
.WithOne("Analytics")
.HasForeignKey("Predictalytics.Domain.Entities.MarketAnalytics", "MarketId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Market");
});
modelBuilder.Entity("Predictalytics.Domain.Entities.MarketOutcome", b =>
{
b.HasOne("Predictalytics.Domain.Entities.Market", "Market")
.WithMany("Outcomes")
.HasForeignKey("MarketId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Market");
});
modelBuilder.Entity("Predictalytics.Domain.Entities.MarketOutcomePriceSnapshot", b =>
{
b.HasOne("Predictalytics.Domain.Entities.MarketOutcome", "MarketOutcome")
.WithMany()
.HasForeignKey("MarketOutcomeId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("MarketOutcome");
});
modelBuilder.Entity("Predictalytics.Domain.Entities.Trade", b =>
{
b.HasOne("Predictalytics.Domain.Entities.Market", "DbMarket")
.WithMany()
.HasForeignKey("DbMarketId")
.OnDelete(DeleteBehavior.SetNull);
b.HasOne("Predictalytics.Domain.Entities.MarketOutcome", "MarketOutcome")
.WithMany()
.HasForeignKey("MarketOutcomeId")
.OnDelete(DeleteBehavior.SetNull);
b.HasOne("Predictalytics.Domain.Entities.Trader", "Trader")
.WithMany("Trades")
.HasForeignKey("TraderId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("DbMarket");
b.Navigation("MarketOutcome");
b.Navigation("Trader");
});
modelBuilder.Entity("Predictalytics.Domain.Entities.TraderAnalytics", b =>
{
b.HasOne("Predictalytics.Domain.Entities.Trader", "Trader")
.WithOne("Analytics")
.HasForeignKey("Predictalytics.Domain.Entities.TraderAnalytics", "TraderId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Trader");
});
modelBuilder.Entity("Predictalytics.Domain.Entities.TraderPosition", b =>
{
b.HasOne("Predictalytics.Domain.Entities.MarketOutcome", "MarketOutcome")
.WithMany()
.HasForeignKey("MarketOutcomeId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Predictalytics.Domain.Entities.Trader", "Trader")
.WithMany("Positions")
.HasForeignKey("TraderId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("MarketOutcome");
b.Navigation("Trader");
});
modelBuilder.Entity("Predictalytics.Domain.Entities.TraderScore", b =>
{
b.HasOne("Predictalytics.Domain.Entities.Trader", "Trader")
.WithOne("CurrentScore")
.HasForeignKey("Predictalytics.Domain.Entities.TraderScore", "TraderId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Trader");
});
modelBuilder.Entity("Predictalytics.Domain.Entities.WatchlistEntry", b =>
{
b.HasOne("Predictalytics.Domain.Entities.Trader", "Trader")
.WithMany("WatchlistEntries")
.HasForeignKey("TraderId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Trader");
});
modelBuilder.Entity("Predictalytics.Domain.Entities.Event", b =>
{
b.Navigation("Markets");
});
modelBuilder.Entity("Predictalytics.Domain.Entities.Market", b =>
{
b.Navigation("Analytics");
b.Navigation("Outcomes");
});
modelBuilder.Entity("Predictalytics.Domain.Entities.Trader", b =>
{
b.Navigation("Analytics");
b.Navigation("CurrentScore");
b.Navigation("Positions");
b.Navigation("Trades");
b.Navigation("WatchlistEntries");
});
#pragma warning restore 612, 618
}
}
}
@@ -0,0 +1,58 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Predictalytics.Infrastructure.Migrations
{
/// <inheritdoc />
public partial class AddMarketCategoryAndSubcategory : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "Subcategory",
table: "Markets",
type: "varchar(128)",
maxLength: 128,
nullable: false,
defaultValue: "")
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.Sql("UPDATE Markets SET Subcategory = Category;");
migrationBuilder.Sql("UPDATE Markets SET Category = 'Other';");
migrationBuilder.AlterColumn<string>(
name: "Category",
table: "Markets",
type: "varchar(64)",
maxLength: 64,
nullable: false,
oldClrType: typeof(string),
oldType: "varchar(128)",
oldMaxLength: 128)
.Annotation("MySql:CharSet", "utf8mb4")
.OldAnnotation("MySql:CharSet", "utf8mb4");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "Subcategory",
table: "Markets");
migrationBuilder.AlterColumn<string>(
name: "Category",
table: "Markets",
type: "varchar(128)",
maxLength: 128,
nullable: false,
oldClrType: typeof(string),
oldType: "varchar(64)",
oldMaxLength: 64)
.Annotation("MySql:CharSet", "utf8mb4")
.OldAnnotation("MySql:CharSet", "utf8mb4");
}
}
}
@@ -0,0 +1,904 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Predictalytics.Infrastructure.Data;
#nullable disable
namespace Predictalytics.Infrastructure.Migrations
{
[DbContext(typeof(AppDbContext))]
[Migration("20260705115634_AddTraderCategoryPerformance")]
partial class AddTraderCategoryPerformance
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "8.0.11")
.HasAnnotation("Relational:MaxIdentifierLength", 64);
MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder);
modelBuilder.Entity("Predictalytics.Domain.Entities.Alert", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<int>("Id"));
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<bool>("IsRead")
.HasColumnType("tinyint(1)");
b.Property<string>("Message")
.IsRequired()
.HasMaxLength(4096)
.HasColumnType("varchar(4096)");
b.Property<int>("Platform")
.HasColumnType("int");
b.Property<int>("Severity")
.HasColumnType("int");
b.Property<string>("Title")
.IsRequired()
.HasMaxLength(512)
.HasColumnType("varchar(512)");
b.Property<int?>("TraderId")
.HasColumnType("int");
b.Property<int>("Type")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("CreatedAt");
b.HasIndex("TraderId");
b.ToTable("Alerts");
});
modelBuilder.Entity("Predictalytics.Domain.Entities.Event", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<int>("Id"));
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<DateTime>("DbCreatedAt")
.HasColumnType("datetime(6)");
b.Property<string>("Description")
.HasMaxLength(4096)
.HasColumnType("varchar(4096)");
b.Property<DateTime?>("EndDate")
.HasColumnType("datetime(6)");
b.Property<string>("ImageUrl")
.HasMaxLength(1024)
.HasColumnType("varchar(1024)");
b.Property<bool>("IsActive")
.HasColumnType("tinyint(1)");
b.Property<bool>("IsClosed")
.HasColumnType("tinyint(1)");
b.Property<DateTime?>("LastUpdatedAt")
.HasColumnType("datetime(6)");
b.Property<int>("Platform")
.HasColumnType("int");
b.Property<long>("PlatformEventId")
.HasColumnType("bigint");
b.Property<string>("Slug")
.IsRequired()
.HasMaxLength(512)
.HasColumnType("varchar(512)");
b.Property<DateTime?>("StartDate")
.HasColumnType("datetime(6)");
b.Property<string>("Tags")
.IsRequired()
.HasMaxLength(1024)
.HasColumnType("varchar(1024)");
b.Property<string>("Title")
.IsRequired()
.HasMaxLength(1024)
.HasColumnType("varchar(1024)");
b.HasKey("Id");
b.HasIndex("Platform", "PlatformEventId")
.IsUnique();
b.ToTable("Events");
});
modelBuilder.Entity("Predictalytics.Domain.Entities.Market", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<int>("Id"));
b.Property<string>("Category")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("varchar(64)");
b.Property<string>("ConditionId")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("varchar(256)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<DateTime>("DbCreatedAt")
.HasColumnType("datetime(6)");
b.Property<string>("Description")
.HasMaxLength(4096)
.HasColumnType("varchar(4096)");
b.Property<DateTime?>("EndDate")
.HasColumnType("datetime(6)");
b.Property<int>("EventId")
.HasColumnType("int");
b.Property<string>("ImageUrl")
.HasMaxLength(1024)
.HasColumnType("varchar(1024)");
b.Property<bool>("IsResolved")
.HasColumnType("tinyint(1)");
b.Property<DateTime?>("LastTradesUpdatedAt")
.HasColumnType("datetime(6)");
b.Property<DateTime?>("LastUpdatedAt")
.HasColumnType("datetime(6)");
b.Property<decimal>("Liquidity")
.HasPrecision(18, 4)
.HasColumnType("decimal(18,4)");
b.Property<string>("MarketSlug")
.IsRequired()
.HasMaxLength(512)
.HasColumnType("varchar(512)");
b.Property<int>("Platform")
.HasColumnType("int");
b.Property<long>("PlatformMarketId")
.HasColumnType("bigint");
b.Property<string>("Question")
.IsRequired()
.HasMaxLength(1024)
.HasColumnType("varchar(1024)");
b.Property<string>("QuestionId")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("varchar(256)");
b.Property<string>("ResolutionOutcome")
.HasColumnType("longtext");
b.Property<DateTime?>("StartDate")
.HasColumnType("datetime(6)");
b.Property<string>("Subcategory")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("varchar(128)");
b.Property<decimal>("Volume")
.HasPrecision(18, 4)
.HasColumnType("decimal(18,4)");
b.Property<decimal>("Volume24h")
.HasPrecision(18, 4)
.HasColumnType("decimal(18,4)");
b.HasKey("Id");
b.HasIndex("EventId");
b.HasIndex("Platform", "PlatformMarketId")
.IsUnique();
b.ToTable("Markets");
});
modelBuilder.Entity("Predictalytics.Domain.Entities.MarketAnalytics", b =>
{
b.Property<int>("MarketId")
.HasColumnType("int");
b.Property<decimal>("AverageTradeSize")
.HasPrecision(18, 4)
.HasColumnType("decimal(18,4)");
b.Property<decimal>("BotActivityScore")
.HasPrecision(8, 4)
.HasColumnType("decimal(8,4)");
b.Property<DateTime>("LastCalculatedAt")
.HasColumnType("datetime(6)");
b.Property<int>("UniqueTradersCount")
.HasColumnType("int");
b.HasKey("MarketId");
b.ToTable("MarketAnalytics");
});
modelBuilder.Entity("Predictalytics.Domain.Entities.MarketOutcome", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<int>("Id"));
b.Property<decimal>("CurrentPrice")
.HasPrecision(18, 8)
.HasColumnType("decimal(18,8)");
b.Property<string>("Label")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("varchar(256)");
b.Property<int>("MarketId")
.HasColumnType("int");
b.Property<int>("OutcomeIndex")
.HasColumnType("int");
b.Property<string>("TokenId")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("varchar(256)");
b.HasKey("Id");
b.HasIndex("TokenId");
b.HasIndex("MarketId", "OutcomeIndex")
.IsUnique();
b.ToTable("MarketOutcomes");
});
modelBuilder.Entity("Predictalytics.Domain.Entities.MarketOutcomePriceSnapshot", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<int>("Id"));
b.Property<int>("MarketOutcomeId")
.HasColumnType("int");
b.Property<decimal>("Price")
.HasPrecision(10, 6)
.HasColumnType("decimal(10,6)");
b.Property<DateTime>("Timestamp")
.HasColumnType("datetime(6)");
b.HasKey("Id");
b.HasIndex("MarketOutcomeId", "Timestamp");
b.ToTable("MarketOutcomePriceSnapshots");
});
modelBuilder.Entity("Predictalytics.Domain.Entities.PlatformConfig", b =>
{
b.Property<int>("Id")
.HasColumnType("int");
b.Property<string>("BaseUrl")
.HasMaxLength(1024)
.HasColumnType("varchar(1024)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<string>("DisplayName")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("varchar(256)");
b.Property<bool>("IsActive")
.HasColumnType("tinyint(1)");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("varchar(128)");
b.Property<string>("SettingsJson")
.HasColumnType("longtext");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("datetime(6)");
b.HasKey("Id");
b.ToTable("PlatformConfigs");
});
modelBuilder.Entity("Predictalytics.Domain.Entities.Trade", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<long>("Id"));
b.Property<decimal>("Amount")
.HasPrecision(18, 4)
.HasColumnType("decimal(18,4)");
b.Property<string>("AssetId")
.IsRequired()
.HasMaxLength(80)
.HasColumnType("varchar(80)");
b.Property<int?>("DbMarketId")
.HasColumnType("int");
b.Property<DateTime>("ExecutedAt")
.HasColumnType("datetime(6)");
b.Property<bool>("IsContextEnriched")
.HasColumnType("tinyint(1)");
b.Property<string>("MarketId")
.IsRequired()
.HasMaxLength(66)
.HasColumnType("varchar(66)");
b.Property<int?>("MarketOutcomeId")
.HasColumnType("int");
b.Property<string>("Outcome")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("varchar(128)");
b.Property<int>("Platform")
.HasColumnType("int");
b.Property<string>("PlatformTradeId")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("varchar(256)");
b.Property<decimal?>("PostTradePrice1m")
.HasColumnType("decimal(18,4)");
b.Property<decimal?>("PreTradePrice1m")
.HasColumnType("decimal(18,4)");
b.Property<decimal>("Price")
.HasPrecision(18, 6)
.HasColumnType("decimal(18,6)");
b.Property<int>("Side")
.HasColumnType("int");
b.Property<decimal>("Size")
.HasPrecision(14, 6)
.HasColumnType("decimal(14,6)");
b.Property<int>("TraderId")
.HasColumnType("int");
b.Property<string>("TransactionHash")
.HasMaxLength(66)
.HasColumnType("varchar(66)");
b.HasKey("Id");
b.HasIndex("AssetId");
b.HasIndex("DbMarketId");
b.HasIndex("ExecutedAt");
b.HasIndex("MarketOutcomeId");
b.HasIndex("TraderId");
b.HasIndex("Platform", "PlatformTradeId")
.IsUnique();
b.ToTable("Trades");
});
modelBuilder.Entity("Predictalytics.Domain.Entities.Trader", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<int>("Id"));
b.Property<string>("AiStrategySummary")
.HasColumnType("longtext");
b.Property<DateTime?>("AiStrategyUpdatedAt")
.HasColumnType("datetime(6)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<string>("DisplayName")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("varchar(256)");
b.Property<bool>("IsAutoDiscovered")
.HasColumnType("tinyint(1)");
b.Property<bool>("IsInitialImportComplete")
.HasColumnType("tinyint(1)");
b.Property<bool>("IsSuspectedBot")
.HasColumnType("tinyint(1)");
b.Property<DateTime?>("LastApiErrorAt")
.HasColumnType("datetime(6)");
b.Property<DateTime?>("LastPolledAt")
.HasColumnType("datetime(6)");
b.Property<DateTime?>("LastTradesUpdatedAt")
.HasColumnType("datetime(6)");
b.Property<int?>("ManualPriorityOverride")
.HasColumnType("int");
b.Property<string>("Notes")
.HasColumnType("longtext");
b.Property<int>("Platform")
.HasColumnType("int");
b.Property<string>("PlatformUserId")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("varchar(128)");
b.Property<int>("Strategy")
.HasColumnType("int");
b.Property<int>("Tier")
.HasColumnType("int");
b.Property<decimal>("TotalPnl")
.HasPrecision(18, 4)
.HasColumnType("decimal(18,4)");
b.Property<int>("TotalTrades")
.HasColumnType("int");
b.Property<decimal>("WinRate")
.HasPrecision(8, 4)
.HasColumnType("decimal(8,4)");
b.HasKey("Id");
b.HasIndex("Platform", "PlatformUserId")
.IsUnique();
b.ToTable("Traders");
});
modelBuilder.Entity("Predictalytics.Domain.Entities.TraderAnalytics", b =>
{
b.Property<int>("TraderId")
.HasColumnType("int");
b.Property<DateTime>("LastCalculatedAt")
.HasColumnType("datetime(6)");
b.Property<decimal>("OverallPnL")
.HasPrecision(18, 4)
.HasColumnType("decimal(18,4)");
b.Property<decimal>("OverallWinRate")
.HasPrecision(8, 4)
.HasColumnType("decimal(8,4)");
b.Property<decimal>("PnL24h")
.HasPrecision(18, 4)
.HasColumnType("decimal(18,4)");
b.Property<decimal>("PnL30d")
.HasPrecision(18, 4)
.HasColumnType("decimal(18,4)");
b.Property<decimal>("PnL7d")
.HasPrecision(18, 4)
.HasColumnType("decimal(18,4)");
b.Property<decimal>("WinRate24h")
.HasPrecision(8, 4)
.HasColumnType("decimal(8,4)");
b.Property<decimal>("WinRate30d")
.HasPrecision(8, 4)
.HasColumnType("decimal(8,4)");
b.Property<decimal>("WinRate7d")
.HasPrecision(8, 4)
.HasColumnType("decimal(8,4)");
b.HasKey("TraderId");
b.ToTable("TraderAnalytics");
});
modelBuilder.Entity("Predictalytics.Domain.Entities.TraderCategoryPerformance", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<int>("Id"));
b.Property<string>("Category")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("varchar(64)");
b.Property<decimal>("TotalPnL")
.HasPrecision(18, 4)
.HasColumnType("decimal(18,4)");
b.Property<int>("TotalTrades")
.HasColumnType("int");
b.Property<decimal>("TotalVolume")
.HasPrecision(18, 4)
.HasColumnType("decimal(18,4)");
b.Property<int>("TraderId")
.HasColumnType("int");
b.Property<int>("WinningTrades")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("TraderId", "Category")
.IsUnique();
b.ToTable("TraderCategoryPerformances");
});
modelBuilder.Entity("Predictalytics.Domain.Entities.TraderPosition", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<int>("Id"));
b.Property<decimal>("AvgCost")
.HasPrecision(10, 6)
.HasColumnType("decimal(10,6)");
b.Property<DateTime>("LastUpdatedAt")
.HasColumnType("datetime(6)");
b.Property<int>("MarketOutcomeId")
.HasColumnType("int");
b.Property<decimal>("RealizedPnl")
.HasPrecision(18, 4)
.HasColumnType("decimal(18,4)");
b.Property<decimal>("SharesHeld")
.HasPrecision(14, 6)
.HasColumnType("decimal(14,6)");
b.Property<int>("TraderId")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("MarketOutcomeId");
b.HasIndex("TraderId", "MarketOutcomeId")
.IsUnique();
b.ToTable("TraderPositions");
});
modelBuilder.Entity("Predictalytics.Domain.Entities.TraderScore", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<int>("Id"));
b.Property<decimal>("ActivityScore")
.HasPrecision(8, 4)
.HasColumnType("decimal(8,4)");
b.Property<DateTime>("CalculatedAt")
.HasColumnType("datetime(6)");
b.Property<decimal>("CombinedScore")
.HasPrecision(8, 4)
.HasColumnType("decimal(8,4)");
b.Property<decimal>("CopytradingScore")
.HasPrecision(8, 4)
.HasColumnType("decimal(8,4)");
b.Property<decimal>("QualityScore")
.HasPrecision(8, 4)
.HasColumnType("decimal(8,4)");
b.Property<int>("Rank")
.HasColumnType("int");
b.Property<decimal>("TimingScore")
.HasPrecision(8, 4)
.HasColumnType("decimal(8,4)");
b.Property<int>("TraderId")
.HasColumnType("int");
b.Property<decimal>("VolumeScore")
.HasPrecision(8, 4)
.HasColumnType("decimal(8,4)");
b.HasKey("Id");
b.HasIndex("TraderId")
.IsUnique();
b.ToTable("TraderScores");
});
modelBuilder.Entity("Predictalytics.Domain.Entities.WatchlistEntry", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<int>("Id"));
b.Property<DateTime>("AddedAt")
.HasColumnType("datetime(6)");
b.Property<bool>("AlertsEnabled")
.HasColumnType("tinyint(1)");
b.Property<string>("Label")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("varchar(256)");
b.Property<string>("Notes")
.HasColumnType("longtext");
b.Property<int>("TraderId")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("TraderId")
.IsUnique();
b.ToTable("WatchlistEntries");
});
modelBuilder.Entity("Predictalytics.Domain.Entities.Alert", b =>
{
b.HasOne("Predictalytics.Domain.Entities.Trader", "Trader")
.WithMany()
.HasForeignKey("TraderId")
.OnDelete(DeleteBehavior.SetNull);
b.Navigation("Trader");
});
modelBuilder.Entity("Predictalytics.Domain.Entities.Market", b =>
{
b.HasOne("Predictalytics.Domain.Entities.Event", "Event")
.WithMany("Markets")
.HasForeignKey("EventId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Event");
});
modelBuilder.Entity("Predictalytics.Domain.Entities.MarketAnalytics", b =>
{
b.HasOne("Predictalytics.Domain.Entities.Market", "Market")
.WithOne("Analytics")
.HasForeignKey("Predictalytics.Domain.Entities.MarketAnalytics", "MarketId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Market");
});
modelBuilder.Entity("Predictalytics.Domain.Entities.MarketOutcome", b =>
{
b.HasOne("Predictalytics.Domain.Entities.Market", "Market")
.WithMany("Outcomes")
.HasForeignKey("MarketId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Market");
});
modelBuilder.Entity("Predictalytics.Domain.Entities.MarketOutcomePriceSnapshot", b =>
{
b.HasOne("Predictalytics.Domain.Entities.MarketOutcome", "MarketOutcome")
.WithMany()
.HasForeignKey("MarketOutcomeId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("MarketOutcome");
});
modelBuilder.Entity("Predictalytics.Domain.Entities.Trade", b =>
{
b.HasOne("Predictalytics.Domain.Entities.Market", "DbMarket")
.WithMany()
.HasForeignKey("DbMarketId")
.OnDelete(DeleteBehavior.SetNull);
b.HasOne("Predictalytics.Domain.Entities.MarketOutcome", "MarketOutcome")
.WithMany()
.HasForeignKey("MarketOutcomeId")
.OnDelete(DeleteBehavior.SetNull);
b.HasOne("Predictalytics.Domain.Entities.Trader", "Trader")
.WithMany("Trades")
.HasForeignKey("TraderId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("DbMarket");
b.Navigation("MarketOutcome");
b.Navigation("Trader");
});
modelBuilder.Entity("Predictalytics.Domain.Entities.TraderAnalytics", b =>
{
b.HasOne("Predictalytics.Domain.Entities.Trader", "Trader")
.WithOne("Analytics")
.HasForeignKey("Predictalytics.Domain.Entities.TraderAnalytics", "TraderId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Trader");
});
modelBuilder.Entity("Predictalytics.Domain.Entities.TraderCategoryPerformance", b =>
{
b.HasOne("Predictalytics.Domain.Entities.Trader", "Trader")
.WithMany()
.HasForeignKey("TraderId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Trader");
});
modelBuilder.Entity("Predictalytics.Domain.Entities.TraderPosition", b =>
{
b.HasOne("Predictalytics.Domain.Entities.MarketOutcome", "MarketOutcome")
.WithMany()
.HasForeignKey("MarketOutcomeId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Predictalytics.Domain.Entities.Trader", "Trader")
.WithMany("Positions")
.HasForeignKey("TraderId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("MarketOutcome");
b.Navigation("Trader");
});
modelBuilder.Entity("Predictalytics.Domain.Entities.TraderScore", b =>
{
b.HasOne("Predictalytics.Domain.Entities.Trader", "Trader")
.WithOne("CurrentScore")
.HasForeignKey("Predictalytics.Domain.Entities.TraderScore", "TraderId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Trader");
});
modelBuilder.Entity("Predictalytics.Domain.Entities.WatchlistEntry", b =>
{
b.HasOne("Predictalytics.Domain.Entities.Trader", "Trader")
.WithMany("WatchlistEntries")
.HasForeignKey("TraderId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Trader");
});
modelBuilder.Entity("Predictalytics.Domain.Entities.Event", b =>
{
b.Navigation("Markets");
});
modelBuilder.Entity("Predictalytics.Domain.Entities.Market", b =>
{
b.Navigation("Analytics");
b.Navigation("Outcomes");
});
modelBuilder.Entity("Predictalytics.Domain.Entities.Trader", b =>
{
b.Navigation("Analytics");
b.Navigation("CurrentScore");
b.Navigation("Positions");
b.Navigation("Trades");
b.Navigation("WatchlistEntries");
});
#pragma warning restore 612, 618
}
}
}
@@ -0,0 +1,78 @@
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Predictalytics.Infrastructure.Migrations
{
/// <inheritdoc />
public partial class AddTraderCategoryPerformance : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<decimal>(
name: "Price",
table: "Trades",
type: "decimal(18,6)",
precision: 18,
scale: 6,
nullable: false,
oldClrType: typeof(decimal),
oldType: "decimal(10,6)",
oldPrecision: 10,
oldScale: 6);
migrationBuilder.CreateTable(
name: "TraderCategoryPerformances",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
TraderId = table.Column<int>(type: "int", nullable: false),
Category = table.Column<string>(type: "varchar(64)", maxLength: 64, nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
TotalVolume = table.Column<decimal>(type: "decimal(18,4)", precision: 18, scale: 4, nullable: false),
TotalPnL = table.Column<decimal>(type: "decimal(18,4)", precision: 18, scale: 4, nullable: false),
TotalTrades = table.Column<int>(type: "int", nullable: false),
WinningTrades = table.Column<int>(type: "int", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_TraderCategoryPerformances", x => x.Id);
table.ForeignKey(
name: "FK_TraderCategoryPerformances_Traders_TraderId",
column: x => x.TraderId,
principalTable: "Traders",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
})
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateIndex(
name: "IX_TraderCategoryPerformances_TraderId_Category",
table: "TraderCategoryPerformances",
columns: new[] { "TraderId", "Category" },
unique: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "TraderCategoryPerformances");
migrationBuilder.AlterColumn<decimal>(
name: "Price",
table: "Trades",
type: "decimal(10,6)",
precision: 10,
scale: 6,
nullable: false,
oldClrType: typeof(decimal),
oldType: "decimal(18,6)",
oldPrecision: 18,
oldScale: 6);
}
}
}
@@ -143,8 +143,8 @@ namespace Predictalytics.Infrastructure.Migrations
b.Property<string>("Category") b.Property<string>("Category")
.IsRequired() .IsRequired()
.HasMaxLength(128) .HasMaxLength(64)
.HasColumnType("varchar(128)"); .HasColumnType("varchar(64)");
b.Property<string>("ConditionId") b.Property<string>("ConditionId")
.IsRequired() .IsRequired()
@@ -211,6 +211,11 @@ namespace Predictalytics.Infrastructure.Migrations
b.Property<DateTime?>("StartDate") b.Property<DateTime?>("StartDate")
.HasColumnType("datetime(6)"); .HasColumnType("datetime(6)");
b.Property<string>("Subcategory")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("varchar(128)");
b.Property<decimal>("Volume") b.Property<decimal>("Volume")
.HasPrecision(18, 4) .HasPrecision(18, 4)
.HasColumnType("decimal(18,4)"); .HasColumnType("decimal(18,4)");
@@ -406,8 +411,8 @@ namespace Predictalytics.Infrastructure.Migrations
.HasColumnType("decimal(18,4)"); .HasColumnType("decimal(18,4)");
b.Property<decimal>("Price") b.Property<decimal>("Price")
.HasPrecision(10, 6) .HasPrecision(18, 6)
.HasColumnType("decimal(10,6)"); .HasColumnType("decimal(18,6)");
b.Property<int>("Side") b.Property<int>("Side")
.HasColumnType("int"); .HasColumnType("int");
@@ -565,6 +570,44 @@ namespace Predictalytics.Infrastructure.Migrations
b.ToTable("TraderAnalytics"); b.ToTable("TraderAnalytics");
}); });
modelBuilder.Entity("Predictalytics.Domain.Entities.TraderCategoryPerformance", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<int>("Id"));
b.Property<string>("Category")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("varchar(64)");
b.Property<decimal>("TotalPnL")
.HasPrecision(18, 4)
.HasColumnType("decimal(18,4)");
b.Property<int>("TotalTrades")
.HasColumnType("int");
b.Property<decimal>("TotalVolume")
.HasPrecision(18, 4)
.HasColumnType("decimal(18,4)");
b.Property<int>("TraderId")
.HasColumnType("int");
b.Property<int>("WinningTrades")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("TraderId", "Category")
.IsUnique();
b.ToTable("TraderCategoryPerformances");
});
modelBuilder.Entity("Predictalytics.Domain.Entities.TraderPosition", b => modelBuilder.Entity("Predictalytics.Domain.Entities.TraderPosition", b =>
{ {
b.Property<int>("Id") b.Property<int>("Id")
@@ -776,6 +819,17 @@ namespace Predictalytics.Infrastructure.Migrations
b.Navigation("Trader"); b.Navigation("Trader");
}); });
modelBuilder.Entity("Predictalytics.Domain.Entities.TraderCategoryPerformance", b =>
{
b.HasOne("Predictalytics.Domain.Entities.Trader", "Trader")
.WithMany()
.HasForeignKey("TraderId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Trader");
});
modelBuilder.Entity("Predictalytics.Domain.Entities.TraderPosition", b => modelBuilder.Entity("Predictalytics.Domain.Entities.TraderPosition", b =>
{ {
b.HasOne("Predictalytics.Domain.Entities.MarketOutcome", "MarketOutcome") b.HasOne("Predictalytics.Domain.Entities.MarketOutcome", "MarketOutcome")
@@ -246,6 +246,9 @@ public class LimitlessProvider : IPlatformProvider
private Market MapLimitlessMarket(LimitlessMarketResponse raw) private Market MapLimitlessMarket(LimitlessMarketResponse raw)
{ {
var conditionId = raw.Address ?? raw.Slug ?? Guid.NewGuid().ToString(); var conditionId = raw.Address ?? raw.Slug ?? Guid.NewGuid().ToString();
var firstCat = raw.Categories?.FirstOrDefault() ?? "";
var catMap = Predictalytics.Infrastructure.Helpers.MarketCategoryMapper.Map(firstCat, string.Join(",", raw.Categories ?? []));
var market = new Market var market = new Market
{ {
Platform = PlatformType.Limitless, Platform = PlatformType.Limitless,
@@ -254,7 +257,8 @@ public class LimitlessProvider : IPlatformProvider
MarketSlug = raw.Slug ?? "", MarketSlug = raw.Slug ?? "",
Question = raw.Title ?? "", Question = raw.Title ?? "",
Description = raw.Description ?? "", Description = raw.Description ?? "",
Category = raw.Categories?.FirstOrDefault() ?? "", Category = catMap.Category,
Subcategory = catMap.Subcategory,
ImageUrl = raw.ImageUrl ?? "", ImageUrl = raw.ImageUrl ?? "",
Volume = decimal.TryParse(raw.VolumeFormatted?.Replace(" USDC", ""), out var vol) ? vol : 0, Volume = decimal.TryParse(raw.VolumeFormatted?.Replace(" USDC", ""), out var vol) ? vol : 0,
Liquidity = (decimal)(raw.Liquidity ?? 0), Liquidity = (decimal)(raw.Liquidity ?? 0),
@@ -279,6 +279,8 @@ public class PolymarketProvider : IPlatformProvider
{ {
long.TryParse(raw.Id, out var marketNumericId); long.TryParse(raw.Id, out var marketNumericId);
var catMap = Predictalytics.Infrastructure.Helpers.MarketCategoryMapper.Map(raw.Category ?? "", parentTags);
var market = new Market var market = new Market
{ {
Platform = PlatformType.Polymarket, Platform = PlatformType.Polymarket,
@@ -289,7 +291,8 @@ public class PolymarketProvider : IPlatformProvider
Description = raw.Description, Description = raw.Description,
ImageUrl = raw.Image, ImageUrl = raw.Image,
Question = raw.Question, Question = raw.Question,
Category = string.IsNullOrWhiteSpace(raw.Category) ? parentTags : raw.Category, Category = catMap.Category,
Subcategory = catMap.Subcategory,
Volume = (decimal)raw.Volume, Volume = (decimal)raw.Volume,
Volume24h = (decimal)raw.Volume24hr, Volume24h = (decimal)raw.Volume24hr,
Liquidity = (decimal)raw.Liquidity, Liquidity = (decimal)raw.Liquidity,
@@ -225,6 +225,31 @@ public class PositionPnLEngine : IPositionPnLEngine
trader.TotalPnl = overallPnl; trader.TotalPnl = overallPnl;
trader.WinRate = winRateOverall; trader.WinRate = winRateOverall;
// Calculate Category Performance
var existingCatPerf = await _db.TraderCategoryPerformances
.Where(tcp => tcp.TraderId == traderId)
.ToDictionaryAsync(tcp => tcp.Category, ct);
var newCatPerf = CalculateCategoryPerformances(trades, tempPositions);
foreach (var kvp in newCatPerf)
{
if (existingCatPerf.TryGetValue(kvp.Key, out var existing))
{
existing.TotalVolume = kvp.Value.TotalVolume;
existing.TotalPnL = kvp.Value.TotalPnL;
existing.TotalTrades = kvp.Value.TotalTrades;
existing.WinningTrades = kvp.Value.WinningTrades;
_db.TraderCategoryPerformances.Update(existing);
}
else
{
var newEntity = kvp.Value;
newEntity.TraderId = traderId;
_db.TraderCategoryPerformances.Add(newEntity);
}
}
// Save changes to database // Save changes to database
await _db.SaveChangesAsync(ct); await _db.SaveChangesAsync(ct);
@@ -322,4 +347,64 @@ public class PositionPnLEngine : IPositionPnLEngine
return false; return false;
} }
private static Dictionary<MarketCategory, TraderCategoryPerformance> CalculateCategoryPerformances(
List<Trade> trades,
Dictionary<int, TraderPosition> finalPositions)
{
var result = new Dictionary<MarketCategory, TraderCategoryPerformance>();
var tradesByMarket = trades
.Where(t => t.MarketOutcome?.Market != null)
.GroupBy(t => t.MarketOutcome!.Market!);
foreach (var marketGroup in tradesByMarket)
{
var market = marketGroup.Key;
var category = market.Category;
if (!result.TryGetValue(category, out var perf))
{
perf = new TraderCategoryPerformance { Category = category };
result[category] = perf;
}
// Add volume
perf.TotalVolume += marketGroup.Sum(t => t.Amount);
// Determine if market is closed for this trader
var outcomeIds = marketGroup
.Where(t => t.MarketOutcomeId.HasValue)
.Select(t => t.MarketOutcomeId!.Value)
.Distinct()
.ToList();
var isClosed = outcomeIds.All(oid => !finalPositions.TryGetValue(oid, out var pos) || pos.SharesHeld == 0);
if (!isClosed && market.IsResolved)
{
isClosed = true;
}
if (isClosed)
{
decimal marketPnl = 0;
foreach (var oid in outcomeIds)
{
if (finalPositions.TryGetValue(oid, out var pos))
{
marketPnl += pos.RealizedPnl;
}
}
perf.TotalPnL += marketPnl;
perf.TotalTrades += 1;
if (marketPnl > 0)
{
perf.WinningTrades += 1;
}
}
}
return result;
}
} }
+2 -2
View File
@@ -175,7 +175,7 @@ public partial class MainForm : Form
} }
} }
private async void syncMarketsaToolStripMenuItem_Click(object sender, EventArgs e) private async void syncMarketsaToolStripMenuItem_Click(object? sender, EventArgs e)
{ {
if (_workerRunning) if (_workerRunning)
{ {
@@ -207,7 +207,7 @@ public partial class MainForm : Form
} }
} }
private async void btn_dbUpdate_Click(object sender, EventArgs e) private async void btn_dbUpdate_Click(object? sender, EventArgs e)
{ {
if (_workerRunning) if (_workerRunning)
{ {