/* FinanceIQ v5 — Frontend Logic */
// ══════════════ THEME TOGGLE ══════════════
function getTheme() { return localStorage.getItem('fiq-theme') || 'dark'; }
function setTheme(t) {
document.documentElement.setAttribute('data-theme', t);
localStorage.setItem('fiq-theme', t);
const icon = document.getElementById('themeIcon');
const label = document.getElementById('themeLabel');
if (icon) icon.setAttribute('data-lucide', t === 'dark' ? 'moon' : 'sun');
if (label) label.textContent = t === 'dark' ? 'Dark Mode' : 'Light Mode';
if (typeof lucide !== 'undefined') lucide.createIcons();
// Update charts if they exist
if (chartInstance) updateChartTheme();
if (mcChartInstance) updateMCChartTheme();
}
function updateChartTheme() {
const t = getTheme();
const bg = t === 'dark' ? '#141a2a' : '#ffffff';
const txt = t === 'dark' ? '#8b95a8' : '#5a6577';
const grid = t === 'dark' ? 'rgba(255,255,255,0.03)' : 'rgba(0,0,0,0.04)';
const border = t === 'dark' ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.1)';
chartInstance.applyOptions({ layout: { background: { color: bg }, textColor: txt }, grid: { vertLines: { color: grid }, horzLines: { color: grid } }, timeScale: { borderColor: border }, rightPriceScale: { borderColor: border } });
}
function updateMCChartTheme() {
const t = getTheme();
const bg = t === 'dark' ? '#141a2a' : '#ffffff';
const txt = t === 'dark' ? '#8b95a8' : '#5a6577';
const grid = t === 'dark' ? 'rgba(255,255,255,0.03)' : 'rgba(0,0,0,0.04)';
const border = t === 'dark' ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.1)';
mcChartInstance.applyOptions({ layout: { background: { color: bg }, textColor: txt }, grid: { vertLines: { color: grid }, horzLines: { color: grid } }, timeScale: { borderColor: border }, rightPriceScale: { borderColor: border } });
}
function getChartColors() {
const t = getTheme();
return {
bg: t === 'dark' ? '#141a2a' : '#ffffff',
text: t === 'dark' ? '#8b95a8' : '#5a6577',
grid: t === 'dark' ? 'rgba(255,255,255,0.03)' : 'rgba(0,0,0,0.04)',
border: t === 'dark' ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.1)',
};
}
// Apply saved theme on load
document.addEventListener('DOMContentLoaded', () => {
setTheme(getTheme());
const themeBtn = document.getElementById('themeToggle');
if (themeBtn) themeBtn.addEventListener('click', () => setTheme(getTheme() === 'dark' ? 'light' : 'dark'));
// Sidebar toggle for mobile
const sidebarToggle = document.getElementById('sidebarToggle');
const sidebar = document.getElementById('sidebar');
if (sidebarToggle && sidebar) {
sidebarToggle.addEventListener('click', () => sidebar.classList.toggle('open'));
}
// Asset type selector
document.querySelectorAll('.asset-btn').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('.asset-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
currentAssetType = btn.dataset.asset;
const input = document.getElementById('tickerInput');
const placeholders = { stocks: 'Search ticker (e.g. AAPL, TSLA, NIFTY)', futures: 'Search futures (e.g. ES=F, NQ=F, GC=F)', options: 'Search underlying (e.g. AAPL, SPY)', currencies: 'Search pair (e.g. USDINR=X, EURUSD=X)' };
if (input) input.placeholder = placeholders[currentAssetType] || placeholders.stocks;
// Filter sidebar nav items by asset type
filterSidebarByAsset(currentAssetType);
});
});
});
let chartInstance = null;
let candleSeries = null;
let volumeSeries = null;
let overlays = {};
let analysisData = null;
let newsData = null;
let currentTicker = "";
let currentAssetType = "stocks";
// Helper: detect asset type from ticker
function getAssetType(ticker) {
if (!ticker) return currentAssetType;
if (ticker.endsWith('=F')) return 'futures';
if (ticker.endsWith('=X')) return 'currencies';
if (ticker.startsWith('^')) return 'stocks'; // indices treated as stocks
return currentAssetType; // use the selector's value
}
// Filter sidebar nav items by asset type
function filterSidebarByAsset(assetType) {
document.querySelectorAll('.nav-item[data-asset]').forEach(btn => {
const allowed = (btn.dataset.asset || '').split(',');
if (allowed.includes(assetType)) {
btn.style.display = '';
} else {
btn.style.display = 'none';
// If this tab was active, switch to overview
if (btn.classList.contains('active')) {
btn.classList.remove('active');
document.querySelectorAll('.tab-page').forEach(p => p.classList.remove('active'));
const overviewBtn = document.querySelector('.nav-item[data-tab="overview"]');
if (overviewBtn) overviewBtn.classList.add('active');
const overviewPage = document.getElementById('page-overview');
if (overviewPage) overviewPage.classList.add('active');
}
}
});
}
// ══════════════ CURRENCY STATE ══════════════
let currencyRates = { USD: 1.0, GBP: 0.79, INR: 83.5 };
let currentCurrency = "GBP"; // default
const currencySymbols = { USD: "$", GBP: "£", INR: "₹" };
// Fetch live rates on load
(async function fetchCurrencyRates() {
try {
const res = await fetch("/api/currency", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({}) });
const data = await res.json();
currencyRates = data;
} catch (e) { console.warn("Using fallback currency rates"); }
})();
function convertCurrency(usdValue) {
if (usdValue === null || usdValue === undefined || usdValue === "N/A") return "N/A";
const n = parseFloat(usdValue);
if (isNaN(n)) return String(usdValue);
return n * currencyRates[currentCurrency];
}
function fmtCurrency(usdValue) {
const c = convertCurrency(usdValue);
if (c === "N/A") return "N/A";
const sym = currencySymbols[currentCurrency];
return sym + Number(c).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 });
}
// ══════════════ WELCOME PAGE ══════════════
document.getElementById("letsBeginBtn").addEventListener("click", () => {
document.getElementById("welcomePage").style.display = "none";
document.getElementById("mainApp").style.display = "flex";
});
// ══════════════ SIDEBAR NAVIGATION ══════════════
document.querySelectorAll(".nav-item[data-tab]").forEach(btn => {
btn.addEventListener("click", () => {
document.querySelectorAll(".nav-item[data-tab]").forEach(b => b.classList.remove("active"));
document.querySelectorAll(".tab-page").forEach(p => p.classList.remove("active"));
btn.classList.add("active");
document.getElementById("page-" + btn.dataset.tab).classList.add("active");
// Close mobile sidebar
const sidebar = document.getElementById('sidebar');
if (sidebar && window.innerWidth <= 1024) sidebar.classList.remove('open');
});
});
// ══════════════ SEARCH / SUGGEST ══════════════
const tickerInput = document.getElementById("tickerInput");
const dropdown = document.getElementById("dropdown");
const timeframeSelect = document.getElementById("timeframeSelect");
const periodSelect = document.getElementById("periodSelect");
const currencySelect = document.getElementById("currencySelect");
const customDates = document.getElementById("customDates");
let debounceTimer;
tickerInput.addEventListener("input", () => {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(async () => {
const q = tickerInput.value.trim();
if (q.length < 1) { dropdown.style.display = "none"; return; }
try {
const res = await fetch(`/api/suggest?q=${encodeURIComponent(q)}&asset_type=${encodeURIComponent(currentAssetType)}`);
const data = await res.json();
if (!data.length) { dropdown.style.display = "none"; return; }
dropdown.innerHTML = data.map(d =>
`
${d.ticker}
${d.name}
`
).join("");
dropdown.style.display = "block";
dropdown.querySelectorAll(".dropdown-item").forEach(item => {
item.addEventListener("click", () => {
tickerInput.value = item.dataset.ticker;
dropdown.style.display = "none";
});
});
} catch (e) { dropdown.style.display = "none"; }
}, 200);
});
tickerInput.addEventListener("keydown", e => { if (e.key === "Enter") { dropdown.style.display = "none"; runAnalysis(); } });
document.addEventListener("click", e => { if (!e.target.closest(".ticker-wrapper")) dropdown.style.display = "none"; });
timeframeSelect.addEventListener("change", () => { customDates.style.display = timeframeSelect.value === "custom" ? "flex" : "none"; });
currencySelect.addEventListener("change", () => {
currentCurrency = currencySelect.value;
if (analysisData) refreshCurrencyDisplay();
});
document.getElementById("analyzeBtn").addEventListener("click", runAnalysis);
// ══════════════ REFRESH CURRENCY DISPLAY ══════════════
function refreshCurrencyDisplay() {
if (!analysisData) return;
renderKeyMetrics(analysisData);
renderTechnicalIndicators(analysisData);
renderFinancials(analysisData);
// Re-render items that show currency values
if (analysisData._dcf) renderDCFData(analysisData._dcf);
if (analysisData._dividends) renderDividendData(analysisData._dividends);
if (analysisData._mc) renderMCStats(analysisData._mc);
}
// ══════════════ MAIN ANALYSIS ══════════════
async function runAnalysis() {
const ticker = tickerInput.value.trim().toUpperCase();
if (!ticker) return;
currentTicker = ticker;
const detectedType = getAssetType(ticker);
filterSidebarByAsset(detectedType);
showLoader("Fetching market data...");
const body = { ticker, timeframe: timeframeSelect.value, period: periodSelect.value };
if (timeframeSelect.value === "custom") {
body.start = document.getElementById("startDate").value;
body.end = document.getElementById("endDate").value;
}
try {
updateLoader("Analyzing " + ticker + "...");
const res = await fetch("/api/analyze", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) });
if (!res.ok) { const err = await res.json().catch(() => ({})); throw new Error(err.error || `HTTP ${res.status}`); }
analysisData = await res.json();
const exportBtn = document.getElementById("exportBtnSidebar");
if (exportBtn) exportBtn.style.display = "flex";
document.querySelectorAll(".nav-item[data-tab]").forEach(b => b.classList.remove("active"));
document.querySelectorAll(".tab-page").forEach(p => p.classList.remove("active"));
document.querySelector('.nav-item[data-tab="overview"]').classList.add("active");
document.getElementById("page-overview").classList.add("active");
// Always render chart & key metrics
renderChart(analysisData);
renderKeyMetrics(analysisData);
renderQuickSignal(analysisData);
renderTechnicalIndicators(analysisData);
// Conditional fetches based on asset type
if (detectedType === 'stocks') {
renderFinancials(analysisData);
fetchPatterns(ticker, analysisData.price_history);
fetchAnalyst(ticker);
fetchInsider(ticker);
fetchEarnings(ticker);
fetchDCF(ticker);
fetchZScore(ticker);
fetchDividends(ticker);
fetchHeatmap();
fetchCompetitors(ticker);
}
if (detectedType === 'stocks' || detectedType === 'futures') {
fetchMonteCarlo(ticker, analysisData.price_history);
fetchCorrelation(ticker);
fetchMacro();
fetchPatterns(ticker, analysisData.price_history);
}
if (detectedType === 'currencies') {
fetchMonteCarlo(ticker, analysisData.price_history);
fetchCorrelation(ticker);
}
if (detectedType === 'options') {
// Load the options chain for this ticker
loadOptionsChainForTicker(ticker);
}
// Always fetch these
fetchSentiment();
fetchNews(ticker);
fetchAI(ticker);
hideLoader();
startLivePolling();
} catch (err) {
hideLoader();
alert("Error: " + err.message);
}
}
// ══════════════ LOADER ══════════════
function showLoader(msg) { document.getElementById("loader").style.display = "flex"; document.getElementById("loaderText").textContent = msg || "Analyzing..."; }
function updateLoader(msg) { document.getElementById("loaderText").textContent = msg; }
function hideLoader() { document.getElementById("loader").style.display = "none"; }
// ══════════════ CHART ══════════════
let chartResizeObserver = null;
function renderChart(data) {
const container = document.getElementById("chartContainer");
container.innerHTML = "";
document.getElementById("chartControls").style.display = "flex";
// Cleanup previous chart & observer
if (chartResizeObserver) { chartResizeObserver.disconnect(); chartResizeObserver = null; }
if (chartInstance) { chartInstance.remove(); chartInstance = null; }
const cc = getChartColors();
const rect = container.getBoundingClientRect();
chartInstance = LightweightCharts.createChart(container, {
width: rect.width || 800,
height: rect.height || 420,
layout: { background: { color: cc.bg }, textColor: cc.text },
grid: { vertLines: { color: cc.grid }, horzLines: { color: cc.grid } },
crosshair: { mode: LightweightCharts.CrosshairMode.Normal },
timeScale: { borderColor: cc.border, timeVisible: false },
rightPriceScale: { borderColor: cc.border },
});
// Responsive resize
chartResizeObserver = new ResizeObserver(entries => {
for (const entry of entries) {
const { width, height } = entry.contentRect;
if (chartInstance && width > 0 && height > 0) {
chartInstance.resize(width, height);
}
}
});
chartResizeObserver.observe(container);
candleSeries = chartInstance.addCandlestickSeries({
upColor: "#22c55e", downColor: "#ef4444",
borderUpColor: "#22c55e", borderDownColor: "#ef4444",
wickUpColor: "#22c55e", wickDownColor: "#ef4444"
});
// Validate and sort price data
const prices = (data.price_history || [])
.filter(p => p.date && p.open != null && p.close != null && !isNaN(p.open) && !isNaN(p.close))
.map(p => ({
time: String(p.date).slice(0, 10),
open: Number(p.open),
high: Number(p.high),
low: Number(p.low),
close: Number(p.close)
}))
.sort((a, b) => a.time.localeCompare(b.time));
// Remove duplicates (same date)
const seen = new Set();
const uniquePrices = prices.filter(p => {
if (seen.has(p.time)) return false;
seen.add(p.time);
return true;
});
candleSeries.setData(uniquePrices);
volumeSeries = chartInstance.addHistogramSeries({ priceFormat: { type: "volume" }, priceScaleId: "vol" });
chartInstance.priceScale("vol").applyOptions({ scaleMargins: { top: 0.85, bottom: 0 } });
const volumeData = (data.price_history || [])
.filter(p => p.date && p.volume != null)
.map(p => ({
time: String(p.date).slice(0, 10),
value: Number(p.volume) || 0,
color: Number(p.close) >= Number(p.open) ? "rgba(34,197,94,0.3)" : "rgba(239,68,68,0.3)"
}))
.sort((a, b) => a.time.localeCompare(b.time));
const seenVol = new Set();
volumeSeries.setData(volumeData.filter(p => { if (seenVol.has(p.time)) return false; seenVol.add(p.time); return true; }));
chartInstance.timeScale().fitContent();
overlays = {};
setupOverlayToggles(data);
}
function setupOverlayToggles(data) {
const prices = data.price_history || [];
const closes = prices.map(p => p.close);
const times = prices.map(p => p.time || p.date);
function ema(arr, period) {
const k = 2 / (period + 1);
const result = [arr[0]];
for (let i = 1; i < arr.length; i++) result.push(arr[i] * k + result[i - 1] * (1 - k));
return result;
}
function sma(arr, period) {
const result = [];
for (let i = 0; i < arr.length; i++) {
if (i < period - 1) { result.push(null); continue; }
let sum = 0;
for (let j = i - period + 1; j <= i; j++) sum += arr[j];
result.push(sum / period);
}
return result;
}
function addLine(id, values, color) {
if (overlays[id]) { chartInstance.removeSeries(overlays[id]); delete overlays[id]; }
const s = chartInstance.addLineSeries({ color, lineWidth: 1, priceLineVisible: false, lastValueVisible: false });
const d = [];
for (let i = 0; i < values.length; i++) { if (values[i] !== null) d.push({ time: times[i], value: values[i] }); }
s.setData(d);
overlays[id] = s;
}
function removeLine(id) { if (overlays[id]) { chartInstance.removeSeries(overlays[id]); delete overlays[id]; } }
const emaConfig = { togEma5: [5, "#f59e0b"], togEma10: [10, "#06b6d4"], togEma20: [20, "#8b5cf6"], togSma200: [200, "#ec4899"] };
Object.entries(emaConfig).forEach(([id, [period, color]]) => {
const el = document.getElementById(id);
if (!el) return;
el.checked = false;
el.onchange = () => {
if (el.checked) {
const vals = id.startsWith("togSma") ? sma(closes, period) : ema(closes, period);
addLine(id, vals, color);
} else removeLine(id);
};
});
const bbEl = document.getElementById("togBb");
if (bbEl) {
bbEl.checked = false;
bbEl.onchange = () => {
if (bbEl.checked) {
const sma20 = sma(closes, 20);
const upper = [], lower = [];
for (let i = 0; i < closes.length; i++) {
if (sma20[i] === null) { upper.push(null); lower.push(null); continue; }
let sum = 0;
for (let j = i - 19; j <= i; j++) sum += (closes[j] - sma20[i]) ** 2;
const std = Math.sqrt(sum / 20);
upper.push(sma20[i] + 2 * std);
lower.push(sma20[i] - 2 * std);
}
addLine("bbUpper", upper, "rgba(99,102,241,0.5)");
addLine("bbLower", lower, "rgba(99,102,241,0.5)");
addLine("bbMid", sma20, "rgba(99,102,241,0.3)");
} else { removeLine("bbUpper"); removeLine("bbLower"); removeLine("bbMid"); }
};
}
const fibEl = document.getElementById("togFib");
if (fibEl) {
fibEl.checked = false;
fibEl.onchange = () => {
if (fibEl.checked) {
const hi = Math.max(...closes), lo = Math.min(...closes);
[0, 0.236, 0.382, 0.5, 0.618, 0.786, 1].forEach((lvl, idx) => {
const val = hi - (hi - lo) * lvl;
const colors = ["#22c55e", "#84cc16", "#eab308", "#f59e0b", "#f97316", "#ef4444", "#dc2626"];
addLine("fib" + idx, closes.map(() => val), colors[idx]);
});
} else { for (let i = 0; i < 7; i++) removeLine("fib" + i); }
};
}
const customBtn = document.getElementById("addCustomEma");
if (customBtn) {
customBtn.onclick = () => {
const period = parseInt(document.getElementById("customEmaPeriod").value);
if (period >= 2 && period <= 500) addLine("custom" + period, ema(closes, period), "#a78bfa");
};
}
}
// ══════════════ KEY METRICS ══════════════
function renderKeyMetrics(data) {
const t = data.technicals || {};
const r = data.ratios || {};
const items = [
{ label: "Price", value: fmtCurrency(t.price) },
{ label: "RSI", value: fmt(t.rsi), cls: t.rsi > 70 ? "negative" : t.rsi < 30 ? "positive" : "" },
{ label: "MACD", value: fmt(t.macd), cls: t.macd > t.macd_signal ? "positive" : "negative" },
{ label: "SMA 200", value: fmtCurrency(t.sma_200) },
{ label: "P/E", value: fmt(r.pe) },
{ label: "EV/EBITDA", value: fmt(r.ev_ebitda) },
{ label: "ROE", value: fmtPct(r.roe) },
{ label: "ATR", value: fmtCurrency(t.atr) },
];
document.getElementById("keyMetrics").innerHTML = items.map(i =>
``
).join("");
}
// ══════════════ QUICK SIGNAL ══════════════
function renderQuickSignal(data) {
const t = data.technicals || {};
let score = 0, reasons = [];
if (t.rsi < 30) { score += 2; reasons.push("RSI oversold"); }
else if (t.rsi > 70) { score -= 2; reasons.push("RSI overbought"); }
if (t.macd > t.macd_signal) { score += 1; reasons.push("MACD bullish"); }
else { score -= 1; reasons.push("MACD bearish"); }
if (t.price > t.sma_200) { score += 1; reasons.push("Above SMA200"); }
else if (t.sma_200) { score -= 1; reasons.push("Below SMA200"); }
if (t.price > t.ema_20) { score += 1; reasons.push("Above EMA20"); }
const cls = score >= 2 ? "bullish" : score <= -2 ? "bearish" : "neutral";
const label = score >= 2 ? "BULLISH" : score <= -2 ? "BEARISH" : "NEUTRAL";
document.getElementById("quickSignal").innerHTML = `
${label}
${reasons.join(" · ")}
`;
}
// ══════════════ TECHNICAL INDICATORS ══════════════
function renderTechnicalIndicators(data) {
const t = data.technicals || {};
const items = [
{ label: "EMA 5", value: fmtCurrency(t.ema_5) }, { label: "EMA 10", value: fmtCurrency(t.ema_10) },
{ label: "EMA 20", value: fmtCurrency(t.ema_20) }, { label: "EMA 50", value: fmtCurrency(t.ema_50) },
{ label: "SMA 200", value: fmtCurrency(t.sma_200) }, { label: "RSI (14)", value: fmt(t.rsi), cls: t.rsi > 70 ? "negative" : t.rsi < 30 ? "positive" : "" },
{ label: "MACD", value: fmt(t.macd) }, { label: "MACD Signal", value: fmt(t.macd_signal) },
{ label: "BB Upper", value: fmtCurrency(t.bb_upper) }, { label: "BB Lower", value: fmtCurrency(t.bb_lower) },
{ label: "ATR (14)", value: fmtCurrency(t.atr) }, { label: "VWAP", value: fmtCurrency(t.vwap) },
];
document.getElementById("technicalIndicators").innerHTML = items.map(i =>
``
).join("");
}
// ══════════════ FINANCIALS ══════════════
function renderFinancials(data) {
const f = data.financials || {}, r = data.ratios || {}, fc = data.fcff || {};
document.getElementById("incomeStatement").innerHTML = renderGrid(f, true);
document.getElementById("ratiosGrid").innerHTML = renderGrid(r, false);
document.getElementById("fcffGrid").innerHTML = renderGrid(fc, true);
}
function renderGrid(obj, asCurrency) {
return Object.entries(obj).map(([k, v]) => {
const label = k.replace(/_/g, " ").replace(/\b\w/g, c => c.toUpperCase());
const val = asCurrency && typeof v === "number" && Math.abs(v) > 100 ? fmtCurrencyLarge(v) : fmtVal(v);
return ``;
}).join("");
}
// ══════════════ FETCH: CANDLESTICK PATTERNS (last 7 days + prediction) ══════════════
async function fetchPatterns(ticker, prices) {
try {
const res = await fetch("/api/patterns", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ ticker, prices, lookback_days: 7 }) });
const data = await res.json();
const el = document.getElementById("candlestickPatterns");
const outlookEl = document.getElementById("patternOutlook");
// Render outlook summary
if (data.outlook) {
const cls = data.outlook.startsWith("BULLISH") ? "bullish" : data.outlook.startsWith("BEARISH") ? "bearish" : "neutral";
outlookEl.className = "pattern-outlook-box " + cls;
outlookEl.innerHTML = `
🔮 7-Day Pattern Outlook: ${data.outlook}
Bullish: ${data.bullish_count || 0} · Bearish: ${data.bearish_count || 0} · Neutral: ${data.neutral_count || 0}
`;
}
if (!data.patterns || !data.patterns.length) {
el.innerHTML = 'No significant patterns detected in the last 7 trading days.
';
return;
}
el.innerHTML = data.patterns.map(p => `
${p.type}
${p.pattern}
${p.date}
${p.description}
🔮 ${p.prediction || ""}
`).join("");
} catch (e) { console.error("Patterns:", e); }
}
// ══════════════ FETCH: ANALYST RATINGS (N/A handling) ══════════════
async function fetchAnalyst(ticker) {
try {
const res = await fetch("/api/analyst", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ ticker }) });
const data = await res.json();
const el = document.getElementById("analystRatings");
if (data.error || data.available === false) {
el.innerHTML = `📊 Analyst ratings are not available for ${ticker}.
This ticker may not be covered by Wall Street analysts on Finnhub.
`;
return;
}
const r = data.recommendation || {};
const total = (r.strong_buy || 0) + (r.buy || 0) + (r.hold || 0) + (r.sell || 0) + (r.strong_sell || 0) || 1;
const pt = data.price_target || {};
el.innerHTML = `
${analystBar("Strong Buy", r.strong_buy, total, "buy")}
${analystBar("Buy", r.buy, total, "buy")}
${analystBar("Hold", r.hold, total, "hold")}
${analystBar("Sell", r.sell, total, "sell")}
${analystBar("Strong Sell", r.strong_sell, total, "sell")}
Target Low
${pt.low !== "N/A" ? fmtCurrency(pt.low) : "N/A"}
Target Mean
${pt.mean !== "N/A" ? fmtCurrency(pt.mean) : "N/A"}
Target Median
${pt.median !== "N/A" ? fmtCurrency(pt.median) : "N/A"}
Target High
${pt.high !== "N/A" ? fmtCurrency(pt.high) : "N/A"}
`;
} catch (e) { console.error("Analyst:", e); }
}
function analystBar(label, count, total, cls) {
const pct = Math.round((count || 0) / total * 100);
return ``;
}
// ══════════════ FETCH: INSIDER TRADING ══════════════
async function fetchInsider(ticker) {
try {
const res = await fetch("/api/insider", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ ticker }) });
const data = await res.json();
const el = document.getElementById("insiderTrading");
if (!data.transactions || !data.transactions.length) { el.innerHTML = 'No recent insider transactions.
'; return; }
const rows = data.transactions.slice(0, 10).map(t => {
const isBuy = (t.change > 0 || t.transaction_type === "P - Purchase");
return `| ${t.filing_date} | ${t.name} | ${t.transaction_type || (isBuy ? 'Buy' : 'Sell')} | ${fmtNum(t.change)} |
`;
}).join("");
el.innerHTML = `| Date | Insider | Type | Shares |
${rows}
`;
} catch (e) { console.error("Insider:", e); }
}
// ══════════════ FETCH: MARKET SENTIMENT ══════════════
async function fetchSentiment() {
try {
const res = await fetch("/api/sentiment-market", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({}) });
const data = await res.json();
const fg = data.fear_greed || {};
const fgVal = fg.value || 50;
const fgCls = fgVal < 25 ? "negative" : fgVal < 45 ? "muted" : fgVal < 55 ? "" : fgVal < 75 ? "" : "positive";
document.getElementById("fearGreedGauge").innerHTML = `
${fgVal}
${fg.description || "Neutral"}
`;
const vix = data.vix || {};
const vixCls = vix.value < 15 ? "positive" : vix.value < 25 ? "" : vix.value < 35 ? "muted" : "negative";
document.getElementById("vixDisplay").innerHTML = `
${vix.value || "N/A"}
${vix.label || ""}
`;
} catch (e) { console.error("Sentiment:", e); }
}
// ══════════════ FETCH: NEWS ══════════════
async function fetchNews(ticker) {
try {
const res = await fetch("/api/news", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ ticker }) });
newsData = await res.json();
const el = document.getElementById("newsSection");
if (!newsData.news || !newsData.news.length) { el.innerHTML = 'No recent news.
'; return; }
const avgCls = newsData.overall_label === "Positive" ? "positive" : newsData.overall_label === "Negative" ? "negative" : "neutral";
el.innerHTML = `Overall: ${newsData.overall_label} (${newsData.average_sentiment})
` +
newsData.news.map(n => {
const cls = n.sentiment_label === "Positive" ? "positive" : n.sentiment_label === "Negative" ? "negative" : "neutral";
return `
${n.sentiment_label} (${n.sentiment_score})${n.source || ""}
`;
}).join("");
} catch (e) { console.error("News:", e); }
}
// ══════════════ FETCH: FRED MACRO (with good/bad + explanations) ══════════════
async function fetchMacro() {
try {
const res = await fetch("/api/macro", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({}) });
const data = await res.json();
const el = document.getElementById("macroIndicators");
if (data.error) { el.innerHTML = `${data.error}
`; return; }
el.innerHTML = Object.entries(data.indicators || {}).map(([k, v]) => {
const statusBadge = v.status ? `${v.status}` : "";
const explanation = v.explanation ? `${v.explanation}
` : "";
return `
${v.label} ${statusBadge}
${v.value}
${explanation}
`;
}).join("");
} catch (e) { console.error("Macro:", e); }
}
// ══════════════ FETCH: EARNINGS ══════════════
async function fetchEarnings(ticker) {
try {
const res = await fetch("/api/earnings", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ ticker }) });
const data = await res.json();
const el = document.getElementById("earningsTable");
if (!data.earnings || !data.earnings.length) { el.innerHTML = 'No earnings data available.
'; return; }
const rows = data.earnings.map(e => {
const surprise = parseFloat(e.surprise_pct);
const cls = surprise > 0 ? "surprise-positive" : surprise < 0 ? "surprise-negative" : "";
return `| ${e.period} | ${fmtCurrency(e.estimate)} | ${fmtCurrency(e.actual)} | ${fmtCurrency(e.surprise)} | ${fmtPctRaw(e.surprise_pct)} |
`;
}).join("");
el.innerHTML = `| Period | Estimate | Actual | Surprise | Surprise % |
${rows}
`;
} catch (e) { console.error("Earnings:", e); }
}
// ══════════════ FETCH: MONTE CARLO (selectable bands) ══════════════
let mcChartInstance = null;
let mcBandSeries = {};
let mcRawData = null;
async function fetchMonteCarlo(ticker, prices) {
try {
const res = await fetch("/api/monte-carlo", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ ticker, prices, days: 60, simulations: 1000 }) });
mcRawData = await res.json();
if (mcRawData.error) { document.getElementById("monteCarloChart").innerHTML = `${mcRawData.error}
`; return; }
analysisData._mc = mcRawData;
renderMCChart(mcRawData);
renderMCStats(mcRawData);
setupMCBandToggles();
} catch (e) { console.error("Monte Carlo:", e); }
}
function renderMCChart(data) {
const container = document.getElementById("monteCarloChart");
container.innerHTML = "";
mcBandSeries = {};
const cc = getChartColors();
mcChartInstance = LightweightCharts.createChart(container, {
layout: { background: { color: cc.bg }, textColor: cc.text },
grid: { vertLines: { color: cc.grid }, horzLines: { color: cc.grid } },
timeScale: { borderColor: cc.border }, rightPriceScale: { borderColor: cc.border },
});
const today = new Date();
function dayStr(offset) { const d = new Date(today); d.setDate(d.getDate() + offset); return d.toISOString().slice(0, 10); }
const bands = [
{ key: "p90", color: "rgba(34,197,94,0.4)", label: "P90 (Bull)", lineWidth: 1 },
{ key: "p75", color: "rgba(34,197,94,0.6)", label: "P75", lineWidth: 1 },
{ key: "p50", color: "rgba(99,102,241,0.9)", label: "P50 (Median)", lineWidth: 2 },
{ key: "p25", color: "rgba(239,68,68,0.6)", label: "P25", lineWidth: 1 },
{ key: "p10", color: "rgba(239,68,68,0.4)", label: "P10 (Bear)", lineWidth: 1 },
];
bands.forEach(b => {
if (data.percentiles && data.percentiles[b.key]) {
const series = mcChartInstance.addLineSeries({ color: b.color, lineWidth: b.lineWidth, priceLineVisible: false, lastValueVisible: false });
series.setData(data.percentiles[b.key].map((v, i) => ({ time: dayStr(i + 1), value: v })));
mcBandSeries[b.key] = series;
}
});
mcChartInstance.timeScale().fitContent();
}
function renderMCStats(data) {
const s = data.final_stats || {};
document.getElementById("monteCarloStats").innerHTML = [
{ label: "Start Price", value: fmtCurrency(data.start_price) },
{ label: "Median (P50)", value: fmtCurrency(s.median) },
{ label: "Mean", value: fmtCurrency(s.mean) },
{ label: "P10 (Bearish)", value: fmtCurrency(s.p10), cls: "negative" },
{ label: "P90 (Bullish)", value: fmtCurrency(s.p90), cls: "positive" },
].map(i => ``).join("");
}
function setupMCBandToggles() {
const bandMap = { togP90: "p90", togP75: "p75", togP50: "p50", togP25: "p25", togP10: "p10" };
Object.entries(bandMap).forEach(([elId, bandKey]) => {
const el = document.getElementById(elId);
if (!el) return;
el.checked = true;
el.onchange = () => {
if (mcBandSeries[bandKey]) {
if (el.checked) {
mcBandSeries[bandKey].applyOptions({ visible: true });
} else {
mcBandSeries[bandKey].applyOptions({ visible: false });
}
}
};
});
}
// ══════════════ FETCH: DCF ══════════════
async function fetchDCF(ticker) {
try {
const res = await fetch("/api/dcf", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ ticker }) });
const data = await res.json();
if (data.error) { document.getElementById("dcfValuation").innerHTML = `${data.error}
`; return; }
analysisData._dcf = data;
renderDCFData(data);
} catch (e) { console.error("DCF:", e); }
}
function renderDCFData(data) {
const el = document.getElementById("dcfValuation");
const cls = data.verdict === "UNDERVALUED" ? "undervalued" : data.verdict === "OVERVALUED" ? "overvalued" : "fair";
const arrow = data.upside_pct > 0 ? "↑" : "↓";
el.innerHTML = `
${data.verdict}
${arrow} ${Math.abs(data.upside_pct)}% ${data.upside_pct > 0 ? 'upside' : 'downside'}
Intrinsic Value
${fmtCurrency(data.intrinsic_value)}
Current Price
${fmtCurrency(data.current_price)}
Growth Rate
${data.growth_rate}%
Base FCF
${fmtCurrencyLarge(data.base_fcf)}
Enterprise Value
${fmtCurrencyLarge(data.enterprise_value)}
`;
}
// ══════════════ FETCH: Z-SCORE ══════════════
async function fetchZScore(ticker) {
try {
const res = await fetch("/api/zscore", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ ticker }) });
const data = await res.json();
const el = document.getElementById("zScore");
if (data.error) { el.innerHTML = `${data.error}
`; return; }
const cls = data.zone === "Safe Zone" ? "safe" : data.zone === "Grey Zone" ? "grey" : "distress";
el.innerHTML = `
${data.z_score}
${data.zone}
${Object.entries(data.components || {}).map(([k, v]) =>
`
${k.replace(/_/g, " ")}
${v}
`
).join("")}
`;
} catch (e) { console.error("ZScore:", e); }
}
// ══════════════ FETCH: DIVIDENDS ══════════════
async function fetchDividends(ticker) {
try {
const res = await fetch("/api/dividends", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ ticker }) });
const data = await res.json();
if (data.error) { document.getElementById("dividendAnalysis").innerHTML = `${data.error}
`; return; }
analysisData._dividends = data;
renderDividendData(data);
} catch (e) { console.error("Dividends:", e); }
}
function renderDividendData(data) {
const el = document.getElementById("dividendAnalysis");
const yld = data.dividend_yield && data.dividend_yield !== "N/A" ? (data.dividend_yield * 100).toFixed(2) + "%" : "N/A";
const payout = data.payout_ratio && data.payout_ratio !== "N/A" ? (data.payout_ratio * 100).toFixed(1) + "%" : "N/A";
el.innerHTML = `
Annual Rate
${fmtCurrency(data.dividend_rate)}
5Y Avg Yield
${data.five_year_avg_yield !== "N/A" ? data.five_year_avg_yield + "%" : "N/A"}
`;
}
// ══════════════ FETCH: CORRELATION (FIXED) ══════════════
async function fetchCorrelation(ticker) {
try {
const res = await fetch("/api/correlation", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ ticker }) });
const data = await res.json();
const el = document.getElementById("correlationMatrix");
if (data.error) { el.innerHTML = `${data.error}
`; return; }
const tickers = data.tickers || [];
const m = data.matrix || {};
let html = ' | ';
tickers.forEach(t => html += `${t} | `);
html += '
';
tickers.forEach(row => {
html += `| ${row} | `;
tickers.forEach(col => {
const val = (m[col] && m[col][row] !== undefined) ? m[col][row] : 0;
const bg = corrColor(val);
html += `${val.toFixed(2)} | `;
});
html += '
';
});
html += '
';
el.innerHTML = html;
} catch (e) { console.error("Correlation:", e); }
}
function corrColor(val) {
if (val >= 0) return `rgba(34,197,94,${Math.abs(val) * 0.6})`;
return `rgba(239,68,68,${Math.abs(val) * 0.6})`;
}
// ══════════════ FETCH: SECTOR HEATMAP ══════════════
let heatmapData = null;
async function fetchHeatmap() {
try {
const res = await fetch("/api/heatmap", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({}) });
heatmapData = await res.json();
if (heatmapData.error) { document.getElementById("sectorHeatmap").innerHTML = `${heatmapData.error}
`; return; }
const controls = document.getElementById("heatmapControls");
controls.innerHTML = (heatmapData.timeframes || []).map((tf, i) =>
``
).join("");
controls.querySelectorAll(".heatmap-btn").forEach(btn => {
btn.addEventListener("click", () => {
controls.querySelectorAll(".heatmap-btn").forEach(b => b.classList.remove("active"));
btn.classList.add("active");
renderHeatmap(btn.dataset.tf);
});
});
if (heatmapData.timeframes && heatmapData.timeframes.length) renderHeatmap(heatmapData.timeframes[0]);
} catch (e) { console.error("Heatmap:", e); }
}
function renderHeatmap(tf) {
const sectors = heatmapData.sectors[tf] || {};
const entries = Object.entries(sectors);
if (!entries.length) {
document.getElementById("sectorHeatmap").innerHTML = 'Sector data temporarily unavailable (API rate limit).
';
return;
}
document.getElementById("sectorHeatmap").innerHTML = entries.map(([name, pct]) => {
const bg = pct >= 0 ? `rgba(34,197,94,${Math.min(Math.abs(pct) / 5, 0.7) + 0.1})` : `rgba(239,68,68,${Math.min(Math.abs(pct) / 5, 0.7) + 0.1})`;
const color = Math.abs(pct) > 1 ? "white" : "var(--text-secondary)";
return `${name}
${pct > 0 ? "+" : ""}${pct.toFixed(2)}%
`;
}).join("");
}
// ══════════════ FETCH: COMPETITORS (FIXED field names) ══════════════
async function fetchCompetitors(ticker) {
try {
const res = await fetch("/api/competitors", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ ticker }) });
const data = await res.json();
const el = document.getElementById("compSection");
if (!data.peers || !data.peers.length) {
el.innerHTML = `No competitor data available. ${data.sector || "unknown"}
`;
if (data.error) el.innerHTML += `${data.error}
`;
return;
}
let html = `Sector: ${data.sector}
`;
html += '| Ticker | Market Cap | P/E | EV/EBITDA | ROE | Net Margin | Gross Margin |
';
data.peers.forEach(p => {
const cls = p.is_target ? ' class="comp-target"' : '';
html += `
| ${p.ticker} |
${fmtCurrencyLarge(p.market_cap)} |
${fmtVal(p.pe_ratio)} |
${fmtVal(p.ev_ebitda)} |
${fmtPctVal(p.roe)} |
${fmtPctVal(p.net_margin)} |
${fmtPctVal(p.gross_margin)} |
`;
});
html += '
';
el.innerHTML = html;
} catch (e) { console.error("Competitors:", e); }
}
// ══════════════ FETCH: AI ══════════════
async function fetchAI(ticker) {
try {
const payload = { ticker, analysis: analysisData || {}, news: newsData || {} };
const res = await fetch("/api/ai", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload) });
const data = await res.json();
document.getElementById("aiContent").innerHTML = data.overview ? formatAI(data.overview) : `${data.error || "No response"}
`;
} catch (e) { document.getElementById("aiContent").innerHTML = 'AI unavailable.
'; }
}
document.getElementById("askAi").addEventListener("click", async () => {
const q = document.getElementById("aiInput").value.trim();
if (!q || !currentTicker) return;
document.getElementById("aiContent").innerHTML = 'Thinking...
';
try {
const payload = { ticker: currentTicker, analysis: analysisData || {}, news: newsData || {}, question: q };
const res = await fetch("/api/ai", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload) });
const data = await res.json();
document.getElementById("aiContent").innerHTML = data.overview ? formatAI(data.overview) : `${data.error || "No response"}
`;
} catch (e) { document.getElementById("aiContent").innerHTML = 'Error.
'; }
});
function formatAI(text) {
return text.replace(/\*\*(.*?)\*\*/g, '$1')
.replace(/^### (.*?)$/gm, '$1
')
.replace(/^## (.*?)$/gm, '$1
')
.replace(/^# (.*?)$/gm, '$1
')
.replace(/\n- /g, '\n• ')
.replace(/\n/g, '
');
}
// ══════════════ EXPORT ══════════════
document.getElementById("exportBtnSidebar").addEventListener("click", async () => {
if (!currentTicker || !analysisData) return;
try {
const payload = { ticker: currentTicker, analysis: analysisData, news: newsData || {}, ai_overview: document.getElementById("aiContent").innerText || "" };
const res = await fetch("/api/export", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload) });
const blob = await res.blob();
const url = URL.createObjectURL(blob);
const a = document.createElement("a"); a.href = url; a.download = `${currentTicker}_analysis.xlsx`; a.click();
} catch (e) { alert("Export failed."); }
});
// ══════════════ FORMATTING HELPERS ══════════════
function fmt(v) { if (v === null || v === undefined || v === "N/A") return "N/A"; return typeof v === "number" ? v.toFixed(2) : String(v); }
function fmtPct(v) { if (v === null || v === undefined || v === "N/A") return "N/A"; return (v * 100).toFixed(2) + "%"; }
function fmtPctRaw(v) { if (v === null || v === undefined || v === "N/A") return "N/A"; return parseFloat(v).toFixed(2) + "%"; }
function fmtVal(v) { if (v === null || v === undefined || v === "N/A") return "N/A"; return typeof v === "number" ? v.toLocaleString(undefined, { maximumFractionDigits: 2 }) : String(v); }
function fmtPctVal(v) { if (v === null || v === undefined || v === "N/A") return "N/A"; return typeof v === "number" ? (v * 100).toFixed(2) + "%" : String(v); }
function fmtNum(v) { if (v === null || v === undefined || v === "N/A") return "N/A"; return Number(v).toLocaleString(); }
function fmtLarge(v) {
if (v === null || v === undefined || v === "N/A") return "N/A";
const n = Number(v);
if (isNaN(n)) return String(v);
if (Math.abs(n) >= 1e12) return (n / 1e12).toFixed(2) + "T";
if (Math.abs(n) >= 1e9) return (n / 1e9).toFixed(2) + "B";
if (Math.abs(n) >= 1e6) return (n / 1e6).toFixed(2) + "M";
if (Math.abs(n) >= 1e3) return (n / 1e3).toFixed(1) + "K";
return n.toFixed(2);
}
function fmtCurrencyLarge(v) {
if (v === null || v === undefined || v === "N/A") return "N/A";
const converted = convertCurrency(v);
if (converted === "N/A") return "N/A";
const sym = currencySymbols[currentCurrency];
const n = Number(converted);
if (isNaN(n)) return String(v);
if (Math.abs(n) >= 1e12) return sym + (n / 1e12).toFixed(2) + "T";
if (Math.abs(n) >= 1e9) return sym + (n / 1e9).toFixed(2) + "B";
if (Math.abs(n) >= 1e6) return sym + (n / 1e6).toFixed(2) + "M";
if (Math.abs(n) >= 1e3) return sym + (n / 1e3).toFixed(1) + "K";
return sym + n.toFixed(2);
}
// ══════════════ LIVE PRICE AUTO-REFRESH (15s) ══════════════
let livePollInterval = null;
function startLivePolling() {
stopLivePolling();
livePollInterval = setInterval(async () => {
if (!currentTicker) return;
try {
const res = await fetch("/api/live-price", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ ticker: currentTicker })
});
const data = await res.json();
if (data.price && analysisData && analysisData.technicals) {
analysisData.technicals.price = data.price;
// Update the price display in Key Metrics
const priceEl = document.querySelector("#keyMetrics .data-item .data-value");
if (priceEl) {
priceEl.textContent = fmtCurrency(data.price);
priceEl.classList.add("price-flash");
setTimeout(() => priceEl.classList.remove("price-flash"), 600);
}
// Update last candle on chart if available
if (candleSeries && data.price) {
const now = new Date();
const todayStr = now.toISOString().slice(0, 10);
candleSeries.update({
time: todayStr,
open: data.price,
high: data.price,
low: data.price,
close: data.price,
});
}
}
} catch (e) { /* silent fail for polling */ }
}, 15000);
}
function stopLivePolling() {
if (livePollInterval) {
clearInterval(livePollInterval);
livePollInterval = null;
}
}
// Start polling after each analysis
const origRunAnalysis = runAnalysis;
// ══════════════ OPTIONS CHAIN ══════════════
let optionsChainData = null;
let showCallsOrPuts = 'calls';
async function loadOptionsChainForTicker(ticker) {
try {
const res = await fetch("/api/options/chain", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ ticker })
});
optionsChainData = await res.json();
if (optionsChainData.error) {
document.getElementById("optionsChainTable").innerHTML = `${optionsChainData.error}
`;
return;
}
// Populate expiry selector
const sel = document.getElementById("optExpiry");
sel.innerHTML = (optionsChainData.expirations || []).map(e => ``).join("");
// Set payoff strike default to current price
if (optionsChainData.current_price) {
document.getElementById("payoffStrike").value = Math.round(optionsChainData.current_price);
}
renderOptionsChain();
} catch (e) {
document.getElementById("optionsChainTable").innerHTML = `Failed to load options chain
`;
}
}
function renderOptionsChain() {
if (!optionsChainData) return;
const data = showCallsOrPuts === 'calls' ? optionsChainData.calls : optionsChainData.puts;
if (!data || !data.length) {
document.getElementById("optionsChainTable").innerHTML = `No data available
`;
return;
}
const curPrice = optionsChainData.current_price || 0;
let html = `
| Strike | Last | Bid | Ask | Volume | OI | IV |
Delta | Gamma | Theta | Vega |
`;
for (const opt of data) {
const itm = (showCallsOrPuts === 'calls' && opt.strike < curPrice) || (showCallsOrPuts === 'puts' && opt.strike > curPrice);
const cls = itm ? ' class="positive"' : '';
html += `
| ${opt.strike} |
${(opt.lastPrice || 0).toFixed(2)} |
${(opt.bid || 0).toFixed(2)} |
${(opt.ask || 0).toFixed(2)} |
${(opt.volume || 0).toLocaleString()} |
${(opt.openInterest || 0).toLocaleString()} |
${((opt.impliedVolatility || 0) * 100).toFixed(1)}% |
${(opt.delta || 0).toFixed(4)} |
${(opt.gamma || 0).toFixed(6)} |
${(opt.theta || 0).toFixed(4)} |
${(opt.vega || 0).toFixed(4)} |
`;
}
html += '
';
document.getElementById("optionsChainTable").innerHTML = html;
}
// Options chain button events
document.getElementById("loadChainBtn")?.addEventListener("click", async () => {
if (!currentTicker) return;
const exp = document.getElementById("optExpiry").value;
try {
const res = await fetch("/api/options/chain", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ ticker: currentTicker, expiration: exp })
});
optionsChainData = await res.json();
renderOptionsChain();
} catch (e) { console.error(e); }
});
document.querySelectorAll('[data-chain]').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('[data-chain]').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
showCallsOrPuts = btn.dataset.chain;
renderOptionsChain();
});
});
// Payoff Diagram
let payoffChartInstance = null;
document.getElementById("drawPayoffBtn")?.addEventListener("click", async () => {
const strike = parseFloat(document.getElementById("payoffStrike").value) || 100;
const premium = parseFloat(document.getElementById("payoffPremium").value) || 5;
const optType = document.getElementById("payoffType").value;
const dir = document.getElementById("payoffDirection").value;
const curPrice = optionsChainData?.current_price || strike;
try {
const res = await fetch("/api/options/payoff", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ strike, premium, option_type: optType, is_long: dir === 'long', current_price: curPrice })
});
const data = await res.json();
renderPayoffChart(data.points, strike);
} catch (e) { console.error(e); }
});
function renderPayoffChart(points, strike) {
const container = document.getElementById("payoffChart");
container.innerHTML = "";
if (payoffChartInstance) { payoffChartInstance.remove(); payoffChartInstance = null; }
const cc = getChartColors();
payoffChartInstance = LightweightCharts.createChart(container, {
width: container.clientWidth || 600, height: 300,
layout: { background: { color: cc.bg }, textColor: cc.text },
grid: { vertLines: { color: cc.grid }, horzLines: { color: cc.grid } },
rightPriceScale: { borderColor: cc.border },
});
const series = payoffChartInstance.addLineSeries({ color: '#6366f1', lineWidth: 2 });
const d = points.map((p, i) => ({ time: i + 1, value: p.profit }));
series.setData(d);
// Add zero line
const zeroLine = payoffChartInstance.addLineSeries({ color: 'rgba(255,255,255,0.2)', lineWidth: 1, lineStyle: 2 });
zeroLine.setData(d.map(p => ({ time: p.time, value: 0 })));
payoffChartInstance.timeScale().fitContent();
}
// ══════════════ MARKET DASHBOARD ══════════════
async function loadMarketDashboard() {
try {
const res = await fetch("/api/market-summary");
const data = await res.json();
if (data.error) { console.error(data.error); return; }
renderMarketCards("dashIndices", data.indices || []);
renderMarketCards("dashCommodities", data.commodities || []);
renderMarketCards("dashCurrencies", data.currencies || []);
} catch (e) { console.error("Market dashboard failed:", e); }
}
function renderMarketCards(containerId, items) {
const container = document.getElementById(containerId);
if (!container || !items.length) return;
container.innerHTML = items.map(item => {
const sign = item.change >= 0 ? '+' : '';
const cls = item.change >= 0 ? 'positive' : 'negative';
const arrow = item.change >= 0 ? '▲' : '▼';
return `
${item.name}
${item.price > 0 ? item.price.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 }) : '—'}
${arrow} ${sign}${item.change.toFixed(2)} (${sign}${item.change_pct.toFixed(2)}%)
`;
}).join('');
// Animate counters
container.querySelectorAll('.mc-price[data-target]').forEach(el => {
animateCounter(el, parseFloat(el.dataset.target));
});
}
function animateCounter(el, target) {
if (!target || target <= 0) return;
const duration = 800;
const start = performance.now();
const startVal = 0;
function tick(now) {
const elapsed = now - start;
const progress = Math.min(elapsed / duration, 1);
const eased = 1 - Math.pow(1 - progress, 3); // easeOutCubic
const current = startVal + (target - startVal) * eased;
el.textContent = current.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 });
if (progress < 1) requestAnimationFrame(tick);
}
requestAnimationFrame(tick);
}
// Auto-refresh dashboard every 60s
let dashboardInterval = null;
function startDashboardRefresh() {
if (dashboardInterval) clearInterval(dashboardInterval);
loadMarketDashboard();
dashboardInterval = setInterval(loadMarketDashboard, 60000);
}
// ══════════════ PORTFOLIO ══════════════
let equityCurveChart = null;
async function loadPortfolio() {
try {
const res = await fetch("/api/portfolio");
const data = await res.json();
if (data.error) { console.error(data.error); return; }
renderPortfolioSummary(data);
renderPositionsTable(data.positions);
renderPendingOrders(data.pending_orders || []);
loadPortfolioAnalytics();
loadTransactionHistory();
loadEquityCurve();
} catch (e) { console.error("Portfolio load failed:", e); }
}
function renderPortfolioSummary(data) {
const fmt = (v) => '$' + v.toLocaleString(undefined, { minimumFractionDigits: 2 });
const pnlClass = data.total_pnl >= 0 ? 'positive' : 'negative';
document.getElementById("pfTotalValue").textContent = fmt(data.total_value);
document.getElementById("pfCash").textContent = fmt(data.cash);
const bpEl = document.getElementById("pfBuyingPower");
if (bpEl) bpEl.textContent = fmt(data.buying_power || data.cash);
document.getElementById("pfPositionsValue").textContent = fmt(data.positions_value);
const muEl = document.getElementById("pfMarginUsed");
if (muEl) muEl.textContent = fmt(data.margin_used || 0);
const pnlEl = document.getElementById("pfPnL");
pnlEl.textContent = (data.total_pnl >= 0 ? '+' : '') + fmt(data.total_pnl);
pnlEl.className = 'data-value ' + pnlClass;
const retEl = document.getElementById("pfReturnPct");
retEl.textContent = (data.total_pnl_pct >= 0 ? '+' : '') + data.total_pnl_pct.toFixed(2) + '%';
retEl.className = 'data-value ' + pnlClass;
// Update badges
const slipEl = document.getElementById("pfSlippageLabel");
if (slipEl && data.slippage_bps !== undefined) slipEl.textContent = `Slip: ${data.slippage_bps} bps`;
const commEl = document.getElementById("pfCommLabel");
if (commEl && data.commission_per_share !== undefined) commEl.textContent = `Comm: $${data.commission_per_share}/sh`;
}
function renderPositionsTable(positions) {
const container = document.getElementById("positionsTable");
if (!positions || !positions.length) {
container.innerHTML = 'No open positions. Use the Trade Terminal to buy your first asset.
';
return;
}
let html = `
| Ticker | Side | Type | Shares | Avg Cost | Current | Value | P&L | P&L % | Alloc % |
`;
for (const p of positions) {
const pnlClass = p.pnl >= 0 ? 'positive' : 'negative';
const sideClass = p.side === 'LONG' ? 'positive' : 'negative';
html += `
| ${p.ticker} |
${p.side} |
${p.asset_type} |
${p.shares} |
$${p.avg_cost.toFixed(2)} |
$${p.current_price.toFixed(2)} |
$${p.market_value.toLocaleString(undefined, { minimumFractionDigits: 2 })} |
${p.pnl >= 0 ? '+' : ''}$${p.pnl.toFixed(2)} |
${p.pnl_pct >= 0 ? '+' : ''}${p.pnl_pct.toFixed(2)}% |
${p.allocation_pct.toFixed(1)}% |
`;
}
html += '
';
container.innerHTML = html;
}
function renderPendingOrders(orders) {
const container = document.getElementById("pendingOrdersTable");
if (!container) return;
if (!orders || !orders.length) {
container.innerHTML = 'No pending orders.
';
return;
}
let html = `
| ID | Type | Side | Ticker | Shares | Target Price | Created | Action |
`;
for (const o of orders) {
html += `
| #${o.id} |
${o.order_type} |
${o.side} |
${o.ticker} |
${o.shares} |
$${o.target_price.toFixed(2)} |
${o.created_at} |
|
`;
}
html += '
';
container.innerHTML = html;
}
async function cancelPendingOrder(orderId) {
try {
await fetch("/api/portfolio/cancel-order", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ order_id: orderId })
});
loadPortfolio();
} catch (e) { console.error(e); }
}
async function loadEquityCurve() {
try {
const res = await fetch("/api/portfolio/equity-curve");
const data = await res.json();
if (!data.curve || data.curve.length < 2) return;
const container = document.getElementById("equityCurveContainer");
if (!container) return;
if (equityCurveChart) {
equityCurveChart.remove();
equityCurveChart = null;
}
const isDark = document.documentElement.getAttribute('data-theme') !== 'light';
equityCurveChart = LightweightCharts.createChart(container, {
width: container.clientWidth,
height: 260,
layout: { background: { type: 'solid', color: 'transparent' }, textColor: isDark ? '#94a3b8' : '#64748b' },
grid: { vertLines: { visible: false }, horzLines: { color: isDark ? 'rgba(255,255,255,0.04)' : 'rgba(0,0,0,0.06)' } },
rightPriceScale: { borderVisible: false },
timeScale: { borderVisible: false },
});
const series = equityCurveChart.addAreaSeries({
topColor: 'rgba(99,102,241,0.3)',
bottomColor: 'rgba(99,102,241,0.02)',
lineColor: '#6366f1',
lineWidth: 2,
});
const chartData = data.curve.map((pt, i) => ({
time: pt.timestamp.split('T')[0] || pt.timestamp.split(' ')[0],
value: pt.total_value
}));
// Deduplicate by time (keep last)
const seen = {};
const uniqueData = [];
for (const d of chartData) {
seen[d.time] = d;
}
for (const key of Object.keys(seen).sort()) {
uniqueData.push(seen[key]);
}
if (uniqueData.length >= 2) {
series.setData(uniqueData);
equityCurveChart.timeScale().fitContent();
}
new ResizeObserver(() => {
equityCurveChart?.applyOptions({ width: container.clientWidth });
}).observe(container);
} catch (e) { console.error("Equity curve:", e); }
}
async function loadPortfolioAnalytics() {
try {
const res = await fetch("/api/portfolio/analytics");
const data = await res.json();
const container = document.getElementById("portfolioAnalytics");
container.innerHTML = `
Sharpe Ratio
${data.sharpe_ratio}
Sortino Ratio
${data.sortino_ratio || 0}
Calmar Ratio
${data.calmar_ratio || 0}
Profit Factor
${data.profit_factor || 0}
Max Drawdown
${data.max_drawdown_pct}%
Win Rate
${data.win_rate}%
Total Trades
${data.total_trades}
Winning
${data.winning_trades}
Losing
${data.losing_trades}
Avg Loss
$${data.avg_loss}
Best Trade
$${data.best_trade}
Worst Trade
$${data.worst_trade}
Gross Profit
$${data.gross_profit || 0}
Gross Loss
$${data.gross_loss || 0}
Total Commission
${data.total_commission || 0}
Total Slippage
${data.total_slippage || 0}
Total Return
${data.total_return_pct}%
`;
} catch (e) { console.error(e); }
}
async function loadTransactionHistory() {
try {
const res = await fetch("/api/portfolio/history");
const data = await res.json();
const container = document.getElementById("transactionHistory");
if (!data.transactions || !data.transactions.length) {
container.innerHTML = 'No transactions yet.
';
return;
}
let html = `
| Date | Action | Side | Ticker | Shares | Price | Slip | Comm | Total | P&L |
`;
for (const t of data.transactions) {
const actionColors = { BUY: 'positive', SELL: 'negative', SHORT: 'negative', COVER: 'positive' };
const actionClass = actionColors[t.action] || '';
const pnlStr = t.pnl != null ? `${t.pnl >= 0 ? '+' : ''}$${t.pnl.toFixed(2)}` : '—';
html += `
| ${t.timestamp} |
${t.action} |
${t.side || 'LONG'} |
${t.ticker} |
${t.shares} |
$${t.price.toFixed(4)} |
$${(t.slippage || 0).toFixed(4)} |
$${(t.commission || 0).toFixed(2)} |
$${t.total.toFixed(2)} |
${pnlStr} |
`;
}
html += '
';
container.innerHTML = html;
} catch (e) { console.error(e); }
}
// ── Trade Terminal Controls ──────────────────────
// Order type toggle: show/hide limit price field
document.getElementById("tradeOrderType")?.addEventListener("change", (e) => {
const priceField = document.getElementById("tradeLimitPrice");
if (priceField) {
priceField.style.display = (e.target.value === 'market') ? 'none' : '';
}
});
// Trade search autocomplete
let tradeSearchTimeout = null;
document.getElementById("tradeTicker")?.addEventListener("input", async (e) => {
const q = e.target.value.trim();
const dropdown = document.getElementById("tradeDropdown");
if (!dropdown) return;
if (q.length < 1) { dropdown.classList.remove("open"); return; }
clearTimeout(tradeSearchTimeout);
tradeSearchTimeout = setTimeout(async () => {
try {
const res = await fetch(`/api/suggest?q=${encodeURIComponent(q)}`);
const results = await res.json();
if (!results.length) { dropdown.classList.remove("open"); return; }
dropdown.innerHTML = results.slice(0, 8).map(r =>
`
${r.ticker}
${r.name}
`
).join('');
dropdown.classList.add("open");
dropdown.querySelectorAll('.trade-dropdown-item').forEach(item => {
item.addEventListener('click', () => {
document.getElementById("tradeTicker").value = item.dataset.ticker;
dropdown.classList.remove("open");
});
});
} catch (e) { dropdown.classList.remove("open"); }
}, 250);
});
// Close dropdown on outside click
document.addEventListener('click', (e) => {
const dropdown = document.getElementById("tradeDropdown");
const input = document.getElementById("tradeTicker");
if (dropdown && !dropdown.contains(e.target) && e.target !== input) {
dropdown.classList.remove("open");
}
});
// Helper to get trade params
function getTradeParams() {
const ticker = document.getElementById("tradeTicker").value.trim().toUpperCase();
const shares = parseFloat(document.getElementById("tradeShares").value);
const assetType = document.getElementById("tradeAssetType").value;
const orderType = document.getElementById("tradeOrderType")?.value || 'market';
const limitPrice = parseFloat(document.getElementById("tradeLimitPrice")?.value) || 0;
return { ticker, shares, assetType, orderType, limitPrice };
}
async function executeTrade(action) {
const { ticker, shares, assetType, orderType, limitPrice } = getTradeParams();
const resultEl = document.getElementById("tradeResult");
if (!ticker || !shares || shares <= 0) {
resultEl.innerHTML = 'Enter a valid ticker and quantity';
return;
}
// Handle limit/stop orders
if (orderType !== 'market' && (action === 'buy' || action === 'sell')) {
if (limitPrice <= 0) {
resultEl.innerHTML = 'Enter a valid price for limit/stop order';
return;
}
const endpoint = orderType === 'limit' ? '/api/portfolio/limit-order' : '/api/portfolio/stop-order';
resultEl.innerHTML = 'Placing order...';
try {
const res = await fetch(endpoint, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
ticker, shares, price: limitPrice,
side: action === 'buy' ? 'BUY' : 'SELL',
asset_type: assetType
})
});
const data = await res.json();
if (data.error) {
resultEl.innerHTML = `${data.error}`;
} else {
resultEl.innerHTML = `${orderType.toUpperCase()} order placed: ${data.side} ${data.shares} ${data.ticker} @ $${data.target_price}`;
loadPortfolio();
}
} catch (e) { resultEl.innerHTML = `Error: ${e.message}`; }
return;
}
// Market orders
const endpoints = {
buy: '/api/portfolio/buy',
sell: '/api/portfolio/sell',
short: '/api/portfolio/short',
cover: '/api/portfolio/cover'
};
resultEl.innerHTML = 'Processing...';
try {
const res = await fetch(endpoints[action], {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ ticker, shares, asset_type: assetType })
});
const data = await res.json();
if (data.error) {
resultEl.innerHTML = `${data.error}`;
} else {
const labels = { buy: 'Bought', sell: 'Sold', short: 'Shorted', cover: 'Covered' };
let msg = `${labels[action]} ${data.shares} ${data.ticker} @ $${data.price.toFixed(2)}`;
if (data.commission) msg += ` | Comm: $${data.commission.toFixed(2)}`;
if (data.pnl !== undefined) msg += ` | P&L: ${data.pnl >= 0 ? '+' : ''}$${data.pnl.toFixed(2)}`;
resultEl.innerHTML = `${msg}`;
loadPortfolio();
}
} catch (e) { resultEl.innerHTML = `Error: ${e.message}`; }
}
// Button handlers
document.getElementById("buyBtn")?.addEventListener("click", () => executeTrade('buy'));
document.getElementById("sellBtn")?.addEventListener("click", () => executeTrade('sell'));
document.getElementById("shortBtn")?.addEventListener("click", () => executeTrade('short'));
document.getElementById("coverBtn")?.addEventListener("click", () => executeTrade('cover'));
// Reset portfolio
document.getElementById("resetPortfolioBtn")?.addEventListener("click", async () => {
if (!confirm("Reset portfolio to $100,000? All positions, orders, and history will be deleted.")) return;
try {
await fetch("/api/portfolio/reset", { method: "POST" });
loadPortfolio();
} catch (e) { console.error(e); }
});
// Load portfolio when tab is clicked
document.querySelector('.nav-item[data-tab="portfolio"]')?.addEventListener("click", () => {
loadPortfolio();
});
// ── App Init ──────────────────────────────────
document.addEventListener("DOMContentLoaded", () => {
// Load market dashboard when app starts (after welcome dismiss)
const letsBeginBtn = document.getElementById("letsBeginBtn");
if (letsBeginBtn) {
const origHandler = letsBeginBtn.onclick;
letsBeginBtn.addEventListener("click", () => {
setTimeout(() => startDashboardRefresh(), 500);
});
}
// Also load portfolio after a delay
setTimeout(() => loadPortfolio(), 1500);
});