Implement WinRate, Bankroll, Positions UI and add Trades30d to Analytics

This commit is contained in:
Richard
2026-07-09 12:44:32 +02:00
parent 3b11b71188
commit 7964194e9e
16 changed files with 1318 additions and 28 deletions
@@ -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);
});
@@ -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);
+35 -9
View File
@@ -111,7 +111,7 @@
<div class="card-header"><h2>Top Traders</h2></div>
<div class="table-wrap">
<table class="data-table" id="topTradersTable">
<thead><tr><th>#</th><th>Trader</th><th>Platform</th><th>Score</th><th>Win Rate</th><th>PnL</th><th>Tier</th><th>Trades</th></tr></thead>
<thead><tr><th>#</th><th>Trader</th><th>Platform</th><th>Score</th><th>Win Rate</th><th>PnL</th><th>Trades (30d|All)</th></tr></thead>
<tbody id="topTradersBody"></tbody>
</table>
</div>
@@ -177,7 +177,7 @@
<th style="cursor:pointer" onclick="setTraderSort('copyability')">Copyability ↕</th>
<th style="cursor:pointer" onclick="setTraderSort('winrate')">Win Rate ↕</th>
<th style="cursor:pointer" onclick="setTraderSort('pnl')">PnL ↕</th>
<th>Tier</th>
<th style="cursor:pointer" onclick="setTraderSort('trades')">Trades (30d|All) ↕</th>
<th>Strategy</th>
<th>Actions</th>
</tr></thead>
@@ -188,7 +188,26 @@
</section>
<!-- Markets Page -->
<section class="page" id="page-markets"><h1 class="page-title">Markets</h1><div class="card"><div class="table-wrap"><table class="data-table"><thead><tr><th>Platform</th><th>Question</th><th>Volume</th><th>Liquidity</th><th>End Date</th><th>Status</th></tr></thead><tbody id="allMarketsBody"></tbody></table></div></div></section>
<section class="page" id="page-markets">
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:24px; flex-wrap:wrap; gap:12px;">
<div style="display:flex; align-items:center; gap:16px; flex-wrap:wrap;">
<h1 class="page-title" style="margin-bottom:0">Markets</h1>
<select id="marketsCategory" class="platform-select" onchange="loadMarkets()">
<option value="All">All Categories</option>
<option value="Politics">Politics</option>
<option value="Crypto">Crypto</option>
<option value="Sports">Sports</option>
<option value="PopCulture">PopCulture</option>
<option value="Other">Other</option>
</select>
<div class="search-box">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
<input type="text" id="marketsSearchInput" placeholder="Search markets..." autocomplete="off" onkeypress="if(event.key==='Enter') loadMarkets()">
</div>
</div>
</div>
<div class="card"><div class="table-wrap"><table class="data-table"><thead><tr><th>Platform</th><th>Question</th><th>Volume</th><th>Liquidity</th><th>End Date</th><th>Status</th></tr></thead><tbody id="allMarketsBody"></tbody></table></div></div>
</section>
<!-- Alerts Page -->
<section class="page" id="page-alerts"><h1 class="page-title">Alerts</h1><div class="card" id="alertsList"></div></section>
@@ -264,8 +283,11 @@
<button class="btn-sm btn-outline" id="btn-open-platform" style="display:none;">
🌐 Open Platform
</button>
<button class="btn-sm btn-primary" id="btn-refresh-trader">
⟱ Sync History
<button class="btn-sm btn-primary" id="btn-sync-trader">
⟱ Sync
</button>
<button class="btn-sm btn-primary" id="btn-analyze-trader">
⚙ Analyze
</button>
</div>
</div>
@@ -302,14 +324,15 @@
<div class="tab-content" id="td-tab-analytics" style="display:block;">
<div class="metrics-grid">
<div class="metric-card"><div class="metric-label">Win Rate</div><div class="metric-value" id="td-winrate">---</div></div>
<div class="metric-card"><div class="metric-label">Win Rate (30d)</div><div class="metric-value" id="td-winrate30d">---</div></div>
<div class="metric-card"><div class="metric-label">Total PnL</div><div class="metric-value" id="td-pnl">---</div></div>
<div class="metric-card"><div class="metric-label">PnL (30d)</div><div class="metric-value" id="td-pnl30d">---</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"><div class="metric-label">Est. Bankroll</div><div class="metric-value" id="td-bankroll">---</div></div>
<div class="metric-card"><div class="metric-label">Quality Edge</div><div class="metric-value" id="td-quality-score">---</div></div>
<div class="metric-card"><div class="metric-label">Copyability</div><div class="metric-value" id="td-copyability-score">---</div></div>
<div class="metric-card accent">
<div class="metric-label">Combined Score</div>
<div class="metric-value" id="td-score">---</div>
<button class="btn-sm btn-outline" id="btn-force-analyze" style="margin-top:8px; padding:4px 8px; width:100%; border-color:rgba(255,255,255,0.2);">Recalculate</button>
</div>
</div>
@@ -351,8 +374,8 @@
<div class="card-header"><h2>Active Positions</h2></div>
<div class="table-wrap">
<table class="data-table">
<thead><tr><th>Market</th><th>Outcome</th><th>Shares</th><th>Avg Price</th><th>Est. PnL</th></tr></thead>
<tbody id="td-positionsBody"><tr><td colspan="5" style="text-align:center;">Coming soon</td></tr></tbody>
<thead><tr><th>Market / Outcome</th><th>Shares</th><th>Avg Price</th><th>Current Price</th><th>Realized PnL</th><th>Unrealized PnL</th></tr></thead>
<tbody id="td-positionsBody"><tr><td colspan="6" style="text-align:center;">Loading...</td></tr></tbody>
</table>
</div>
</div>
@@ -394,6 +417,9 @@
<div class="metric-card"><div class="metric-label">Volume</div><div class="metric-value" id="md-volume"></div></div>
<div class="metric-card"><div class="metric-label">Liquidity</div><div class="metric-value" id="md-liquidity"></div></div>
<div class="metric-card"><div class="metric-label">Status</div><div class="metric-value" id="md-status"></div></div>
<div class="metric-card accent"><div class="metric-label">Bot Activity</div><div class="metric-value" id="md-bot-activity"></div></div>
<div class="metric-card"><div class="metric-label">Unique Traders</div><div class="metric-value" id="md-unique-traders"></div></div>
<div class="metric-card"><div class="metric-label">Avg Trade Size</div><div class="metric-value" id="md-avg-trade-size"></div></div>
</div>
<div class="card">
<div class="card-header"><h2>Outcomes</h2></div>
+63 -12
View File
@@ -221,8 +221,7 @@ async function loadDashboard() {
<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.pnl(t.totalPnl)}</td>
<td onclick="viewTrader(${t.id})" style="cursor:pointer">${fmt.tier(t.tier)}</td>
<td onclick="viewTrader(${t.id})" style="cursor:pointer">${fmt.num(t.totalTrades)}</td>
<td onclick="viewTrader(${t.id})" style="cursor:pointer">${t.trades30d} | ${t.totalTrades}</td>
</tr>
`).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() {
<td>${Number(t.copytradingCopyabilityScore || 0).toFixed(1)}</td>
<td>${fmt.pct(t.winRate)}</td>
<td>${fmt.pnl(t.totalPnl)}</td>
<td>${fmt.tier(t.tier)}</td>
<td>${t.trades30d} | ${t.totalTrades}</td>
<td>${t.strategy}</td>
<td>
<div style="display:flex; gap:4px;">
@@ -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 = '<tr><td colspan="6"><div class="empty-state"><p>No markets found.</p></div></td></tr>'; 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) {
<td>${fmt.usd(tr.amount)}</td>
</tr>
`).join('');
loadTraderPositions(id);
}
async function loadTraderPositions(id) {
const tbody = document.getElementById('td-positionsBody');
tbody.innerHTML = '<tr><td colspan="6" style="text-align:center;">Loading...</td></tr>';
try {
const data = await api(`/api/traders/${id}/positions`);
if (!data || !data.length) {
tbody.innerHTML = '<tr><td colspan="6"><div class="empty-state"><p>No active positions.</p></div></td></tr>';
return;
}
tbody.innerHTML = data.map(p => `
<tr>
<td title="Market ID: ${p.marketId}">
<div style="font-weight:600">${p.marketName || p.marketId}</div>
<div style="font-size:0.85em; color:var(--text-muted)">${p.category || 'Other'} &middot; ${p.outcomeToken || 'Unknown'}</div>
</td>
<td>${fmt.num(p.sharesHeld)}</td>
<td>${Number(p.avgCost).toFixed(2)}</td>
<td>${Number(p.currentPrice).toFixed(2)}</td>
<td>${fmt.pnl(p.realizedPnl)}</td>
<td>${fmt.pnl(p.unrealizedPnl)}</td>
</tr>
`).join('');
} catch (e) {
tbody.innerHTML = '<tr><td colspan="6" style="text-align:center; color:var(--pnl-negative);">Error loading positions.</td></tr>';
}
}
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 = `<img src="${m.imageUrl}" alt="Market" style="width:100%; border-radius:8px; margin-bottom:16px;">`;
@@ -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<MarketOutcomeDto> Outcomes { get; set; } = new List<MarketOutcomeDto>();
public IReadOnlyList<TradeDto> RecentTrades { get; set; } = new List<TradeDto>();
}
@@ -9,4 +9,5 @@ public class MarketDto
public double Liquidity { get; set; }
public DateTime? EndDate { get; set; }
public bool IsResolved { get; set; }
}
@@ -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
);
@@ -17,8 +17,11 @@ public interface IAnalyticsService
/// <summary>Get a trader's details.</summary>
Task<TraderDetailDto?> GetTraderDetailAsync(int traderId, CancellationToken ct = default);
/// <summary>Get a trader's positions.</summary>
Task<IReadOnlyList<TraderPositionDto>> GetTraderPositionsAsync(int traderId, CancellationToken ct = default);
/// <summary>Get list of markets.</summary>
Task<IReadOnlyList<MarketDto>> GetMarketsAsync(int skip = 0, int take = 50, string? platform = null, CancellationToken ct = default);
Task<IReadOnlyList<MarketDto>> GetMarketsAsync(int skip = 0, int take = 50, string? platform = null, string? category = null, string? query = null, CancellationToken ct = default);
/// <summary>Get market details.</summary>
Task<MarketDetailDto?> GetMarketDetailAsync(int marketId, CancellationToken ct = default);
@@ -145,6 +145,23 @@ public class AnalyticsService : IAnalyticsService
analysis.BotIndicators, analysis.Summary, tradeDtos);
}
public async Task<IReadOnlyList<TraderPositionDto>> 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<IReadOnlyList<TraderDto>> 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<IReadOnlyList<MarketDto>> GetMarketsAsync(int skip = 0, int take = 50, string? platform = null, CancellationToken ct = default)
public async Task<IReadOnlyList<MarketDto>> 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<PlatformType>(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<MarketCategory>(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(
@@ -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; }
@@ -27,4 +27,6 @@ public interface ITraderRepository
Task<IReadOnlyList<Trader>> GetTradersForPollingAsync(int take, CancellationToken ct = default);
Task<IReadOnlyList<Trader>> SearchAsync(string query, int take = 20, CancellationToken ct = default);
Task<IReadOnlyList<TraderPosition>> GetPositionsAsync(int traderId, CancellationToken ct = default);
}
@@ -155,4 +155,14 @@ public class TraderRepository : ITraderRepository
.Take(take)
.ToListAsync(ct);
}
public async Task<IReadOnlyList<TraderPosition>> 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);
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,29 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Predictalytics.Infrastructure.Migrations
{
/// <inheritdoc />
public partial class AddTrades30dToAnalytics : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int>(
name: "Trades30d",
table: "TraderAnalytics",
type: "int",
nullable: false,
defaultValue: 0);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "Trades30d",
table: "TraderAnalytics");
}
}
}
@@ -667,6 +667,9 @@ namespace Predictalytics.Infrastructure.Migrations
.HasPrecision(18, 4)
.HasColumnType("decimal(18,4)");
b.Property<int>("Trades30d")
.HasColumnType("int");
b.Property<decimal>("WinRate24h")
.HasPrecision(8, 4)
.HasColumnType("decimal(8,4)");
@@ -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);