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 = `
Run a scan to see the filtering pipeline
`;
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 += `
${stage.label}
${stage.count_out}
${timeStr ? `
${timeStr}
` : ''}
`;
if (i < stages.length - 1) {
const dropPct = stage.count_in > 0 ? Math.round((1 - stage.count_out / stage.count_in) * 100) : 0;
html += `
`;
}
});
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 = '' +
stage.tickers.slice(0, 30).map(t => `${t}`).join('') +
(stage.tickers.length > 30 ? `+${stage.tickers.length - 30} more` : '') +
'
';
}
let rejectedHtml = '';
if (stage.rejected && stage.rejected.length > 0) {
rejectedHtml = ' Rejected:
' +
stage.rejected.slice(0, 10).map(r =>
`
${r.ticker}${r.reason || r.signal_type || ''}
`
).join('') +
(stage.rejected.length > 10 ? `
+${stage.rejected.length - 10} more
` : '') +
'
';
}
pipelineDetails.innerHTML = `
${stage.details}
${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 += `
${mkt.toUpperCase()} Market Regime
${(r.regime || '').replace(/_/g, ' ').toUpperCase()}
Trend: ${r.trend} | Volatility: ${r.volatility_state} | VIX: ${r.vix || 'N/A'} | Breadth: ${r.breadth || 'N/A'}
${r.description || ''}
`;
}
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 = `| No opportunities found in the last scan. |
`;
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 `
|
${s.ticker}${name !== s.ticker ? name : ''} ${sector ? '· ' + sector : ''} |
${(s.signal_type || '').replace(/_/g, ' ').toUpperCase()} |
${s.alpha_score || '-'} |
${fmtPrice(s.entry_price)} |
${fmtPrice(s.target_1)} |
${fmtPrice(s.target_2)} |
${fmtPrice(s.target_3)} |
${fmtPrice(s.stop_loss)} |
${s.risk_reward || '-'} |
${s.quantity || '-'} |
${reasonSummary} |
${narrative} |
`;
}).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 = ` 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, '$1').replace(/\n/g, '
');
div.innerHTML = `${formatted}
`;
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 = '';
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 = '';
chatInput.focus();
}
};
chatSend.addEventListener('click', sendMessage);
chatInput.addEventListener('keypress', (e) => { if (e.key === 'Enter') sendMessage(); });
// Initial load
loadDashboard();
});