feat: implement Part D and E from FIXPLAN

- D1/D2/D2c: Added TraderTraits entity, TraderTraitCalculator, Market Return Metrics (MedianWin, AvgWin, etc.), and trait filters
- D3: Implemented HF-Trader Tiering via IngestMode (Full, Aggregated, SnapshotOnly) and updated TradeHistoryWorker to respect tiers
- E1-E5: Added MasterStatus to Trader, TraderWindowMetrics for rolling analytics, Fingerprint metrics (PriceBandProfile, P50/P90), Copyability aggregates (Volume, Drift, Edge)
- E6: Implemented GET /api/traders/{id}/profile and GET /api/traders/correlation
- Replaced FIXPLAN-2026-07-09.md with FIXPLAN-TODO.md and FIXPLAN-DONE.md
- Cleaned up API docs and plan to use generic terms (removed hardcoded PolyTrader references)
- Added respective EF Core Migrations
This commit is contained in:
Richard
2026-07-14 09:04:31 +02:00
parent a1fcb4ace5
commit 16431f38a5
39 changed files with 9028 additions and 167 deletions
+48 -2
View File
@@ -292,6 +292,11 @@ async function loadTraders() {
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 = '<tr><td colspan="11"><div class="empty-state"><p>No traders tracked yet.</p></div></td></tr>'; return; }
@@ -333,7 +338,10 @@ async function loadTraders() {
<td>${fmt.pct(t.winRate)}</td>
<td>${fmt.pnl(t.totalPnl)}</td>
<td>${t.trades30d} | ${t.totalTrades}</td>
<td>${t.strategy}</td>
<td>
${t.strategy}
${t.traits ? '<div style="display:flex; flex-wrap:wrap; gap:4px; margin-top:4px;">' + t.traits.map(tr => `<span style="font-size:10px; padding:2px 6px; background:var(--bg-input); border-radius:10px;">${tr}</span>`).join('') + '</div>' : ''}
</td>
<td>
<div style="display:flex; gap:4px;">
<button class="btn-sm" onclick="viewTrader(${t.id})">Details</button>
@@ -451,6 +459,16 @@ async function viewTrader(id) {
document.getElementById('td-platformId').textContent = t.platformUserId;
document.getElementById('td-tier').innerHTML = fmt.tier(t.tier);
document.getElementById('td-strategy').textContent = t.strategy;
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 => `<span title="Value: ${Number(tr.value).toFixed(4)}" style="font-size:11px; padding:2px 8px; background:var(--bg-input); border-radius:12px; border:1px solid var(--border);">${tr.trait}</span>`).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);
@@ -460,6 +478,15 @@ async function viewTrader(id) {
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 = `<span class="side-buy">+${Number(medianWin).toFixed(1)}%</span> / <span class="side-sell">${Number(medianLoss).toFixed(1)}%</span>`;
document.getElementById('td-profit-factor').textContent = t.profitFactor ? Number(t.profitFactor).toFixed(2) : '—';
document.getElementById('td-expectancy').innerHTML = expectancy > 0 ? `<span class="side-buy">+${expectancy.toFixed(1)}%</span>` : `<span class="side-sell">${expectancy.toFixed(1)}%</span>`;
document.getElementById('td-ai-summary').textContent = t.aiStrategySummary || 'Not analyzed yet.';
const syncBtn = document.getElementById('btn-sync-trader');
@@ -722,4 +749,23 @@ async function queueBacklogAnalysis() {
}
// Initialize Dashboard
loadDashboard();
document.addEventListener('DOMContentLoaded', async () => {
// 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 = t;
filterTrait.appendChild(opt);
});
}
}
} catch (e) {
console.error('Failed to load traits', e);
}
loadDashboard();
});