Implement WinRate, Bankroll, Positions UI and add Trades30d to Analytics
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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'} · ${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);
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+1100
File diff suppressed because it is too large
Load Diff
+29
@@ -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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user