// Predictalytics Analytics — Dashboard Application const API_BASE = ''; // ─── Theme Toggle ─── const themeToggle = document.getElementById('themeToggle'); const html = document.documentElement; const savedTheme = localStorage.getItem('ba-theme') || 'dark'; html.setAttribute('data-theme', savedTheme); themeToggle.addEventListener('click', () => { const current = html.getAttribute('data-theme'); const next = current === 'dark' ? 'light' : 'dark'; html.setAttribute('data-theme', next); localStorage.setItem('ba-theme', next); updateChartColors(); }); // ─── Navigation ─── document.querySelectorAll('.nav-item[data-page]').forEach(item => { item.addEventListener('click', e => { e.preventDefault(); const page = item.dataset.page; document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active')); item.classList.add('active'); document.querySelectorAll('.page').forEach(p => p.classList.remove('active')); document.getElementById(`page-${page}`).classList.add('active'); if (page === 'traders') loadTraders(); if (page === 'alerts') loadAlerts(); if (page === 'markets') loadMarkets(); if (page === 'jobs') loadJobs(); if (page === 'watchlist') loadWatchlist(); }); }); let pageHistory = ['dashboard']; function navigateTo(pageId) { document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active')); document.querySelectorAll('.page').forEach(p => p.classList.remove('active')); const page = document.getElementById(`page-${pageId}`); if (page) { page.classList.add('active'); pageHistory.push(pageId); } } function navigateBack() { if (pageHistory.length > 1) { pageHistory.pop(); const prev = pageHistory[pageHistory.length - 1]; document.querySelectorAll('.page').forEach(p => p.classList.remove('active')); document.getElementById(`page-${prev}`).classList.add('active'); } } // ─── Search ─── const searchInput = document.getElementById('searchInput'); searchInput?.addEventListener('keypress', e => { if (e.key === 'Enter') { const query = searchInput.value.trim(); if (query) performSearch(query); } }); async function performSearch(query) { navigateTo('search'); document.getElementById('search-title').textContent = `Search Results for "${query}"`; const data = await api(`/api/search?q=${encodeURIComponent(query)}`); const tBody = document.getElementById('searchTradersBody'); const mBody = document.getElementById('searchMarketsBody'); if (!data) return; tBody.innerHTML = data.traders.map((t, i) => ` ${i + 1} ${t.displayName} ${t.platform} ${Number(t.combinedScore).toFixed(1)} ${fmt.tier(t.tier)} `).join('') || 'No traders found'; mBody.innerHTML = data.markets.map(m => ` ${m.platform} ${m.question.substring(0, 50)}... ${fmt.usd(m.volume)} ${m.isResolved ? 'Resolved' : 'Active'} `).join('') || 'No markets found'; } async function manualAddTrader() { const platform = document.getElementById('addPlatform').value; const wallet = document.getElementById('addWallet').value.trim(); if (!wallet) return; const res = await fetch(`/api/traders?platform=${platform}&wallet=${wallet}`, { method: 'POST' }); if (res.ok) { const data = await res.json(); alert('Trader added successfully! Sync will start shortly.'); document.getElementById('addWallet').value = ''; viewTrader(data.id); } else { alert('Failed to add trader.'); } } let currentPlatform = 'All'; let currentSort = 'default'; let sortDirection = -1; document.getElementById('platformSelect')?.addEventListener('change', (e) => { currentPlatform = e.target.value; refreshActivePage(); }); document.getElementById('sortSelect')?.addEventListener('change', (e) => { currentSort = e.target.value; refreshActivePage(); }); function refreshActivePage() { const activePage = document.querySelector('.page.active')?.id; if (activePage === 'page-dashboard') loadDashboard(); else if (activePage === 'page-traders') loadTraders(); else if (activePage === 'page-markets') loadMarkets(); else if (activePage === 'page-watchlist') loadWatchlist(); } // ─── API Helpers ─── async function api(endpoint, options = {}) { try { const res = await fetch(`${API_BASE}${endpoint}`, options); if (!res.ok) throw new Error(`HTTP ${res.status}`); const text = await res.text(); return text ? JSON.parse(text) : true; } catch (err) { console.error(`API Error [${endpoint}]:`, err); return null; } } // ─── Format Helpers ─── const fmt = { usd: v => { if (v === null || v === undefined) return '$0'; const n = Number(v); return n >= 1000000 ? `$${(n/1000000).toFixed(1)}M` : n >= 1000 ? `$${(n/1000).toFixed(1)}K` : `$${n.toFixed(0)}`; }, pct: v => { if (v === null || v === undefined) return '0%'; const n = Number(v); const color = n > 55 ? 'var(--pnl-positive)' : n < 45 ? 'var(--pnl-negative)' : 'var(--text)'; return `${n.toFixed(1)}%`; }, num: v => { if (v === null || v === undefined) return '0'; return Number(v).toLocaleString(); }, time: v => { if (!v) return '—'; const d = new Date(v); const now = new Date(); const diff = (now - d) / 1000; if (diff < 60) return `${Math.floor(diff)}s ago`; if (diff < 3600) return `${Math.floor(diff/60)}m ago`; if (diff < 86400) return `${Math.floor(diff/3600)}h ago`; return d.toLocaleDateString(); }, tier: t => { if (!t) return '—'; const cls = `tier-${t.toString().toLowerCase()}`; return `${t}`; }, side: s => { if (!s) return '—'; return `${s}`; }, pnl: v => { if (v === null || v === undefined) return '$0'; const n = Number(v); return `${fmt.usd(Math.abs(n))}${n >= 0 ? ' ▲' : ' ▼'}`; } }; // ─── Chart instances ─── let platformChart, tierChart; function getChartColors() { return { text: '#8B93A7', grid: 'rgba(255,255,255,0.07)', bg: 'rgba(255,255,255,0.045)' }; } function getAvatarBg(id) { const gradients = [ 'linear-gradient(135deg,#1652F0,#4c8dff)', 'linear-gradient(135deg,#12D48A,#0a8f5f)', 'linear-gradient(135deg,#7c5cff,#4c8dff)', 'linear-gradient(135deg,#ff9d4c,#ff6b8a)', 'linear-gradient(135deg,#F6465D,#ff6b8a)' ]; return gradients[id % gradients.length]; } function getTraitClass(trait) { if (!trait) return 'tier-unknown'; const lower = trait.toLowerCase(); if (lower.includes('winrate') || lower.includes('wins') || lower.includes('farming') || lower.includes('insider')) return 'trait-winrate'; if (lower.includes('volume') || lower.includes('amounts') || lower.includes('sizes') || lower.includes('stake')) return 'trait-volume'; if (lower.includes('whale') || lower.includes('scalper') || lower.includes('martingale')) return 'trait-whales'; if (lower.includes('sentiment') || lower.includes('cadence') || lower.includes('24_7') || lower.includes('copyable') || lower.includes('wallet')) return 'trait-sentiment'; return 'tier-unknown'; } const TraitMetadata = { "sub_second_cadence": { name: "Sub-Second Cadence", desc: "Executes trades in sub-second intervals, highly indicative of algorithmic execution." }, "always_on_24_7": { name: "24/7 Activity", desc: "Trades at all hours of the day and night with very short gaps, indicating bot operation." }, "uniform_sizes": { name: "Uniform Sizes", desc: "Executes trades with identical position sizes, suggesting a systematic layout." }, "round_amounts": { name: "Round Amounts", desc: "Executes trades with round amounts (e.g. 100, 500, 1000 USD), common for manual traders." }, "uses_split_merge": { name: "Split & Merge", desc: "Splits large positions into smaller orders or merges multiple positions to optimize slippage." }, "both_sides_same_market": { name: "Two-Sided Market Maker", desc: "Buys and sells in the same market, extracting spreads or resolving inventory." }, "resolution_farming": { name: "Resolution Farmer", desc: "Buys options at high probabilities (e.g., >93%) to collect predictable small returns." }, "longshot_buyer": { name: "Longshot Buyer", desc: "Buys options at low probabilities (e.g., <10%), seeking rare high-payoff events." }, "scalper": { name: "Scalper", desc: "Holds positions for very short periods (typically <1 hour) to lock in quick profits." }, "holds_to_resolution": { name: "Holds to Resolution", desc: "Keeps positions open until the market officially resolves, avoiding early exits." }, "fresh_wallet": { name: "Fresh Wallet", desc: "Wallet was created recently (less than 30 days of active history)." }, "stable_stake_fraction": { name: "Stable Stake", desc: "Risk per trade is a highly stable fraction of estimated total bankroll." }, "possible_insider": { name: "Possible Insider", desc: "Consistently wins low-probability bets with very low volume, indicating asymmetric information." }, "thin_margin_wins": { name: "Thin Margin Winner", desc: "Consistently resolves trades with small margins of win (low ROI)." }, "high_payoff_wins": { name: "High Payoff Winner", desc: "Consistently resolves trades with high margins of win (high ROI)." }, "sells_at_loss": { name: "Sells at Loss", desc: "Uses strict stop-loss rules, selling options at a loss when the market goes against them." }, "days_active": { name: "Days Active", desc: "The number of days this wallet has been active on-chain." }, "trades_last_30_days": { name: "Trades Last 30d", desc: "The total number of trades executed in the last 30 days." }, "martingale_pattern": { name: "Martingale Trader", desc: "Increases bet size after losses to attempt to recoup losses, typical of high-risk strategies." }, "not_copyable_hf": { name: "Uncopyable (High Freq)", desc: "Trades at frequencies too high to replicate manually or with standard copytrading setups." } }; function getTraitDisplayName(traitKey) { const meta = TraitMetadata[traitKey]; return meta ? meta.name : traitKey.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase()); } function getTraitDescription(traitKey) { const meta = TraitMetadata[traitKey]; return meta ? meta.desc : 'No description available.'; } // ─── Dashboard Load ─── async function loadDashboard() { const data = await api('/api/dashboard'); if (!data) { document.getElementById('metricTraders').textContent = '0'; document.getElementById('metricActive').textContent = '0'; document.getElementById('metricTrades').textContent = '0'; document.getElementById('metricVolume').textContent = '$0'; return; } // Metrics document.getElementById('metricTraders').textContent = fmt.num(data.totalTraders); document.getElementById('metricActive').textContent = fmt.num(data.activeTraders24h); document.getElementById('metricTrades').textContent = fmt.num(data.totalTrades); document.getElementById('metricVolume').textContent = fmt.usd(data.totalVolume24h); // Alert badge const badge = document.getElementById('alertBadge'); if (data.unreadAlerts > 0) { badge.textContent = data.unreadAlerts; badge.style.display = 'inline'; } else { badge.style.display = 'none'; } // Top Traders table const tbody = document.getElementById('topTradersBody'); tbody.innerHTML = data.topTraders.map((t, i) => ` ${i + 1} ${t.displayName} ${t.platform} ${Number(t.combinedScore).toFixed(1)} ${fmt.pct(t.winRate)} ${fmt.pnl(t.totalPnl)} ${t.trades30d} | ${t.totalTrades} `).join(''); // Recent Trades table const rBody = document.getElementById('recentTradesBody'); rBody.innerHTML = data.recentTrades.map(t => ` ${fmt.time(t.executedAt)} ${t.traderName} ${fmt.side(t.side)} ${Number(t.price).toFixed(2)} ${fmt.num(t.size)} `).join(''); const headerVol = document.getElementById('headerVolumeBadge'); if (headerVol) { headerVol.textContent = `24H VOL: ${fmt.usd(data.volume24h)}`; } // Platform Chart const cc = getChartColors(); const pLabels = Object.keys(data.platformBreakdown.traderCounts); const pData = Object.values(data.platformBreakdown.traderCounts); if (platformChart) platformChart.destroy(); platformChart = new Chart(document.getElementById('platformChart'), { type: 'doughnut', data: { labels: pLabels.length ? pLabels : ['Keine Daten'], datasets: [{ data: pData.length ? pData : [1], backgroundColor: ['#1652F0', '#12D48A', '#7c5cff', '#ff9d4c', '#F6465D', '#5b9dff'], borderWidth: 0 }] }, options: { responsive: true, maintainAspectRatio: false, cutout: '70%', plugins: { legend: { position: 'bottom', labels: { color: cc.text, padding: 16, font: { family: "'Manrope'", size: 12 } } } } } }); // Tier chart const tierData = data.topTraders.reduce((acc, t) => { acc[t.tier] = (acc[t.tier] || 0) + 1; return acc; }, {}); if (tierChart) tierChart.destroy(); const tLabels = Object.keys(tierData).length ? Object.keys(tierData) : ['Keine Daten']; const tData = Object.values(tierData).length ? Object.values(tierData) : [1]; tierChart = new Chart(document.getElementById('tierChart'), { type: 'bar', data: { labels: tLabels, datasets: [{ label: 'Trader', data: tData, backgroundColor: '#1652F0', borderRadius: 6, barThickness: 32 }] }, options: { responsive: true, maintainAspectRatio: false, scales: { x: { grid: { display: false }, ticks: { color: cc.text, font: { family: "'Manrope'" } } }, y: { grid: { color: cc.grid }, ticks: { color: cc.text, font: { family: "'Manrope'" } } } }, plugins: { legend: { display: false } } } }); } function setTraderSort(field) { if (currentSort === field) { sortDirection *= -1; } else { currentSort = field; sortDirection = -1; // Default to descending } // Update tradersSort dropdown if it exists to match const sortSelect = document.getElementById('tradersSort'); if (sortSelect) sortSelect.value = currentSort; loadTraders(); } async function loadTraders() { let url = '/api/traders?skip=0&take=100'; if (currentPlatform !== 'All') url += `&platform=${currentPlatform}`; const hcCheckbox = document.getElementById('chk-highly-copyable'); if (hcCheckbox && hcCheckbox.checked) { url += `&highlyCopyable=true`; } const filterTrait = document.getElementById('filterTrait'); if (filterTrait && filterTrait.value) { url += `&trait=${encodeURIComponent(filterTrait.value)}`; } let data = await api(url); const tbody = document.getElementById('allTradersBody'); if (!data || !data.length) { tbody.innerHTML = '

