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");
|
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);
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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'} · ${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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
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)
|
.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);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user