SiddharthVenba's picture
Initial commit for HF Space
75d9b3c
Raw
History Blame Contribute Delete
15.9 kB
document.addEventListener('DOMContentLoaded', () => {
// --- Elements ---
const btnScanUS = document.getElementById('btnScanUS');
const btnScanIN = document.getElementById('btnScanIN');
const signalsTableBody = document.getElementById('signalsTableBody');
const lastUpdated = document.getElementById('lastUpdated');
const pipelineFunnel = document.getElementById('pipelineFunnel');
const pipelineDetails = document.getElementById('pipelineDetails');
const regimeBanner = document.getElementById('regimeBanner');
const scanProgress = document.getElementById('scanProgress');
const progressStep = document.getElementById('progressStep');
const progressBar = document.getElementById('progressBar');
// Stats
const statUniverse = document.getElementById('statUniverse');
const statRaw = document.getElementById('statRaw');
const statFiltered = document.getElementById('statFiltered');
const statFinal = document.getElementById('statFinal');
const statDuration = document.getElementById('statDuration');
// Chat
const chatFab = document.getElementById('chatFab');
const chatPanel = document.getElementById('chatPanel');
const chatClose = document.getElementById('chatClose');
const chatInput = document.getElementById('chatInput');
const chatSend = document.getElementById('chatSend');
const chatMessages = document.getElementById('chatMessages');
const API_BASE = '/api';
const fmt = (n) => n != null ? Number(n).toLocaleString() : '0';
const fmtPrice = (n) => n ? parseFloat(n).toFixed(2) : '-';
// --- Pipeline Rendering ---
const renderPipeline = (stages) => {
if (!stages || stages.length === 0) {
pipelineFunnel.innerHTML = `<div style="text-align:center; padding:40px; color:var(--text-muted); width:100%;">
<i class="fa-solid fa-play" style="margin-right:8px;"></i> Run a scan to see the filtering pipeline
</div>`;
return;
}
let html = '';
stages.forEach((stage, i) => {
const stateClass = stage.count_out === 0 ? 'empty' : (i === stages.length - 1 ? 'active' : 'completed');
const timeStr = stage.duration_ms > 0 ? `${(stage.duration_ms / 1000).toFixed(1)}s` : '';
html += `
<div class="pipeline-stage ${stateClass}" data-stage-idx="${i}" onclick="window._showStageDetails(${i})">
<div class="stage-icon"><i class="fa-solid ${stage.icon}"></i></div>
<div class="stage-label">${stage.label}</div>
<div class="stage-count" style="animation-delay:${i * 0.08}s">${stage.count_out}</div>
${timeStr ? `<div class="stage-time">${timeStr}</div>` : ''}
</div>`;
if (i < stages.length - 1) {
const dropPct = stage.count_in > 0 ? Math.round((1 - stage.count_out / stage.count_in) * 100) : 0;
html += `<div class="stage-arrow"><i class="fa-solid fa-chevron-right"></i></div>`;
}
});
pipelineFunnel.innerHTML = html;
};
// Stage details panel
let _pipelineStages = [];
window._showStageDetails = (idx) => {
const stage = _pipelineStages[idx];
if (!stage) return;
// Toggle active class
document.querySelectorAll('.pipeline-stage').forEach((el, i) => {
el.classList.toggle('active', i === idx);
});
const dropCount = stage.count_in - stage.count_out;
const dropPct = stage.count_in > 0 ? ((dropCount / stage.count_in) * 100).toFixed(1) : 0;
let tickerHtml = '';
if (stage.tickers && stage.tickers.length > 0) {
tickerHtml = '<div class="details-tickers">' +
stage.tickers.slice(0, 30).map(t => `<span class="details-ticker-tag passed">${t}</span>`).join('') +
(stage.tickers.length > 30 ? `<span class="details-ticker-tag">+${stage.tickers.length - 30} more</span>` : '') +
'</div>';
}
let rejectedHtml = '';
if (stage.rejected && stage.rejected.length > 0) {
rejectedHtml = '<div class="rejected-list"><div style="font-size:0.75rem;color:var(--accent-rose);font-weight:600;margin-bottom:6px;"><i class="fa-solid fa-xmark"></i> Rejected:</div>' +
stage.rejected.slice(0, 10).map(r =>
`<div class="rejected-item"><span class="ticker">${r.ticker}</span><span class="reason">${r.reason || r.signal_type || ''}</span></div>`
).join('') +
(stage.rejected.length > 10 ? `<div style="font-size:0.7rem;color:var(--text-muted);padding:4px 0;">+${stage.rejected.length - 10} more</div>` : '') +
'</div>';
}
pipelineDetails.innerHTML = `
<div class="details-header">
<div class="details-title"><i class="fa-solid ${stage.icon}"></i> ${stage.label}</div>
<div class="details-stats">
<div class="stat"><i class="fa-solid fa-arrow-right-to-bracket"></i> In: ${stage.count_in}</div>
<div class="stat"><i class="fa-solid fa-arrow-right-from-bracket"></i> Out: ${stage.count_out}</div>
${dropCount > 0 ? `<div class="stat" style="color:var(--accent-rose)"><i class="fa-solid fa-filter-circle-xmark"></i> Dropped: ${dropCount} (${dropPct}%)</div>` : ''}
${stage.duration_ms > 0 ? `<div class="stat"><i class="fa-solid fa-clock"></i> ${(stage.duration_ms / 1000).toFixed(1)}s</div>` : ''}
</div>
</div>
<div class="details-body">${stage.details}</div>
${tickerHtml}
${rejectedHtml}
`;
pipelineDetails.classList.add('visible');
};
// --- Regime Banner ---
const renderRegimes = (regimes) => {
if (!regimes || Object.keys(regimes).length === 0) {
regimeBanner.style.display = 'none';
return;
}
regimeBanner.style.display = 'flex';
let html = '';
for (const [mkt, r] of Object.entries(regimes)) {
const trendClass = r.trend === 'bullish' ? 'bullish' : (r.trend === 'bearish' ? 'bearish' : 'neutral');
const trendIcon = r.trend === 'bullish' ? 'fa-arrow-trend-up' : (r.trend === 'bearish' ? 'fa-arrow-trend-down' : 'fa-arrows-left-right');
html += `
<div class="regime-card">
<div class="regime-card-title"><i class="fa-solid fa-landmark"></i> ${mkt.toUpperCase()} Market Regime</div>
<span class="regime-badge ${trendClass}"><i class="fa-solid ${trendIcon}"></i> ${(r.regime || '').replace(/_/g, ' ').toUpperCase()}</span>
<div class="regime-info">
Trend: <strong>${r.trend}</strong> | Volatility: ${r.volatility_state} | VIX: ${r.vix || 'N/A'} | Breadth: ${r.breadth || 'N/A'}<br>
${r.description || ''}
</div>
</div>`;
}
regimeBanner.innerHTML = html;
};
// --- Stats ---
const updateStats = (stats) => {
if (!stats) return;
statUniverse.innerText = fmt(stats.universe_size);
statRaw.innerText = fmt(stats.raw_signals);
statFiltered.innerText = fmt(stats.risk_filtered || stats.news_filtered || stats.regime_filtered || stats.raw_signals);
statFinal.innerText = fmt(stats.final_signals);
statDuration.innerText = stats.duration_seconds ? `${stats.duration_seconds}s` : '-';
};
// --- Signals Table ---
const updateTable = (signals) => {
if (!signals || signals.length === 0) {
signalsTableBody.innerHTML = `<tr><td colspan="12" style="text-align:center; padding:40px; color:var(--text-muted)">No opportunities found in the last scan.</td></tr>`;
return;
}
signalsTableBody.innerHTML = signals.map((s, i) => {
const sector = (s.fundamental_data && s.fundamental_data.sector) || '';
const name = (s.fundamental_data && s.fundamental_data.name) || s.ticker;
const narrative = s.narrative || s.reason || '';
const reasonSummary = s.reason_summary || s.reason || '';
const alphaColor = (s.alpha_score || 0) >= 70 ? 'gold' : ((s.alpha_score || 0) >= 50 ? 'green' : '');
return `
<tr class="signal-row" data-idx="${i}" onclick="window._toggleNarrative(${i})">
<td><span class="expand-indicator"><i class="fa-solid fa-chevron-right"></i></span></td>
<td><div class="ticker-cell"><span class="ticker-name">${s.ticker}</span><span class="ticker-sector">${name !== s.ticker ? name : ''} ${sector ? '· ' + sector : ''}</span></div></td>
<td><span class="badge">${(s.signal_type || '').replace(/_/g, ' ').toUpperCase()}</span></td>
<td><span class="badge ${alphaColor}">${s.alpha_score || '-'}</span></td>
<td class="mono">${fmtPrice(s.entry_price)}</td>
<td class="mono positive">${fmtPrice(s.target_1)}</td>
<td class="mono positive">${fmtPrice(s.target_2)}</td>
<td class="mono positive">${fmtPrice(s.target_3)}</td>
<td class="mono negative">${fmtPrice(s.stop_loss)}</td>
<td class="mono">${s.risk_reward || '-'}</td>
<td class="mono">${s.quantity || '-'}</td>
<td style="font-size:0.78rem; color:var(--text-muted); max-width:200px;">${reasonSummary}</td>
</tr>
<tr class="narrative-row" id="narrative-${i}" style="display:none;">
<td colspan="12"><div class="narrative-container">${narrative}</div></td>
</tr>`;
}).join('');
};
window._toggleNarrative = (idx) => {
const row = document.getElementById(`narrative-${idx}`);
const signalRow = document.querySelector(`.signal-row[data-idx="${idx}"]`);
if (!row) return;
const isVisible = row.style.display !== 'none';
row.style.display = isVisible ? 'none' : 'table-row';
signalRow.classList.toggle('expanded', !isVisible);
};
// --- Load Dashboard ---
const loadDashboard = async () => {
try {
const res = await fetch(`${API_BASE}/dashboard?t=${Date.now()}`);
const data = await res.json();
if (data.status === 'success') {
updateStats(data.stats);
updateTable(data.signals);
if (data.pipeline_stages) {
_pipelineStages = data.pipeline_stages;
renderPipeline(data.pipeline_stages);
}
renderRegimes(data.regimes);
if (data.last_scan_date) {
lastUpdated.innerText = `Last Scan: ${new Date(data.last_scan_date).toLocaleString()}`;
}
}
} catch (err) { console.error('Failed to load dashboard:', err); }
};
// --- Scan Progress Simulation ---
const pipelineStepNames = [
'Determining universe...', 'Computing technical features...', 'Detecting signals...',
'Analyzing market regime...', 'Running fundamental checks...', 'Fetching news & sentiment...',
'Applying risk filters...', 'Sizing positions...', 'Scoring & ranking...', 'Building trade plans...'
];
let progressInterval = null;
const showProgress = () => {
scanProgress.classList.add('active');
let step = 0;
progressStep.innerText = pipelineStepNames[0];
progressBar.style.width = '5%';
progressInterval = setInterval(() => {
step++;
if (step < pipelineStepNames.length) {
progressStep.innerText = pipelineStepNames[step];
progressBar.style.width = `${(step / pipelineStepNames.length) * 90 + 5}%`;
}
}, 3000);
};
const hideProgress = () => {
progressBar.style.width = '100%';
setTimeout(() => {
scanProgress.classList.remove('active');
if (progressInterval) clearInterval(progressInterval);
progressBar.style.width = '0%';
}, 400);
};
// --- Trigger Scans ---
const triggerScan = async (market) => {
const btn = market === 'us' ? btnScanUS : btnScanIN;
const originalText = btn.innerHTML;
btn.innerHTML = `<i class="fa-solid fa-spinner fa-spin"></i> Scanning...`;
btn.disabled = true;
btnScanUS.disabled = true;
btnScanIN.disabled = true;
showProgress();
try {
const res = await fetch(`${API_BASE}/scan`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ market, top_n: 10 })
});
const data = await res.json();
hideProgress();
if (data.status === 'success') {
updateStats(data.stats);
if (data.pipeline_stages) {
_pipelineStages = data.pipeline_stages;
renderPipeline(data.pipeline_stages);
}
renderRegimes(data.regimes);
updateTable(data.signals);
lastUpdated.innerText = `Last Scan: ${new Date().toLocaleString()}`;
} else {
alert('Scan failed: ' + (data.message || 'Unknown error'));
}
} catch (err) {
hideProgress();
console.error('Scan error:', err);
alert('Scan error. Check console.');
} finally {
btn.innerHTML = originalText;
btn.disabled = false;
btnScanUS.disabled = false;
btnScanIN.disabled = false;
}
};
btnScanUS.addEventListener('click', () => triggerScan('us'));
btnScanIN.addEventListener('click', () => triggerScan('india'));
// --- Chat Logic ---
const toggleChat = () => chatPanel.classList.toggle('open');
chatFab.addEventListener('click', toggleChat);
chatClose.addEventListener('click', toggleChat);
const appendMessage = (text, isUser = false) => {
const div = document.createElement('div');
div.className = `chat-msg ${isUser ? 'chat-msg-user' : 'chat-msg-bot'}`;
const formatted = text.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>').replace(/\n/g, '<br>');
div.innerHTML = `<div class="chat-bubble">${formatted}</div>`;
chatMessages.appendChild(div);
chatMessages.scrollTop = chatMessages.scrollHeight;
};
const sendMessage = async () => {
const text = chatInput.value.trim();
if (!text) return;
appendMessage(text, true);
chatInput.value = '';
chatInput.disabled = true;
chatSend.innerHTML = '<i class="fa-solid fa-ellipsis fa-bounce"></i>';
try {
const res = await fetch(`${API_BASE}/chat`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query: text })
});
const data = await res.json();
appendMessage(data.status === 'success' ? data.response : `❌ Error: ${data.message}`);
} catch (err) {
appendMessage(`❌ Connection error.`);
} finally {
chatInput.disabled = false;
chatSend.innerHTML = '<i class="fa-solid fa-paper-plane"></i>';
chatInput.focus();
}
};
chatSend.addEventListener('click', sendMessage);
chatInput.addEventListener('keypress', (e) => { if (e.key === 'Enter') sendMessage(); });
// Initial load
loadDashboard();
});