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"); 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); return Results.Ok(result);
}); });
@@ -24,6 +24,12 @@ public static class TraderEndpoints
return dd is not null ? Results.Ok(dd) : Results.NotFound(); 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) => group.MapPost("/{id:int}/priority", async (int id, int? score, IScoringService svc, CancellationToken ct) =>
{ {
await svc.SetManualOverrideAsync(id, score, 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="card-header"><h2>Top Traders</h2></div>
<div class="table-wrap"> <div class="table-wrap">
<table class="data-table" id="topTradersTable"> <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> <tbody id="topTradersBody"></tbody>
</table> </table>
</div> </div>
@@ -177,7 +177,7 @@
<th style="cursor:pointer" onclick="setTraderSort('copyability')">Copyability ↕</th> <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('winrate')">Win Rate ↕</th>
<th style="cursor:pointer" onclick="setTraderSort('pnl')">PnL ↕</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>Strategy</th>
<th>Actions</th> <th>Actions</th>
</tr></thead> </tr></thead>
@@ -188,7 +188,26 @@
</section> </section>
<!-- Markets Page --> <!-- 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 --> <!-- Alerts Page -->
<section class="page" id="page-alerts"><h1 class="page-title">Alerts</h1><div class="card" id="alertsList"></div></section> <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;"> <button class="btn-sm btn-outline" id="btn-open-platform" style="display:none;">
🌐 Open Platform 🌐 Open Platform
</button> </button>
<button class="btn-sm btn-primary" id="btn-refresh-trader"> <button class="btn-sm btn-primary" id="btn-sync-trader">
⟱ Sync History ⟱ Sync
</button>
<button class="btn-sm btn-primary" id="btn-analyze-trader">
⚙ Analyze
</button> </button>
</div> </div>
</div> </div>
@@ -302,14 +324,15 @@
<div class="tab-content" id="td-tab-analytics" style="display:block;"> <div class="tab-content" id="td-tab-analytics" style="display:block;">
<div class="metrics-grid"> <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</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">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">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">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"><div class="metric-label">Copyability</div><div class="metric-value" id="td-copyability-score">---</div></div>
<div class="metric-card accent"> <div class="metric-card accent">
<div class="metric-label">Combined Score</div>
<div class="metric-value" id="td-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>
</div> </div>
@@ -351,8 +374,8 @@
<div class="card-header"><h2>Active Positions</h2></div> <div class="card-header"><h2>Active Positions</h2></div>
<div class="table-wrap"> <div class="table-wrap">
<table class="data-table"> <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> <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="5" style="text-align:center;">Coming soon</td></tr></tbody> <tbody id="td-positionsBody"><tr><td colspan="6" style="text-align:center;">Loading...</td></tr></tbody>
</table> </table>
</div> </div>
</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">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">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"><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>
<div class="card"> <div class="card">
<div class="card-header"><h2>Outcomes</h2></div> <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"><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>
<td onclick="viewTrader(${t.id})" style="cursor:pointer">${fmt.pnl(t.totalPnl)}</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">${t.trades30d} | ${t.totalTrades}</td>
<td onclick="viewTrader(${t.id})" style="cursor:pointer">${fmt.num(t.totalTrades)}</td>
</tr> </tr>
`).join(''); `).join('');
@@ -319,6 +318,7 @@ async function loadTraders() {
else if (currentSort === 'copyability') { valA = a.copytradingCopyabilityScore || 0; valB = b.copytradingCopyabilityScore || 0; } 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 === 'winrate') { valA = a.winRate; valB = b.winRate; }
else if (currentSort === 'pnl') { valA = a.totalPnl; valB = b.totalPnl; } 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 === 'name') { return a.displayName.localeCompare(b.displayName) * sortDirection; }
else if (currentSort === 'platform') { return a.platform.localeCompare(b.platform) * sortDirection; } else if (currentSort === 'platform') { return a.platform.localeCompare(b.platform) * sortDirection; }
else { valA = a.combinedScore; valB = b.combinedScore; } else { valA = a.combinedScore; valB = b.combinedScore; }
@@ -336,7 +336,7 @@ async function loadTraders() {
<td>${Number(t.copytradingCopyabilityScore || 0).toFixed(1)}</td> <td>${Number(t.copytradingCopyabilityScore || 0).toFixed(1)}</td>
<td>${fmt.pct(t.winRate)}</td> <td>${fmt.pct(t.winRate)}</td>
<td>${fmt.pnl(t.totalPnl)}</td> <td>${fmt.pnl(t.totalPnl)}</td>
<td>${fmt.tier(t.tier)}</td> <td>${t.trades30d} | ${t.totalTrades}</td>
<td>${t.strategy}</td> <td>${t.strategy}</td>
<td> <td>
<div style="display:flex; gap:4px;"> <div style="display:flex; gap:4px;">
@@ -370,6 +370,17 @@ async function loadAlerts() {
async function loadMarkets() { async function loadMarkets() {
let url = '/api/markets?skip=0&take=100'; let url = '/api/markets?skip=0&take=100';
if (currentPlatform !== 'All') url += `&platform=${currentPlatform}`; 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); let data = await api(url);
const tbody = document.getElementById('allMarketsBody'); 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; } 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) { async function viewTrader(id) {
window.currentTraderId = id;
navigateTo('trader-detail'); navigateTo('trader-detail');
const t = await api(`/api/traders/${id}`); const t = await api(`/api/traders/${id}`);
if (!t) return; if (!t) return;
@@ -405,27 +417,32 @@ async function viewTrader(id) {
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').innerHTML = fmt.pct(t.winRate); 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-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-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-score').textContent = Number(t.combinedScore).toFixed(1);
document.getElementById('td-quality-score').textContent = Number(t.copytradingQualityScore || 0).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-copyability-score').textContent = Number(t.copytradingCopyabilityScore || 0).toFixed(1);
document.getElementById('td-ai-summary').textContent = t.aiStrategySummary || 'Not analyzed yet.'; document.getElementById('td-ai-summary').textContent = t.aiStrategySummary || 'Not analyzed yet.';
const refreshBtn = document.getElementById('btn-refresh-trader'); const syncBtn = document.getElementById('btn-sync-trader');
refreshBtn.onclick = () => manualUpdateTrader(id); if (syncBtn) {
syncBtn.onclick = () => manualUpdateTrader(id);
}
const forceBtn = document.getElementById('btn-force-analyze'); const analyzeBtn = document.getElementById('btn-analyze-trader');
if(forceBtn) { if (analyzeBtn) {
forceBtn.onclick = async () => { analyzeBtn.onclick = async () => {
forceBtn.disabled = true; analyzeBtn.disabled = true;
forceBtn.textContent = '...'; analyzeBtn.textContent = '...';
try { try {
await api(`/api/traders/${id}/force-analyze`, { method: 'POST' }); await api(`/api/traders/${id}/force-analyze`, { method: 'POST' });
alert('Deep Analysis queued! Please wait a moment and then refresh.'); alert('Deep Analysis queued! Please wait a moment and then refresh.');
} finally { } finally {
forceBtn.disabled = false; analyzeBtn.disabled = false;
forceBtn.textContent = 'Recalculate'; analyzeBtn.textContent = '⚙ Analyze';
} }
}; };
} }
@@ -479,6 +496,37 @@ async function viewTrader(id) {
<td>${fmt.usd(tr.amount)}</td> <td>${fmt.usd(tr.amount)}</td>
</tr> </tr>
`).join(''); `).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) { function switchTraderTab(tabId) {
@@ -505,6 +553,9 @@ async function viewMarket(id) {
document.getElementById('md-volume').textContent = fmt.usd(m.volume); document.getElementById('md-volume').textContent = fmt.usd(m.volume);
document.getElementById('md-liquidity').textContent = fmt.usd(m.liquidity); document.getElementById('md-liquidity').textContent = fmt.usd(m.liquidity);
document.getElementById('md-status').textContent = m.isResolved ? 'Resolved' : 'Active'; 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'); 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;">`; 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 bool IsResolved { get; set; }
public string? ResolutionOutcome { get; set; } public string? ResolutionOutcome { get; set; }
public string? ImageUrl { 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<MarketOutcomeDto> Outcomes { get; set; } = new List<MarketOutcomeDto>();
public IReadOnlyList<TradeDto> RecentTrades { get; set; } = new List<TradeDto>(); public IReadOnlyList<TradeDto> RecentTrades { get; set; } = new List<TradeDto>();
} }
@@ -9,4 +9,5 @@ public class MarketDto
public double Liquidity { get; set; } public double Liquidity { get; set; }
public DateTime? EndDate { get; set; } public DateTime? EndDate { get; set; }
public bool IsResolved { get; set; } public bool IsResolved { get; set; }
} }
@@ -16,6 +16,10 @@ public record TraderDto(
decimal WinRate, decimal WinRate,
decimal TotalPnl, decimal TotalPnl,
int TotalTrades, int TotalTrades,
int Trades30d,
decimal PnL30d,
decimal WinRate30d,
decimal EstimatedBankroll,
bool IsOnWatchlist, bool IsOnWatchlist,
bool IsSuspectedBot, bool IsSuspectedBot,
DateTime? LastPolledAt DateTime? LastPolledAt
@@ -34,6 +38,10 @@ public record TraderDetailDto(
decimal WinRate, decimal WinRate,
decimal TotalPnl, decimal TotalPnl,
int TotalTrades, int TotalTrades,
int Trades30d,
decimal PnL30d,
decimal WinRate30d,
decimal EstimatedBankroll,
decimal ActivityScore, decimal ActivityScore,
decimal QualityScore, decimal QualityScore,
decimal VolumeScore, decimal VolumeScore,
@@ -59,3 +67,16 @@ public record TraderCategoryPerformanceDto(
int WinningTrades, int WinningTrades,
decimal WinRate 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> /// <summary>Get a trader's details.</summary>
Task<TraderDetailDto?> GetTraderDetailAsync(int traderId, CancellationToken ct = default); 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> /// <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> /// <summary>Get market details.</summary>
Task<MarketDetailDto?> GetMarketDetailAsync(int marketId, CancellationToken ct = default); Task<MarketDetailDto?> GetMarketDetailAsync(int marketId, CancellationToken ct = default);
@@ -145,6 +145,23 @@ public class AnalyticsService : IAnalyticsService
analysis.BotIndicators, analysis.Summary, tradeDtos); 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) public async Task<IReadOnlyList<TraderDto>> GetTradersAsync(int skip = 0, int take = 50, string? platform = null, bool highlyCopyable = false, CancellationToken ct = default)
{ {
PlatformType? pType = null; PlatformType? pType = null;
@@ -165,7 +182,7 @@ public class AnalyticsService : IAnalyticsService
return traders.Select(t => MapTraderDto(t, wIds)).ToList(); 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. // NOTE: Currently IMarketRepository.GetActiveAsync doesn't support pagination/filtering.
// We will fetch all and filter in memory for now, or you can update repository. // 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)) if (!string.IsNullOrEmpty(platform) && platform != "All" && Enum.TryParse<PlatformType>(platform, true, out var pt))
pType = pt; pType = pt;
var query = markets.AsEnumerable(); var q = markets.AsEnumerable();
if (pType.HasValue) if (pType.HasValue)
query = query.Where(m => m.Platform == pType.Value); q = q.Where(m => m.Platform == pType.Value);
var result = query.Skip(skip).Take(take).Select(m => new MarketDto 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 = q.Skip(skip).Take(take).Select(m => new MarketDto
{ {
Id = m.Id, Id = m.Id,
Platform = m.Platform.ToString(), Platform = m.Platform.ToString(),
@@ -213,6 +236,7 @@ public class AnalyticsService : IAnalyticsService
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,
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?.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, s?.CombinedScore ?? 0, a?.CopytradingScore ?? 0, a?.CopytradingQualityScore ?? 0, a?.CopytradingCopyabilityScore ?? 0, s?.Rank ?? 0, wl != null, trader.CreatedAt, trader.LastPolledAt,
trader.AiStrategySummary, trader.AiStrategySummary,
@@ -248,6 +272,9 @@ public class AnalyticsService : IAnalyticsService
IsResolved = market.IsResolved, IsResolved = market.IsResolved,
ResolutionOutcome = market.ResolutionOutcome, ResolutionOutcome = market.ResolutionOutcome,
ImageUrl = market.ImageUrl, 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(), Outcomes = market.Outcomes.Select(o => new MarketOutcomeDto { Name = o.Label, Price = (double)o.CurrentPrice }).ToList(),
RecentTrades = recentTrades.Select(MapTradeDto).ToList() RecentTrades = recentTrades.Select(MapTradeDto).ToList()
}; };
@@ -455,6 +482,7 @@ public class AnalyticsService : IAnalyticsService
t.CurrentScore?.CombinedScore ?? 0, t.Analytics?.CopytradingScore ?? 0, t.CurrentScore?.CombinedScore ?? 0, t.Analytics?.CopytradingScore ?? 0,
t.Analytics?.CopytradingQualityScore ?? 0, t.Analytics?.CopytradingCopyabilityScore ?? 0, t.Analytics?.CopytradingQualityScore ?? 0, t.Analytics?.CopytradingCopyabilityScore ?? 0,
t.WinRate, t.TotalPnl, t.TotalTrades, 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); wIds.Contains(t.Id), t.IsSuspectedBot, t.LastPolledAt);
private static TradeDto MapTradeDto(Trade t) => new( private static TradeDto MapTradeDto(Trade t) => new(
@@ -22,6 +22,8 @@ public class TraderAnalytics
public decimal PnL24h { get; set; } public decimal PnL24h { get; set; }
public decimal WinRate24h { get; set; } public decimal WinRate24h { get; set; }
public int Trades30d { get; set; }
public decimal EstimatedBankroll { get; set; } public decimal EstimatedBankroll { get; set; }
public decimal CurrentBalance { 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>> GetTradersForPollingAsync(int take, CancellationToken ct = default);
Task<IReadOnlyList<Trader>> SearchAsync(string query, int take = 20, 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) .Take(take)
.ToListAsync(ct); .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) .HasPrecision(18, 4)
.HasColumnType("decimal(18,4)"); .HasColumnType("decimal(18,4)");
b.Property<int>("Trades30d")
.HasColumnType("int");
b.Property<decimal>("WinRate24h") b.Property<decimal>("WinRate24h")
.HasPrecision(8, 4) .HasPrecision(8, 4)
.HasColumnType("decimal(8,4)"); .HasColumnType("decimal(8,4)");
@@ -321,6 +321,9 @@ public class PositionPnLEngine : IPositionPnLEngine
analytics.PnL7d = overallPnl - (snapshot7d?.TotalPnl ?? 0); analytics.PnL7d = overallPnl - (snapshot7d?.TotalPnl ?? 0);
analytics.PnL30d = overallPnl - (snapshot30d?.TotalPnl ?? 0); analytics.PnL30d = overallPnl - (snapshot30d?.TotalPnl ?? 0);
// Count Trades30d
analytics.Trades30d = trades.Count(t => t.ExecutedAt >= cutoff30d);
// Calculate Win Rate on Market level // Calculate Win Rate on Market level
var (winRateOverall, winRate30d, winRate7d, winRate24h) = CalculateMarketWinRates(trades, tempPositions, cutoff30d, cutoff7d, cutoff24h); var (winRateOverall, winRate30d, winRate7d, winRate24h) = CalculateMarketWinRates(trades, tempPositions, cutoff30d, cutoff7d, cutoff24h);