No traders tracked yet.

'; return; } const sortSelect = document.getElementById('tradersSort'); if (sortSelect && sortSelect.value !== currentSort) currentSort = sortSelect.value; const filterWinrateMin = parseFloat(document.getElementById('filterWinrateMin')?.value); const filterCopyabilityMin = parseFloat(document.getElementById('filterCopyabilityMin')?.value); // Filtering if (!isNaN(filterWinrateMin)) data = data.filter(t => t.winRate >= filterWinrateMin); if (!isNaN(filterCopyabilityMin)) data = data.filter(t => t.copytradingCopyabilityScore >= filterCopyabilityMin); // Sorting data.sort((a, b) => { let valA, valB; if (currentSort === 'score') { valA = a.combinedScore; valB = b.combinedScore; } else if (currentSort === 'quality') { valA = a.copytradingQualityScore || 0; valB = b.copytradingQualityScore || 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 === '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; } return (valA < valB ? -1 : valA > valB ? 1 : 0) * sortDirection; }); tbody.innerHTML = data.map((t, i) => ` ${i + 1} ${t.displayName}${t.isSuspectedBot ? ' 🤖' : ''} ${t.platform} ${Number(t.combinedScore).toFixed(1)} ${Number(t.copytradingQualityScore || 0).toFixed(1)} ${Number(t.copytradingCopyabilityScore || 0).toFixed(1)} ${fmt.pct(t.winRate)} ${fmt.pnl(t.totalPnl)} ${t.trades30d} | ${t.totalTrades} ${t.strategy || '—'} ${t.traits ? '
' + t.traits.map(tr => { const rawKey = tr.trait || tr; const name = getTraitDisplayName(rawKey); const desc = getTraitDescription(rawKey); return `${name}`; }).join('') + '
' : ''} `).join(''); } async function loadWatchlist() { const tbody = document.getElementById('watchlistBody'); const data = await api(`/api/watchlist`); if (!Array.isArray(data)) { // api() returns null on HTTP errors — show it instead of a silently empty page. tbody.innerHTML = '⚠ Failed to load watchlist (see browser console / server log).'; return; } tbody.innerHTML = data.map(w => `
${(w.displayName || '?').substring(0, 2).toUpperCase()}
${w.displayName || '?'}
${(w.platformUserId || '').substring(0, 8)}...
${w.platform ?? 'Unknown'} ${Number(w.copytradingScore || 0).toFixed(1)} ${fmt.pct(w.winRate)} ${fmt.pnl(w.totalPnl)} ${w.label || ''} ${new Date(w.createdAt).toLocaleDateString()} `).join('') || 'Your watchlist is empty.'; } async function removeFromWatchlist(id) { if (confirm('Remove this trader from watchlist?')) { await api(`/api/traders/${id}/watchlist`, { method: 'DELETE' }); loadWatchlist(); } } // ─── Alerts Page ─── async function loadAlerts() { const data = await api('/api/alerts?count=50'); const el = document.getElementById('alertsList'); if (!data || !data.length) { el.innerHTML = '

No alerts yet.

'; return; } el.innerHTML = data.map(a => `
🔔
${a.title}
${a.message}
${fmt.time(a.createdAt)}
`).join(''); } // ─── Markets Page ─── async function loadMarkets() { let url = '/api/markets?skip=0&take=100'; if (currentPlatform !== 'All') url += `&platform=${currentPlatform}`; const catSelect = document.getElementById('marketsCategory'); if (catSelect && catSelect.value !== 'All') { url += `&category=${catSelect.value}`; } const searchInput = document.getElementById('marketsSearchInput'); if (searchInput && searchInput.value) { url += `&query=${encodeURIComponent(searchInput.value)}`; } let data = await api(url); const tbody = document.getElementById('allMarketsBody'); if (!data || !data.length) { tbody.innerHTML = '

No markets found.

'; return; } // Sorting if (currentSort === 'score') data.sort((a, b) => b.volume - a.volume); else if (currentSort === 'name') data.sort((a, b) => (a.endDate || '').localeCompare(b.endDate || '')); else if (currentSort === 'pnl') data.sort((a, b) => b.liquidity - a.liquidity); tbody.innerHTML = data.map(m => ` ${m.platform} ${m.question.length > 60 ? m.question.substring(0, 60) + '...' : m.question} ${fmt.usd(m.volume)} ${fmt.usd(m.liquidity)} ${m.endDate ? new Date(m.endDate).toLocaleDateString() : '—'} ${m.isResolved ? 'Resolved' : 'Active'} `).join(''); } async function viewTrader(id) { window.currentTraderId = id; navigateTo('trader-detail'); const t = await api(`/api/traders/${id}`); if (!t) return; // Reset tabs to default (Analytics) switchTraderTab('td-tab-analytics'); document.getElementById('td-name').textContent = `Trader-Profil: ${t.displayName}`; document.getElementById('td-displayName').textContent = t.displayName; document.getElementById('td-platform').textContent = t.platform; document.getElementById('td-platformId').textContent = t.platformUserId; document.getElementById('td-tier').innerHTML = fmt.tier(t.tier); document.getElementById('td-strategy').textContent = t.strategy; const avatarEl = document.getElementById('td-avatar'); if (avatarEl) { avatarEl.textContent = t.displayName ? t.displayName[0].toUpperCase() : 'P'; avatarEl.style.background = getAvatarBg(id); } const traitsContainer = document.getElementById('td-traits-container'); const traitsEl = document.getElementById('td-traits'); if (t.traits && t.traits.length > 0) { traitsContainer.style.display = 'block'; traitsEl.innerHTML = t.traits.map(tr => { const rawKey = tr.trait || tr; const name = getTraitDisplayName(rawKey); const desc = getTraitDescription(rawKey); const scoreVal = tr.value !== undefined ? ` (Value: ${Number(tr.value).toFixed(4)})` : ''; return `${name}`; }).join(''); } else { traitsContainer.style.display = 'none'; traitsEl.innerHTML = ''; } 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); const winRateDec = t.winRate / 100; const medianWin = t.medianWinReturnPct || 0; const medianLoss = t.medianLossReturnPct || 0; const expectancy = (winRateDec * medianWin) - ((1 - winRateDec) * Math.abs(medianLoss)); document.getElementById('td-median-win-loss').innerHTML = `+${Number(medianWin).toFixed(1)}% / ${Number(medianLoss).toFixed(1)}%`; document.getElementById('td-profit-factor').textContent = t.profitFactor ? Number(t.profitFactor).toFixed(2) : '—'; document.getElementById('td-expectancy').innerHTML = expectancy > 0 ? `+${expectancy.toFixed(1)}%` : `${expectancy.toFixed(1)}%`; document.getElementById('td-ai-summary').textContent = t.aiStrategySummary || 'Bisher keine KI-Strategieanalyse durchgeführt.'; const syncBtn = document.getElementById('btn-sync-trader'); if (syncBtn) { syncBtn.onclick = () => queueHistorySync(id); } const deepSyncBtn = document.getElementById('btn-deep-resync-trader'); if (deepSyncBtn) { deepSyncBtn.onclick = () => { if (confirm("Bist du sicher? Dies löscht alle komprimierten Trades, setzt die Positionen zurück und lädt den gesamten Verlauf neu.")) { queueDeepResync(id); } }; } const analyzeBtn = document.getElementById('btn-analyze-trader'); if (analyzeBtn) { analyzeBtn.onclick = () => queueTraderAnalysis(id); } const aiBtn = document.getElementById('btn-ai-analysis'); if (window.capabilities && !window.capabilities.canControl) { aiBtn.style.display = 'none'; } else { aiBtn.style.display = 'inline-block'; aiBtn.onclick = () => triggerAiAnalysis(id, true); } const wlBtn = document.getElementById('btn-toggle-watchlist'); wlBtn.textContent = t.isOnWatchlist ? 'Watchlist (Remove)' : 'Watchlist (Add)'; wlBtn.onclick = async () => { const method = t.isOnWatchlist ? 'DELETE' : 'POST'; await api(`/api/traders/${id}/watchlist`, { method }); viewTrader(id); // Reload to update UI }; const openBtn = document.getElementById('btn-open-platform'); if (t.platform === 'Polymarket') { openBtn.style.display = 'inline-block'; openBtn.textContent = 'Open on Polymarket'; openBtn.onclick = () => window.open(`https://polymarket.com/profile/${t.platformUserId}`, '_blank'); } else { openBtn.style.display = 'none'; } const catBody = document.getElementById('td-categoryBody'); if (t.categoryPerformances && t.categoryPerformances.length > 0) { catBody.innerHTML = t.categoryPerformances.map(c => ` ${c.category} ${c.subcategory || '-'} ${fmt.pct(c.winRate)} ${fmt.pnl(c.totalPnL)} ${fmt.usd(c.totalVolume)} ${(c.avgReturnPct || 0).toFixed(1)}% ${fmt.num(c.totalTrades)} `).join(''); } else { catBody.innerHTML = 'No category data available'; } const tbody = document.getElementById('td-tradesBody'); tbody.innerHTML = t.recentTrades.map(tr => ` ${fmt.time(tr.executedAt)} ${tr.marketName || (tr.marketId.length > 20 ? tr.marketId.substring(0,20)+'...' : tr.marketId)} ${fmt.side(tr.side)} ${Number(tr.price).toFixed(2)} ${fmt.num(tr.size)} ${fmt.usd(tr.amount)} `).join(''); loadTraderPositions(id); } async function loadTraderPositions(id) { const tbody = document.getElementById('td-positionsBody'); tbody.innerHTML = 'Loading...'; try { const data = await api(`/api/traders/${id}/positions`); if (!data || !data.length) { tbody.innerHTML = '

No active positions.

'; return; } tbody.innerHTML = data.map(p => `
${p.marketName || p.marketId}
${p.category || 'Other'} · ${p.outcomeToken || 'Unknown'}
${fmt.num(p.sharesHeld)} ${Number(p.avgCost).toFixed(2)} ${Number(p.currentPrice).toFixed(2)} ${fmt.pnl(p.realizedPnl)} ${fmt.pnl(p.unrealizedPnl)} `).join(''); } catch (e) { tbody.innerHTML = 'Error loading positions.'; } } function switchTraderTab(tabId) { // Hide all tabs document.querySelectorAll('.tab-content').forEach(el => el.style.display = 'none'); // Deactivate all buttons document.querySelectorAll('.btn-tab').forEach(el => el.classList.remove('active')); // Show active tab document.getElementById(tabId).style.display = 'block'; // Activate clicked button const btn = document.querySelector(`.btn-tab[data-tab="${tabId}"]`); if(btn) btn.classList.add('active'); } async function viewMarket(id) { navigateTo('market-detail'); const m = await api(`/api/markets/${id}`); if (!m) return; document.getElementById('md-question').textContent = m.question; document.getElementById('md-platform').textContent = m.platform; document.getElementById('md-category').textContent = m.category; document.getElementById('md-ends').textContent = m.endDate ? new Date(m.endDate).toLocaleDateString() : 'Never'; document.getElementById('md-volume').textContent = fmt.usd(m.volume); document.getElementById('md-liquidity').textContent = fmt.usd(m.liquidity); document.getElementById('md-status').textContent = m.isResolved ? 'Resolved' : 'Active'; document.getElementById('md-bot-activity').textContent = m.botActivityScore ? Number(m.botActivityScore).toFixed(1) : '0'; document.getElementById('md-unique-traders').textContent = fmt.num(m.uniqueTradersCount); document.getElementById('md-avg-trade-size').textContent = fmt.usd(m.averageTradeSize); const imgContainer = document.getElementById('md-image'); if (m.imageUrl) imgContainer.innerHTML = `Market`; else imgContainer.innerHTML = ''; const oList = document.getElementById('md-outcomes'); oList.innerHTML = m.outcomes.map(o => `
${o.name} ${(o.price * 100).toFixed(1)}¢
`).join(''); const tbody = document.getElementById('md-tradesBody'); tbody.innerHTML = m.recentTrades.map(tr => ` ${fmt.time(tr.executedAt)} ${tr.traderName} ${fmt.side(tr.side)} ${Number(tr.price).toFixed(2)} ${fmt.num(tr.size)} ${fmt.usd(tr.amount)} `).join(''); } async function viewMarketByQuestion(question) { // Legacy helper: search markets by question text const data = await api(`/api/search?q=${encodeURIComponent(question)}`); if (data && data.markets.length > 0) { viewMarket(data.markets[0].id); } } function viewMarketById(dbMarketId) { if (dbMarketId) viewMarket(dbMarketId); } // ─── Initial Load ─── loadDashboard(); // Auto-refresh every 30 seconds setInterval(() => { const activePage = document.querySelector('.page.active'); if (activePage?.id === 'page-dashboard') loadDashboard(); }, 30000); async function triggerAiAnalysis(id, manual) { const btn = document.getElementById('btn-ai-analysis'); const oldText = btn.textContent; btn.textContent = 'Analyzing...'; btn.disabled = true; try { const res = await api(`/api/traders/${id}/ai-analysis?manual=${manual}`, { method: 'POST' }); if (res && res.summary) { document.getElementById('td-ai-summary').textContent = res.summary; } else { alert('Analysis failed or returned empty.'); } } catch (e) { console.error(e); alert('Analysis error: ' + e); } finally { btn.textContent = oldText; btn.disabled = false; } } // ─── Jobs Page ─── async function loadJobs() { const data = await api('/api/jobs'); const tbody = document.getElementById('jobsBody'); if (!data || !data.length) { tbody.innerHTML = '

No jobs found.

'; return; } tbody.innerHTML = data.map(j => ` ${j.id} ${j.jobType} ${j.status === 'Completed' ? 'Completed' : j.status === 'Failed' ? 'Failed' : j.status} ${j.traderName || j.traderId} ${fmt.time(j.createdAt)} ${j.startedAt ? fmt.time(j.startedAt) : '—'} ${j.completedAt ? fmt.time(j.completedAt) : '—'} ${j.errorMessage || '—'} `).join(''); } async function queueHistorySync(id) { const res = await fetch(`/api/jobs/sync/${id}`, { method: 'POST' }); if (res.ok) { alert('History Sync job queued successfully.'); } else { alert('Failed to queue history sync.'); } } async function queueDeepResync(id) { const res = await fetch(`/api/jobs/deep-resync/${id}`, { method: 'POST' }); if (res.ok) { alert('Deep Resync job queued successfully.'); } else { alert('Failed to queue deep resync.'); } } async function queueTraderAnalysis(id) { const res = await fetch(`/api/jobs/analyze/${id}`, { method: 'POST' }); if (res.ok) { alert('Trader Analysis job queued successfully.'); } else { alert('Failed to queue trader analysis.'); } } async function queueBacklogAnalysis() { const res = await fetch(`/api/jobs/analyze-backlog?take=50`, { method: 'POST' }); if (res.ok) { const data = await res.json(); alert(`Successfully queued ${data.queued} traders for analysis from backlog.`); loadJobs(); } else { alert('Failed to queue backlog analysis.'); } } // Initialize Dashboard document.addEventListener('DOMContentLoaded', async () => { // Load capabilities first try { const caps = await api('/api/capabilities'); if (caps) { window.capabilities = caps; if (!caps.canControl) { // Hide control-mode guarded elements const jobsNav = document.getElementById('nav-jobs-container'); if (jobsNav) jobsNav.style.display = 'none'; const addTraderPanel = document.getElementById('control-add-trader-panel'); if (addTraderPanel) addTraderPanel.style.display = 'none'; const traderActions = document.getElementById('control-trader-actions'); if (traderActions) traderActions.style.display = 'none'; } } } catch (e) { console.error('Failed to load capabilities', e); } // Load traits for filter try { const traits = await api('/api/traders/traits'); if (traits && Array.isArray(traits)) { const filterTrait = document.getElementById('filterTrait'); if (filterTrait) { traits.forEach(t => { const opt = document.createElement('option'); opt.value = t; opt.textContent = getTraitDisplayName(t); filterTrait.appendChild(opt); }); } } } catch (e) { console.error('Failed to load traits', e); } loadDashboard(); });