diff --git a/src/Predictalytics.Api/Endpoints/MarketEndpoints.cs b/src/Predictalytics.Api/Endpoints/MarketEndpoints.cs index 9b38774..27cfc84 100644 --- a/src/Predictalytics.Api/Endpoints/MarketEndpoints.cs +++ b/src/Predictalytics.Api/Endpoints/MarketEndpoints.cs @@ -8,9 +8,9 @@ public static class MarketEndpoints { var group = app.MapGroup("/api/markets").WithTags("Markets"); - group.MapGet("/", async (IAnalyticsService svc, int? skip, int? take, string? platform, CancellationToken ct) => + group.MapGet("/", async (IAnalyticsService svc, int? skip, int? take, string? platform, string? category, string? query, CancellationToken ct) => { - var result = await svc.GetMarketsAsync(skip ?? 0, take ?? 50, platform, ct); + var result = await svc.GetMarketsAsync(skip ?? 0, take ?? 50, platform, category, query, ct); return Results.Ok(result); }); diff --git a/src/Predictalytics.Api/Endpoints/TraderEndpoints.cs b/src/Predictalytics.Api/Endpoints/TraderEndpoints.cs index 6dbc75c..0351840 100644 --- a/src/Predictalytics.Api/Endpoints/TraderEndpoints.cs +++ b/src/Predictalytics.Api/Endpoints/TraderEndpoints.cs @@ -24,6 +24,12 @@ public static class TraderEndpoints return dd is not null ? Results.Ok(dd) : Results.NotFound(); }); + group.MapGet("/{id:int}/positions", async (int id, IAnalyticsService svc, CancellationToken ct) => + { + var positions = await svc.GetTraderPositionsAsync(id, ct); + return Results.Ok(positions); + }); + group.MapPost("/{id:int}/priority", async (int id, int? score, IScoringService svc, CancellationToken ct) => { await svc.SetManualOverrideAsync(id, score, ct); diff --git a/src/Predictalytics.Api/wwwroot/index.html b/src/Predictalytics.Api/wwwroot/index.html index 015d296..877e2b5 100644 --- a/src/Predictalytics.Api/wwwroot/index.html +++ b/src/Predictalytics.Api/wwwroot/index.html @@ -111,7 +111,7 @@

Top Traders

- +
#TraderPlatformScoreWin RatePnLTierTrades
#TraderPlatformScoreWin RatePnLTrades (30d|All)
@@ -177,7 +177,7 @@ Copyability ↕ Win Rate ↕ PnL ↕ - Tier + Trades (30d|All) ↕ Strategy Actions @@ -188,7 +188,26 @@ -

Markets

PlatformQuestionVolumeLiquidityEnd DateStatus
+
+
+
+

Markets

+ + +
+
+
PlatformQuestionVolumeLiquidityEnd DateStatus
+

Alerts

@@ -264,8 +283,11 @@ - + @@ -302,14 +324,15 @@
Win Rate
---
+
Win Rate (30d)
---
Total PnL
---
+
PnL (30d)
---
Total Trades
---
+
Est. Bankroll
---
Quality Edge
---
Copyability
---
-
Combined Score
---
-
@@ -351,8 +374,8 @@

Active Positions

- - + +
MarketOutcomeSharesAvg PriceEst. PnL
Coming soon
Market / OutcomeSharesAvg PriceCurrent PriceRealized PnLUnrealized PnL
Loading...
@@ -394,6 +417,9 @@
Volume
β€”
Liquidity
β€”
Status
β€”
+
Bot Activity
β€”
+
Unique Traders
β€”
+
Avg Trade Size
β€”

Outcomes

diff --git a/src/Predictalytics.Api/wwwroot/js/app.js b/src/Predictalytics.Api/wwwroot/js/app.js index b0abbb2..f800681 100644 --- a/src/Predictalytics.Api/wwwroot/js/app.js +++ b/src/Predictalytics.Api/wwwroot/js/app.js @@ -221,8 +221,7 @@ async function loadDashboard() { ${Number(t.combinedScore).toFixed(1)} ${fmt.pct(t.winRate)} ${fmt.pnl(t.totalPnl)} - ${fmt.tier(t.tier)} - ${fmt.num(t.totalTrades)} + ${t.trades30d} | ${t.totalTrades} `).join(''); @@ -319,6 +318,7 @@ async function loadTraders() { else if (currentSort === 'copyability') { valA = a.copytradingCopyabilityScore || 0; valB = b.copytradingCopyabilityScore || 0; } else if (currentSort === 'winrate') { valA = a.winRate; valB = b.winRate; } else if (currentSort === 'pnl') { valA = a.totalPnl; valB = b.totalPnl; } + else if (currentSort === 'trades') { valA = a.trades30d; valB = b.trades30d; } else if (currentSort === 'name') { return a.displayName.localeCompare(b.displayName) * sortDirection; } else if (currentSort === 'platform') { return a.platform.localeCompare(b.platform) * sortDirection; } else { valA = a.combinedScore; valB = b.combinedScore; } @@ -336,7 +336,7 @@ async function loadTraders() { ${Number(t.copytradingCopyabilityScore || 0).toFixed(1)} ${fmt.pct(t.winRate)} ${fmt.pnl(t.totalPnl)} - ${fmt.tier(t.tier)} + ${t.trades30d} | ${t.totalTrades} ${t.strategy}
@@ -370,6 +370,17 @@ async function loadAlerts() { async function loadMarkets() { let url = '/api/markets?skip=0&take=100'; if (currentPlatform !== 'All') url += `&platform=${currentPlatform}`; + + const catSelect = document.getElementById('marketsCategory'); + if (catSelect && catSelect.value !== 'All') { + url += `&category=${catSelect.value}`; + } + + const searchInput = document.getElementById('marketsSearchInput'); + if (searchInput && searchInput.value) { + url += `&query=${encodeURIComponent(searchInput.value)}`; + } + let data = await api(url); const tbody = document.getElementById('allMarketsBody'); if (!data || !data.length) { tbody.innerHTML = '

No markets found.

'; return; } @@ -392,6 +403,7 @@ async function loadMarkets() { } async function viewTrader(id) { + window.currentTraderId = id; navigateTo('trader-detail'); const t = await api(`/api/traders/${id}`); if (!t) return; @@ -405,27 +417,32 @@ async function viewTrader(id) { document.getElementById('td-tier').innerHTML = fmt.tier(t.tier); document.getElementById('td-strategy').textContent = t.strategy; document.getElementById('td-winrate').innerHTML = fmt.pct(t.winRate); + document.getElementById('td-winrate30d').innerHTML = fmt.pct(t.winRate30d); document.getElementById('td-pnl').innerHTML = fmt.pnl(t.totalPnl); + document.getElementById('td-pnl30d').innerHTML = fmt.pnl(t.pnL30d); document.getElementById('td-trades').textContent = fmt.num(t.totalTrades); + document.getElementById('td-bankroll').textContent = fmt.usd(t.estimatedBankroll); document.getElementById('td-score').textContent = Number(t.combinedScore).toFixed(1); document.getElementById('td-quality-score').textContent = Number(t.copytradingQualityScore || 0).toFixed(1); document.getElementById('td-copyability-score').textContent = Number(t.copytradingCopyabilityScore || 0).toFixed(1); document.getElementById('td-ai-summary').textContent = t.aiStrategySummary || 'Not analyzed yet.'; - const refreshBtn = document.getElementById('btn-refresh-trader'); - refreshBtn.onclick = () => manualUpdateTrader(id); + const syncBtn = document.getElementById('btn-sync-trader'); + if (syncBtn) { + syncBtn.onclick = () => manualUpdateTrader(id); + } - const forceBtn = document.getElementById('btn-force-analyze'); - if(forceBtn) { - forceBtn.onclick = async () => { - forceBtn.disabled = true; - forceBtn.textContent = '...'; + const analyzeBtn = document.getElementById('btn-analyze-trader'); + if (analyzeBtn) { + analyzeBtn.onclick = async () => { + analyzeBtn.disabled = true; + analyzeBtn.textContent = '...'; try { await api(`/api/traders/${id}/force-analyze`, { method: 'POST' }); alert('Deep Analysis queued! Please wait a moment and then refresh.'); } finally { - forceBtn.disabled = false; - forceBtn.textContent = 'Recalculate'; + analyzeBtn.disabled = false; + analyzeBtn.textContent = 'βš™ Analyze'; } }; } @@ -479,6 +496,37 @@ async function viewTrader(id) { ${fmt.usd(tr.amount)} `).join(''); + + loadTraderPositions(id); +} + +async function loadTraderPositions(id) { + const tbody = document.getElementById('td-positionsBody'); + tbody.innerHTML = 'Loading...'; + + try { + const data = await api(`/api/traders/${id}/positions`); + if (!data || !data.length) { + tbody.innerHTML = '

No active positions.

'; + return; + } + + tbody.innerHTML = data.map(p => ` + + +
${p.marketName || p.marketId}
+
${p.category || 'Other'} · ${p.outcomeToken || 'Unknown'}
+ + ${fmt.num(p.sharesHeld)} + ${Number(p.avgCost).toFixed(2)} + ${Number(p.currentPrice).toFixed(2)} + ${fmt.pnl(p.realizedPnl)} + ${fmt.pnl(p.unrealizedPnl)} + + `).join(''); + } catch (e) { + tbody.innerHTML = 'Error loading positions.'; + } } function switchTraderTab(tabId) { @@ -505,6 +553,9 @@ async function viewMarket(id) { document.getElementById('md-volume').textContent = fmt.usd(m.volume); document.getElementById('md-liquidity').textContent = fmt.usd(m.liquidity); document.getElementById('md-status').textContent = m.isResolved ? 'Resolved' : 'Active'; + document.getElementById('md-bot-activity').textContent = m.botActivityScore ? Number(m.botActivityScore).toFixed(1) : '0'; + document.getElementById('md-unique-traders').textContent = fmt.num(m.uniqueTradersCount); + document.getElementById('md-avg-trade-size').textContent = fmt.usd(m.averageTradeSize); const imgContainer = document.getElementById('md-image'); if (m.imageUrl) imgContainer.innerHTML = `Market`; diff --git a/src/Predictalytics.Application/DTOs/MarketDetailDto.cs b/src/Predictalytics.Application/DTOs/MarketDetailDto.cs index 4e918ac..458c082 100644 --- a/src/Predictalytics.Application/DTOs/MarketDetailDto.cs +++ b/src/Predictalytics.Application/DTOs/MarketDetailDto.cs @@ -15,6 +15,11 @@ public class MarketDetailDto public bool IsResolved { get; set; } public string? ResolutionOutcome { get; set; } public string? ImageUrl { get; set; } + + public decimal BotActivityScore { get; set; } + public int UniqueTradersCount { get; set; } + public decimal AverageTradeSize { get; set; } + public IReadOnlyList Outcomes { get; set; } = new List(); public IReadOnlyList RecentTrades { get; set; } = new List(); } diff --git a/src/Predictalytics.Application/DTOs/MarketDto.cs b/src/Predictalytics.Application/DTOs/MarketDto.cs index c9d711c..2b948e4 100644 --- a/src/Predictalytics.Application/DTOs/MarketDto.cs +++ b/src/Predictalytics.Application/DTOs/MarketDto.cs @@ -9,4 +9,5 @@ public class MarketDto public double Liquidity { get; set; } public DateTime? EndDate { get; set; } public bool IsResolved { get; set; } + } diff --git a/src/Predictalytics.Application/DTOs/TraderDto.cs b/src/Predictalytics.Application/DTOs/TraderDto.cs index 9e2a044..5440b06 100644 --- a/src/Predictalytics.Application/DTOs/TraderDto.cs +++ b/src/Predictalytics.Application/DTOs/TraderDto.cs @@ -16,6 +16,10 @@ public record TraderDto( decimal WinRate, decimal TotalPnl, int TotalTrades, + int Trades30d, + decimal PnL30d, + decimal WinRate30d, + decimal EstimatedBankroll, bool IsOnWatchlist, bool IsSuspectedBot, DateTime? LastPolledAt @@ -34,6 +38,10 @@ public record TraderDetailDto( decimal WinRate, decimal TotalPnl, int TotalTrades, + int Trades30d, + decimal PnL30d, + decimal WinRate30d, + decimal EstimatedBankroll, decimal ActivityScore, decimal QualityScore, decimal VolumeScore, @@ -59,3 +67,16 @@ public record TraderCategoryPerformanceDto( int WinningTrades, decimal WinRate ); + +public record TraderPositionDto( + string MarketId, + string? MarketName, + string? Category, + string? OutcomeToken, + decimal SharesHeld, + decimal AvgCost, + decimal RealizedPnl, + decimal UnrealizedPnl, + decimal CurrentPrice, + DateTime? LastTradeExecutedAt +); diff --git a/src/Predictalytics.Application/Interfaces/IAnalyticsService.cs b/src/Predictalytics.Application/Interfaces/IAnalyticsService.cs index 284ab3b..f9295aa 100644 --- a/src/Predictalytics.Application/Interfaces/IAnalyticsService.cs +++ b/src/Predictalytics.Application/Interfaces/IAnalyticsService.cs @@ -17,8 +17,11 @@ public interface IAnalyticsService /// Get a trader's details. Task GetTraderDetailAsync(int traderId, CancellationToken ct = default); + /// Get a trader's positions. + Task> GetTraderPositionsAsync(int traderId, CancellationToken ct = default); + /// Get list of markets. - Task> GetMarketsAsync(int skip = 0, int take = 50, string? platform = null, CancellationToken ct = default); + Task> GetMarketsAsync(int skip = 0, int take = 50, string? platform = null, string? category = null, string? query = null, CancellationToken ct = default); /// Get market details. Task GetMarketDetailAsync(int marketId, CancellationToken ct = default); diff --git a/src/Predictalytics.Application/Services/AnalyticsService.cs b/src/Predictalytics.Application/Services/AnalyticsService.cs index 7a26dde..dcf7379 100644 --- a/src/Predictalytics.Application/Services/AnalyticsService.cs +++ b/src/Predictalytics.Application/Services/AnalyticsService.cs @@ -145,6 +145,23 @@ public class AnalyticsService : IAnalyticsService analysis.BotIndicators, analysis.Summary, tradeDtos); } + public async Task> GetTraderPositionsAsync(int traderId, CancellationToken ct = default) + { + var positions = await _traderRepo.GetPositionsAsync(traderId, ct); + return positions.Select(p => new TraderPositionDto( + p.MarketOutcome?.Market?.ConditionId ?? p.MarketOutcomeId.ToString(), + p.MarketOutcome?.Market?.Question, + p.MarketOutcome?.Market?.Category.ToString(), + p.MarketOutcome?.TokenId, + p.SharesHeld, + p.AvgCost, + p.RealizedPnl, + p.SharesHeld > 0 && p.MarketOutcome != null ? p.SharesHeld * (p.MarketOutcome.CurrentPrice - p.AvgCost) : 0, + p.MarketOutcome?.CurrentPrice ?? 0, + p.LastTradeExecutedAt + )).ToList(); + } + public async Task> GetTradersAsync(int skip = 0, int take = 50, string? platform = null, bool highlyCopyable = false, CancellationToken ct = default) { PlatformType? pType = null; @@ -165,7 +182,7 @@ public class AnalyticsService : IAnalyticsService return traders.Select(t => MapTraderDto(t, wIds)).ToList(); } - public async Task> GetMarketsAsync(int skip = 0, int take = 50, string? platform = null, CancellationToken ct = default) + public async Task> GetMarketsAsync(int skip = 0, int take = 50, string? platform = null, string? category = null, string? query = null, CancellationToken ct = default) { // NOTE: Currently IMarketRepository.GetActiveAsync doesn't support pagination/filtering. // We will fetch all and filter in memory for now, or you can update repository. @@ -176,11 +193,17 @@ public class AnalyticsService : IAnalyticsService if (!string.IsNullOrEmpty(platform) && platform != "All" && Enum.TryParse(platform, true, out var pt)) pType = pt; - var query = markets.AsEnumerable(); + var q = markets.AsEnumerable(); if (pType.HasValue) - query = query.Where(m => m.Platform == pType.Value); + q = q.Where(m => m.Platform == pType.Value); + + if (!string.IsNullOrEmpty(category) && category != "All" && Enum.TryParse(category, true, out var cat)) + q = q.Where(m => m.Category == cat); + + if (!string.IsNullOrEmpty(query)) + q = q.Where(m => m.Question.Contains(query, StringComparison.OrdinalIgnoreCase) || m.ConditionId.Contains(query)); - var result = query.Skip(skip).Take(take).Select(m => new MarketDto + var result = q.Skip(skip).Take(take).Select(m => new MarketDto { Id = m.Id, Platform = m.Platform.ToString(), @@ -213,6 +236,7 @@ public class AnalyticsService : IAnalyticsService 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.WinRate, trader.TotalPnl, trader.TotalTrades, + a?.Trades30d ?? 0, a?.PnL30d ?? 0, a?.WinRate30d ?? 0, a?.EstimatedBankroll ?? 0, s?.ActivityScore ?? 0, s?.QualityScore ?? 0, s?.VolumeScore ?? 0, s?.TimingScore ?? 0, s?.CombinedScore ?? 0, a?.CopytradingScore ?? 0, a?.CopytradingQualityScore ?? 0, a?.CopytradingCopyabilityScore ?? 0, s?.Rank ?? 0, wl != null, trader.CreatedAt, trader.LastPolledAt, trader.AiStrategySummary, @@ -248,6 +272,9 @@ public class AnalyticsService : IAnalyticsService IsResolved = market.IsResolved, ResolutionOutcome = market.ResolutionOutcome, ImageUrl = market.ImageUrl, + BotActivityScore = market.Analytics?.BotActivityScore ?? 0, + UniqueTradersCount = market.Analytics?.UniqueTradersCount ?? 0, + AverageTradeSize = market.Analytics?.AverageTradeSize ?? 0, Outcomes = market.Outcomes.Select(o => new MarketOutcomeDto { Name = o.Label, Price = (double)o.CurrentPrice }).ToList(), RecentTrades = recentTrades.Select(MapTradeDto).ToList() }; @@ -455,6 +482,7 @@ public class AnalyticsService : IAnalyticsService t.CurrentScore?.CombinedScore ?? 0, t.Analytics?.CopytradingScore ?? 0, t.Analytics?.CopytradingQualityScore ?? 0, t.Analytics?.CopytradingCopyabilityScore ?? 0, t.WinRate, t.TotalPnl, t.TotalTrades, + t.Analytics?.Trades30d ?? 0, t.Analytics?.PnL30d ?? 0, t.Analytics?.WinRate30d ?? 0, t.Analytics?.EstimatedBankroll ?? 0, wIds.Contains(t.Id), t.IsSuspectedBot, t.LastPolledAt); private static TradeDto MapTradeDto(Trade t) => new( diff --git a/src/Predictalytics.Domain/Entities/AnalyticsEntities.cs b/src/Predictalytics.Domain/Entities/AnalyticsEntities.cs index 219b4bb..c23f9bd 100644 --- a/src/Predictalytics.Domain/Entities/AnalyticsEntities.cs +++ b/src/Predictalytics.Domain/Entities/AnalyticsEntities.cs @@ -22,6 +22,8 @@ public class TraderAnalytics public decimal PnL24h { get; set; } public decimal WinRate24h { get; set; } + public int Trades30d { get; set; } + public decimal EstimatedBankroll { get; set; } public decimal CurrentBalance { get; set; } diff --git a/src/Predictalytics.Domain/Interfaces/ITraderRepository.cs b/src/Predictalytics.Domain/Interfaces/ITraderRepository.cs index 65706b3..0ac7123 100644 --- a/src/Predictalytics.Domain/Interfaces/ITraderRepository.cs +++ b/src/Predictalytics.Domain/Interfaces/ITraderRepository.cs @@ -27,4 +27,6 @@ public interface ITraderRepository Task> GetTradersForPollingAsync(int take, CancellationToken ct = default); Task> SearchAsync(string query, int take = 20, CancellationToken ct = default); + + Task> GetPositionsAsync(int traderId, CancellationToken ct = default); } diff --git a/src/Predictalytics.Infrastructure/Data/Repositories/TraderRepository.cs b/src/Predictalytics.Infrastructure/Data/Repositories/TraderRepository.cs index 1b54b1f..467b7b0 100644 --- a/src/Predictalytics.Infrastructure/Data/Repositories/TraderRepository.cs +++ b/src/Predictalytics.Infrastructure/Data/Repositories/TraderRepository.cs @@ -155,4 +155,14 @@ public class TraderRepository : ITraderRepository .Take(take) .ToListAsync(ct); } + + public async Task> GetPositionsAsync(int traderId, CancellationToken ct = default) + { + return await _db.TraderPositions + .Include(p => p.MarketOutcome) + .ThenInclude(o => o.Market) + .Where(p => p.TraderId == traderId && (p.SharesHeld > 0 || p.RealizedPnl != 0)) + .OrderByDescending(p => p.LastTradeExecutedAt ?? DateTime.MinValue) + .ToListAsync(ct); + } } diff --git a/src/Predictalytics.Infrastructure/Migrations/20260709095847_AddTrades30dToAnalytics.Designer.cs b/src/Predictalytics.Infrastructure/Migrations/20260709095847_AddTrades30dToAnalytics.Designer.cs new file mode 100644 index 0000000..d63e8e8 --- /dev/null +++ b/src/Predictalytics.Infrastructure/Migrations/20260709095847_AddTrades30dToAnalytics.Designer.cs @@ -0,0 +1,1100 @@ +ο»Ώ// +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("20260709095847_AddTrades30dToAnalytics")] + partial class AddTrades30dToAnalytics + { + /// + 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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("IsRead") + .HasColumnType("tinyint(1)"); + + b.Property("Message") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("varchar(4096)"); + + b.Property("Platform") + .HasColumnType("int"); + + b.Property("Severity") + .HasColumnType("int"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("varchar(512)"); + + b.Property("TraderId") + .HasColumnType("int"); + + b.Property("Type") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("TraderId"); + + b.ToTable("Alerts"); + }); + + modelBuilder.Entity("Predictalytics.Domain.Entities.BackgroundJob", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CompletedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("ErrorMessage") + .HasMaxLength(4096) + .HasColumnType("varchar(4096)"); + + b.Property("JobType") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("varchar(64)"); + + b.Property("StartedAt") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("varchar(64)"); + + b.Property("TraderId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("JobType"); + + b.HasIndex("Status"); + + b.HasIndex("TraderId"); + + b.ToTable("BackgroundJobs"); + }); + + modelBuilder.Entity("Predictalytics.Domain.Entities.Event", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("DbCreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Description") + .HasMaxLength(4096) + .HasColumnType("varchar(4096)"); + + b.Property("EndDate") + .HasColumnType("datetime(6)"); + + b.Property("ImageUrl") + .HasMaxLength(1024) + .HasColumnType("varchar(1024)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("IsClosed") + .HasColumnType("tinyint(1)"); + + b.Property("LastUpdatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Platform") + .HasColumnType("int"); + + b.Property("PlatformEventId") + .HasColumnType("bigint"); + + b.Property("Slug") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("varchar(512)"); + + b.Property("StartDate") + .HasColumnType("datetime(6)"); + + b.Property("Tags") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("varchar(1024)"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Category") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("varchar(64)"); + + b.Property("ClosedAt") + .HasColumnType("datetime(6)"); + + b.Property("ConditionId") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("DbCreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Description") + .HasMaxLength(4096) + .HasColumnType("varchar(4096)"); + + b.Property("EndDate") + .HasColumnType("datetime(6)"); + + b.Property("EventId") + .HasColumnType("int"); + + b.Property("FeeRateBps") + .HasColumnType("decimal(65,30)"); + + b.Property("ImageUrl") + .HasMaxLength(1024) + .HasColumnType("varchar(1024)"); + + b.Property("IsNegRisk") + .HasColumnType("tinyint(1)"); + + b.Property("IsResolved") + .HasColumnType("tinyint(1)"); + + b.Property("LastTradesUpdatedAt") + .HasColumnType("datetime(6)"); + + b.Property("LastUpdatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Liquidity") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("MarketSlug") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("varchar(512)"); + + b.Property("Platform") + .HasColumnType("int"); + + b.Property("PlatformMarketId") + .HasColumnType("bigint"); + + b.Property("Question") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("varchar(1024)"); + + b.Property("QuestionId") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("ResolutionOutcome") + .HasColumnType("longtext"); + + b.Property("StartDate") + .HasColumnType("datetime(6)"); + + b.Property("Subcategory") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("varchar(128)"); + + b.Property("Volume") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("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("MarketId") + .HasColumnType("int"); + + b.Property("AverageTradeSize") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("BotActivityScore") + .HasPrecision(8, 4) + .HasColumnType("decimal(8,4)"); + + b.Property("LastCalculatedAt") + .HasColumnType("datetime(6)"); + + b.Property("UniqueTradersCount") + .HasColumnType("int"); + + b.HasKey("MarketId"); + + b.ToTable("MarketAnalytics"); + }); + + modelBuilder.Entity("Predictalytics.Domain.Entities.MarketOutcome", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CurrentPrice") + .HasPrecision(18, 8) + .HasColumnType("decimal(18,8)"); + + b.Property("Label") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("MarketId") + .HasColumnType("int"); + + b.Property("OutcomeIndex") + .HasColumnType("int"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("MarketOutcomeId") + .HasColumnType("int"); + + b.Property("Price") + .HasPrecision(10, 6) + .HasColumnType("decimal(10,6)"); + + b.Property("Timestamp") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("MarketOutcomeId", "Timestamp"); + + b.ToTable("MarketOutcomePriceSnapshots"); + }); + + modelBuilder.Entity("Predictalytics.Domain.Entities.PlatformConfig", b => + { + b.Property("Id") + .HasColumnType("int"); + + b.Property("BaseUrl") + .HasMaxLength(1024) + .HasColumnType("varchar(1024)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("varchar(128)"); + + b.Property("SettingsJson") + .HasColumnType("longtext"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.ToTable("PlatformConfigs"); + }); + + modelBuilder.Entity("Predictalytics.Domain.Entities.Trade", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Amount") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("AssetId") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("varchar(80)"); + + b.Property("DbMarketId") + .HasColumnType("int"); + + b.Property("ExecutedAt") + .HasColumnType("datetime(6)"); + + b.Property("IsContextEnriched") + .HasColumnType("tinyint(1)"); + + b.Property("MarketId") + .IsRequired() + .HasMaxLength(66) + .HasColumnType("varchar(66)"); + + b.Property("MarketOutcomeId") + .HasColumnType("int"); + + b.Property("Outcome") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("varchar(128)"); + + b.Property("Platform") + .HasColumnType("int"); + + b.Property("PlatformTradeId") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("PostTradePrice1m") + .HasColumnType("decimal(18,4)"); + + b.Property("PreTradePrice1m") + .HasColumnType("decimal(18,4)"); + + b.Property("Price") + .HasPrecision(18, 6) + .HasColumnType("decimal(18,6)"); + + b.Property("Side") + .HasColumnType("int"); + + b.Property("Size") + .HasPrecision(14, 6) + .HasColumnType("decimal(14,6)"); + + b.Property("TraderId") + .HasColumnType("int"); + + b.Property("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.TradeContext", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("EstimatedOrderType") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("varchar(32)"); + + b.Property("EstimatedSlippage") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("FollowerFillPrice10s") + .HasColumnType("decimal(65,30)"); + + b.Property("FollowerFillPrice60s") + .HasColumnType("decimal(65,30)"); + + b.Property("PriceAfter1m") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("PriceBefore1m") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("TradeId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("TradeId") + .IsUnique(); + + b.ToTable("TradeContexts"); + }); + + modelBuilder.Entity("Predictalytics.Domain.Entities.Trader", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AiStrategySummary") + .HasColumnType("longtext"); + + b.Property("AiStrategyUpdatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("IsAutoDiscovered") + .HasColumnType("tinyint(1)"); + + b.Property("IsInitialImportComplete") + .HasColumnType("tinyint(1)"); + + b.Property("IsSuspectedBot") + .HasColumnType("tinyint(1)"); + + b.Property("LastAnalyzedAt") + .HasColumnType("datetime(6)"); + + b.Property("LastApiErrorAt") + .HasColumnType("datetime(6)"); + + b.Property("LastPolledAt") + .HasColumnType("datetime(6)"); + + b.Property("LastTradesUpdatedAt") + .HasColumnType("datetime(6)"); + + b.Property("ManualPriorityOverride") + .HasColumnType("int"); + + b.Property("Notes") + .HasColumnType("longtext"); + + b.Property("Platform") + .HasColumnType("int"); + + b.Property("PlatformUserId") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("varchar(128)"); + + b.Property("Strategy") + .HasColumnType("int"); + + b.Property("Tier") + .HasColumnType("int"); + + b.Property("TotalPnl") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("TotalTrades") + .HasColumnType("int"); + + b.Property("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("TraderId") + .HasColumnType("int"); + + b.Property("CopytradingCopyabilityScore") + .HasColumnType("decimal(65,30)"); + + b.Property("CopytradingQualityScore") + .HasColumnType("decimal(65,30)"); + + b.Property("CopytradingScore") + .HasColumnType("decimal(65,30)"); + + b.Property("CurrentBalance") + .HasColumnType("decimal(65,30)"); + + b.Property("EstimatedBankroll") + .HasColumnType("decimal(65,30)"); + + b.Property("LastCalculatedAt") + .HasColumnType("datetime(6)"); + + b.Property("OverallPnL") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("OverallWinRate") + .HasPrecision(8, 4) + .HasColumnType("decimal(8,4)"); + + b.Property("PnL24h") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("PnL30d") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("PnL7d") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("Trades30d") + .HasColumnType("int"); + + b.Property("WinRate24h") + .HasPrecision(8, 4) + .HasColumnType("decimal(8,4)"); + + b.Property("WinRate30d") + .HasPrecision(8, 4) + .HasColumnType("decimal(8,4)"); + + b.Property("WinRate7d") + .HasPrecision(8, 4) + .HasColumnType("decimal(8,4)"); + + b.HasKey("TraderId"); + + b.ToTable("TraderAnalytics"); + }); + + modelBuilder.Entity("Predictalytics.Domain.Entities.TraderCategoryPerformance", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Category") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("varchar(64)"); + + b.Property("Subcategory") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("varchar(128)"); + + b.Property("TotalPnL") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("TotalTrades") + .HasColumnType("int"); + + b.Property("TotalVolume") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("TraderId") + .HasColumnType("int"); + + b.Property("WinningTrades") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("TraderId", "Category", "Subcategory") + .IsUnique(); + + b.ToTable("TraderCategoryPerformances"); + }); + + modelBuilder.Entity("Predictalytics.Domain.Entities.TraderDailySnapshot", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CurrentBalance") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("Date") + .HasColumnType("datetime(6)"); + + b.Property("TotalPnl") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("TraderId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("TraderId", "Date") + .IsUnique(); + + b.ToTable("TraderDailySnapshots"); + }); + + modelBuilder.Entity("Predictalytics.Domain.Entities.TraderPosition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AvgCost") + .HasPrecision(10, 6) + .HasColumnType("decimal(10,6)"); + + b.Property("IsHistoryPruned") + .HasColumnType("tinyint(1)"); + + b.Property("LastAppliedTradeId") + .HasColumnType("bigint"); + + b.Property("LastTradeExecutedAt") + .HasColumnType("datetime(6)"); + + b.Property("LastUpdatedAt") + .HasColumnType("datetime(6)"); + + b.Property("MarketOutcomeId") + .HasColumnType("int"); + + b.Property("RealizedPnl") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("SharesHeld") + .HasPrecision(14, 6) + .HasColumnType("decimal(14,6)"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("ActivityScore") + .HasPrecision(5, 2) + .HasColumnType("decimal(5,2)"); + + b.Property("CalculatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CombinedScore") + .HasPrecision(5, 2) + .HasColumnType("decimal(5,2)"); + + b.Property("QualityScore") + .HasPrecision(5, 2) + .HasColumnType("decimal(5,2)"); + + b.Property("Rank") + .HasColumnType("int"); + + b.Property("TimingScore") + .HasPrecision(5, 2) + .HasColumnType("decimal(5,2)"); + + b.Property("TraderId") + .HasColumnType("int"); + + b.Property("VolumeScore") + .HasPrecision(5, 2) + .HasColumnType("decimal(5,2)"); + + b.HasKey("Id"); + + b.HasIndex("TraderId") + .IsUnique(); + + b.ToTable("TraderScores"); + }); + + modelBuilder.Entity("Predictalytics.Domain.Entities.WatchlistEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AddedAt") + .HasColumnType("datetime(6)"); + + b.Property("AlertsEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("Label") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("Notes") + .HasColumnType("longtext"); + + b.Property("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.BackgroundJob", 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.TradeContext", b => + { + b.HasOne("Predictalytics.Domain.Entities.Trade", "Trade") + .WithOne("Context") + .HasForeignKey("Predictalytics.Domain.Entities.TradeContext", "TradeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trade"); + }); + + 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("CategoryPerformances") + .HasForeignKey("TraderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trader"); + }); + + modelBuilder.Entity("Predictalytics.Domain.Entities.TraderDailySnapshot", 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.Trade", b => + { + b.Navigation("Context"); + }); + + modelBuilder.Entity("Predictalytics.Domain.Entities.Trader", b => + { + b.Navigation("Analytics"); + + b.Navigation("CategoryPerformances"); + + b.Navigation("CurrentScore"); + + b.Navigation("Positions"); + + b.Navigation("Trades"); + + b.Navigation("WatchlistEntries"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Predictalytics.Infrastructure/Migrations/20260709095847_AddTrades30dToAnalytics.cs b/src/Predictalytics.Infrastructure/Migrations/20260709095847_AddTrades30dToAnalytics.cs new file mode 100644 index 0000000..ac9774b --- /dev/null +++ b/src/Predictalytics.Infrastructure/Migrations/20260709095847_AddTrades30dToAnalytics.cs @@ -0,0 +1,29 @@ +ο»Ώusing Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Predictalytics.Infrastructure.Migrations +{ + /// + public partial class AddTrades30dToAnalytics : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Trades30d", + table: "TraderAnalytics", + type: "int", + nullable: false, + defaultValue: 0); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Trades30d", + table: "TraderAnalytics"); + } + } +} diff --git a/src/Predictalytics.Infrastructure/Migrations/AppDbContextModelSnapshot.cs b/src/Predictalytics.Infrastructure/Migrations/AppDbContextModelSnapshot.cs index eac4f5b..46601b0 100644 --- a/src/Predictalytics.Infrastructure/Migrations/AppDbContextModelSnapshot.cs +++ b/src/Predictalytics.Infrastructure/Migrations/AppDbContextModelSnapshot.cs @@ -667,6 +667,9 @@ namespace Predictalytics.Infrastructure.Migrations .HasPrecision(18, 4) .HasColumnType("decimal(18,4)"); + b.Property("Trades30d") + .HasColumnType("int"); + b.Property("WinRate24h") .HasPrecision(8, 4) .HasColumnType("decimal(8,4)"); diff --git a/src/Predictalytics.Infrastructure/Services/PositionPnLEngine.cs b/src/Predictalytics.Infrastructure/Services/PositionPnLEngine.cs index 61a2023..b935c96 100644 --- a/src/Predictalytics.Infrastructure/Services/PositionPnLEngine.cs +++ b/src/Predictalytics.Infrastructure/Services/PositionPnLEngine.cs @@ -321,6 +321,9 @@ public class PositionPnLEngine : IPositionPnLEngine analytics.PnL7d = overallPnl - (snapshot7d?.TotalPnl ?? 0); analytics.PnL30d = overallPnl - (snapshot30d?.TotalPnl ?? 0); + // Count Trades30d + analytics.Trades30d = trades.Count(t => t.ExecutedAt >= cutoff30d); + // Calculate Win Rate on Market level var (winRateOverall, winRate30d, winRate7d, winRate24h) = CalculateMarketWinRates(trades, tempPositions, cutoff30d, cutoff7d, cutoff24h);