diff --git "a/static/app.js" "b/static/app.js" --- "a/static/app.js" +++ "b/static/app.js" @@ -1,1193 +1,1196 @@ -// Global Variables -let debounceTimer; - -// --- INITIALIZATION --- -document.addEventListener('DOMContentLoaded', () => { - - // Expandable Cards to Modal Logic - document.addEventListener('click', (e) => { - const card = e.target.closest('.expandable-card'); - const isExpandBtn = e.target.closest('.card-expand-btn'); - if (card && (isExpandBtn || e.target.tagName === 'H3' || e.target.closest('.card-header-flex'))) { - // Extract content - const title = card.querySelector('h3').innerText; - const bodyHtml = card.querySelector('.card-body').innerHTML; - - // Populate modal - document.getElementById('modalTitle').innerText = title; - document.getElementById('modalBody').innerHTML = bodyHtml; - - // Show modal - document.getElementById('globalModal').classList.add('show'); - } - }); - -window.closeModal = function() { - document.getElementById('globalModal').classList.remove('show'); -}; - -window.toggleSidebar = function() { - const sidebar = document.getElementById('appSidebar'); - const mainContent = document.querySelector('.main-content'); - if(sidebar) sidebar.classList.toggle('open'); - if(mainContent) mainContent.classList.toggle('sidebar-open'); -}; - - // Mouse Glow Tracking - document.addEventListener("mousemove", (e) => { - document.querySelectorAll(".mouse-glow, .glass-panel, .expandable-card").forEach((el) => { - const rect = el.getBoundingClientRect(); - el.style.setProperty("--mouse-x", `${e.clientX - rect.left}px`); - el.style.setProperty("--mouse-y", `${e.clientY - rect.top}px`); - el.classList.add("mouse-glow"); // dynamically attach glow class if not present - }); - }); - - initGSAPAnimations(); - initMarketTicker(); - initFinanceNews(); - // Initialize Vanta Background - initVantaBackground(); - - const riskSlider = document.getElementById('risk'); - const riskVal = document.getElementById('riskVal'); - if (riskSlider && riskVal) { - riskSlider.addEventListener('input', (e) => { - riskVal.textContent = e.target.value; - // GSAP tactical feedback animation - gsap.fromTo(riskVal, - { scale: 1.5, color: '#3b82f6', textShadow: '0 0 20px #3b82f6' }, - { scale: 1, color: '#f8fafc', textShadow: 'none', duration: 0.4, ease: "back.out(1.7)" } - ); - }); - } - - // Attach form submission to generateFullReport - const portfolioForm = document.getElementById('portfolioForm'); - if (portfolioForm) { - portfolioForm.addEventListener('submit', async (e) => { - e.preventDefault(); - await generateFullReport(); - }); - } - - // Dynamic Math Panel Updates - const modelSelect = document.getElementById('model'); - if (modelSelect) { - modelSelect.addEventListener('change', (e) => { - const mathFormula = document.getElementById('active-math-formula'); - const mathDesc = document.getElementById('active-math-desc'); - const val = e.target.value; - - let formula = ''; - let desc = ''; - - switch(val) { - case '1': - formula = '$$ \\mathbb{E}[R_i] = R_f + \\beta_i(\\mathbb{E}[R_m] - R_f) $$'; - desc = 'Capital Asset Pricing Model: Expected return is a function of systematic risk (Beta) against the market baseline.'; - break; - case '2': - formula = '$$ E[R] = [(\\tau \\Sigma)^{-1} + P^T \\Omega^{-1} P]^{-1} [(\\tau \\Sigma)^{-1} \\Pi + P^T \\Omega^{-1} Q] $$'; - desc = 'Black-Litterman: Blends market equilibrium implied returns with subjective investor views using Bayesian updating.'; - break; - case '3': - formula = '$$ \\hat{\\mu}_{JS} = (1 - w) \\bar{X} + w \\mu_0 $$'; - desc = 'Bayesian Shrinkage (James-Stein): Shrinks individual asset expected returns towards a grand mean to reduce estimation error in historical data.'; - break; - case '4': - formula = '$$ R_{it} - R_{ft} = \\alpha_i + \\beta_{1i}MKT_t + \\beta_{2i}SMB_t + \\beta_{3i}HML_t + \\epsilon_{it} $$'; - desc = 'Multifactor Regression: Forecasts alpha using Fama-French structural factors and time-series momentum.'; - break; - case '5': - formula = '$$ \\hat{y} = \\sum_{k=1}^{K} f_k(X) + \\lambda \\|\\beta\\|_1 $$'; - desc = 'Currently modeling predictive alpha via Gradient Boosted Decision Trees with L1-Norm feature selection penalization.'; - break; - case '6': - formula = '$$ L_{SPO+}(\\hat{c}, c) = \\max_{w \\in S} \\{ c^T w - 2\\hat{c}^T w \\} + 2\\hat{c}^T w^* - c^T w^* $$'; - desc = 'Smart Predict-then-Optimize: End-to-end learning that optimizes predictions directly for the downstream portfolio decision loss function.'; - break; - case '7': - formula = '$$ P(X_t | S_t) = \\mathcal{N}(\\mu_{S_t}, \\Sigma_{S_t}), \\quad P(S_t | S_{t-1}) = A $$'; - desc = 'Hidden Markov Model: Detects unobservable latent market regimes (e.g. Bull vs Bear) to dynamically switch alpha models.'; - break; - } - - if (mathFormula && mathDesc) { - mathFormula.innerHTML = formula; - mathDesc.innerHTML = desc; - if (window.MathJax) { - MathJax.typesetPromise([mathFormula]); - } - } - }); - } - - // Suite Tabs logic - document.querySelectorAll('.suite-tab').forEach(tab => { - tab.addEventListener('click', (e) => { - document.querySelectorAll('.suite-tab').forEach(t => t.classList.remove('active')); - e.target.classList.add('active'); - // Currently all tabs just show the "View Comprehensive Report" button - }); - }); - - // (Duplicate form listener removed β€” already attached above) - - // Router History Listener - window.addEventListener('popstate', (e) => { - if (e.state && e.state.viewId) { - switchView(e.state.viewId, false); - } else { - // Handle hash fallback or default home - const hash = window.location.hash.replace('#', ''); - if (hash) { - switchView(hash, false); - } else { - switchView('hero', false); - } - } - }); - - // Check initial hash - const initialHash = window.location.hash.replace('#', ''); - if (initialHash) { - switchView(initialHash, false); - } -}); - -// --- NAVIGATION ROUTER --- -window.switchView = function(viewId, pushHistory = true) { - document.querySelectorAll('.view-section').forEach(el => { - el.classList.remove('active'); - el.style.opacity = 0; - }); - document.querySelectorAll('.sidebar-link').forEach(el => el.classList.remove('active')); - - const targetView = document.getElementById('view-' + viewId); - if(targetView) { - targetView.classList.add('active'); - // Elegant GSAP fade in - if (window.gsap) { - gsap.fromTo(targetView, - { opacity: 0, y: 30 }, - { opacity: 1, y: 0, duration: 0.6, ease: "power2.out" } - ); - } else { - targetView.style.opacity = 1; - } - } - - const link = document.querySelector(`.sidebar-link[data-target="${viewId}"]`); - if(link) link.classList.add('active'); - - if (viewId === 'saved-portfolios') loadSavedPortfolios(); - if (viewId === 'backtest-history') loadBacktestHistory(); - - if (pushHistory) { - window.history.pushState({ viewId: viewId }, '', '#' + viewId); - } -}; - -// --- GSAP ANIMATIONS --- -function initGSAPAnimations() { - if (typeof gsap === 'undefined') return; - - gsap.registerPlugin(ScrollTrigger); - - // Staggered entry for Model Zoo Cards - document.querySelectorAll('.zoo-grid').forEach(grid => { - const cards = grid.querySelectorAll('.expandable-card'); - if (cards.length === 0) return; - - gsap.fromTo(cards, - { opacity: 0, y: 50 }, - { - opacity: 1, - y: 0, - duration: 0.8, - stagger: 0.15, - ease: "power3.out", - scrollTrigger: { - trigger: grid, - start: "top 85%" - } - } - ); - }); -} - -// --- MARKET TICKER --- -async function initMarketTicker() { - const container = document.getElementById('liveTickerContent'); - if (!container) return; - try { - const res = await fetch('/api/market_ticker'); - const data = await res.json(); - if(data && Array.isArray(data) && data.length > 0) { - let html = ''; - // Duplicate array for seamless infinite scrolling - const displayData = [...data, ...data, ...data]; - displayData.forEach(item => { - let colorClass = item.change >= 0 ? 'color: #10b981;' : 'color: #ef4444;'; - let sign = item.change >= 0 ? '+' : ''; - html += `
- ${item.name} - $${item.price} - ${sign}${(item.change*100).toFixed(2)}% -
`; - }); - container.innerHTML = html; - } else { - container.innerHTML = "Market data temporarily unavailable β€” refresh in a moment"; - } - } catch(e) { - console.error("Ticker fetch failed:", e); - container.innerHTML = "Market data temporarily unavailable"; - } -} - -// --- FINANCE NEWS MOCKUP --- -async function initFinanceNews() { - try { - const res = await fetch('/api/finance_news'); - const data = await res.json(); - const container = document.getElementById('financeNewsContent'); - if (container && data && data.length > 0) { - let html = ''; - data.forEach(item => { - html += ` -
-
-
${item.source} • ${item.time}
-
${item.title}
-
-
`; - }); - container.innerHTML = html; - } - } catch(e) { - console.error("News fetch failed:", e); - } -} - -// --- PAYLOAD GENERATOR --- -function getPayload() { - let custom_constraints = []; - const advInput = document.getElementById('custom_constraints_input'); - if (advInput && advInput.value.trim() !== '') { - const lines = advInput.value.split('\n'); - lines.forEach(line => { - const parts = line.split(',').map(p => p.trim()); - if (parts.length === 3) { - let asset = parts[0]; - let direction = parts[1].toLowerCase(); - let limit = parseFloat(parts[2]); - if (!isNaN(limit)) { - custom_constraints.push({ - asset: asset, - direction: direction, - limit: limit / 100.0 - }); - } - } - }); - } - - return { - tickers: document.getElementById('tickers').value.split(',').map(t => t.trim()).filter(t => t), - capital: parseFloat(document.getElementById('capital').value) || 100000, - risk_input: parseInt(document.getElementById('risk').value), - model: parseInt(document.getElementById('model').value), - allocation_engine: parseInt(document.getElementById('allocation_engine').value), - allow_shorting: document.getElementById('allow_shorting').checked, - tax_enabled: document.getElementById('tax_enabled').checked, - garch_enabled: document.getElementById('garch_enabled').checked, - custom_constraints: custom_constraints - }; -} - -// --- HERO RADAR CHART --- -function initHeroRadar() { - const ctx = document.getElementById('heroRadarChart'); - if (!ctx) return; - - const data = { - labels: ['Value', 'Momentum', 'Quality', 'Low Volatility', 'Yield'], - datasets: [{ - label: 'Current Regime Exposure', - data: [65, 85, 40, 70, 50], - backgroundColor: 'rgba(96, 165, 250, 0.2)', - borderColor: 'rgba(96, 165, 250, 1)', - pointBackgroundColor: 'rgba(96, 165, 250, 1)', - pointBorderColor: '#fff', - pointHoverBackgroundColor: '#fff', - pointHoverBorderColor: 'rgba(96, 165, 250, 1)' - }] - }; - - const config = { - type: 'radar', - data: data, - options: { - responsive: true, - maintainAspectRatio: false, - scales: { - r: { - angleLines: { color: 'rgba(255, 255, 255, 0.1)' }, - grid: { color: 'rgba(255, 255, 255, 0.1)' }, - pointLabels: { color: '#94a3b8', font: { size: 11, family: 'Inter' } }, - ticks: { display: false, max: 100, min: 0 } - } - }, - plugins: { - legend: { display: false } - } - } - }; - - const chart = new Chart(ctx, config); - - // Simulate dynamic factor shifting - setInterval(() => { - chart.data.datasets[0].data = chart.data.datasets[0].data.map(val => { - let shift = (Math.random() - 0.5) * 15; - return Math.max(10, Math.min(100, val + shift)); - }); - chart.update('active'); - }, 3000); -} - - - -// --- FULL REPORT GENERATION --- -async function generateFullReport() { - const payload = getPayload(); - const accessKey = sessionStorage.getItem('accessKey') || ""; - - // Trigger Cinematic Matrix Loader - const matrixLoader = document.getElementById('matrix-loader'); - const matrixLogs = document.getElementById('matrix-logs'); - const matrixProgress = document.getElementById('matrix-progress'); - const matrixProgressText = document.getElementById('matrix-progress-text'); - - matrixLoader.style.display = 'flex'; - matrixLogs.innerHTML = ''; - matrixProgress.style.width = '0%'; - - const steps = [ - "Initializing quantitative core engine...", - "Fetching raw market time-series...", - "Inverting Covariance Matrix (Handling non-positive definiteness)...", - "Calculating Principal Components for factor extraction...", - "Solving constrained optimization via interior point method...", - "Executing probabilistic stress tests (Monte Carlo)...", - "Applying L1/L2 shrinkage penalties and bounds...", - "Converging Global Minimum / Maximum Sharpe targets...", - "Compiling mathematical HTML portfolio report..." - ]; - - let logIdx = 0; - const interval = setInterval(() => { - if(logIdx < steps.length) { - const el = document.createElement('div'); - el.style.margin = "2px 0"; - el.innerHTML = `> ${steps[logIdx]}`; - matrixLogs.appendChild(el); - - // Auto scroll to bottom - matrixLogs.scrollTop = matrixLogs.scrollHeight; - - // Progress bar (capping at 99% until fully complete) - let pct = Math.floor(Math.min(((logIdx + 1) / steps.length) * 99, 99)); - matrixProgress.style.width = `${pct}%`; - if(matrixProgressText) matrixProgressText.innerText = `${pct}%`; - logIdx++; - } - }, 600); // Fast cinematic log streaming - - try { - const res = await fetch('/api/generate', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'X-Access-Key': accessKey - }, - body: JSON.stringify(payload) - }); - - if (!res.ok) { - clearInterval(interval); - let errTxt = "Generation Failed"; - try { - const errData = await res.json(); - errTxt = errData.detail || errTxt; - } catch (e) {} - matrixLogs.innerHTML += `
> ERROR: ${errTxt}
`; - setTimeout(() => { matrixLoader.style.display = 'none'; }, 4000); - return; - } - - const data = await res.json(); - if (data.status !== "queued") { - clearInterval(interval); - matrixLogs.innerHTML += `
> Unexpected server response.
`; - setTimeout(() => { matrixLoader.style.display = 'none'; }, 3000); - return; - } - - const taskId = data.task_id; - matrixLogs.innerHTML += `
> Job ${taskId.substring(0,8)} queued. Polling compute engine...
`; - - const pollInterval = setInterval(async () => { - try { - const statusRes = await fetch(`/api/status/${taskId}`, { - headers: { 'X-Access-Key': accessKey } - }); - - if (!statusRes.ok) { - clearInterval(pollInterval); - clearInterval(interval); - matrixLogs.innerHTML += `
> Polling failed.
`; - setTimeout(() => { matrixLoader.style.display = 'none'; }, 3000); - return; - } - - const statusData = await statusRes.json(); - if (statusData.status === "completed") { - clearInterval(pollInterval); - clearInterval(interval); - matrixProgress.style.width = '100%'; - if(matrixProgressText) matrixProgressText.innerText = '100%'; - if (statusData.target_weights) { - sessionStorage.setItem("portfolio_context", JSON.stringify(statusData.target_weights)); - - // Save run to local history - try { - const hist = JSON.parse(localStorage.getItem('portfolio_history') || '[]'); - hist.unshift({ - id: taskId.substring(0,8), - date: new Date().toLocaleString(), - return: statusData.expected_return, - volatility: statusData.volatility, - sharpe: statusData.sharpe_ratio, - tickers: Object.keys(statusData.target_weights).length - }); - localStorage.setItem('portfolio_history', JSON.stringify(hist.slice(0, 10))); - } catch(e) {} - } - matrixLogs.innerHTML += `
> OPTIMIZATION COMPLETE. REDIRECTING...
`; - setTimeout(() => { - matrixLoader.style.display = 'none'; - window.openReportFrame(); - }, 1500); - } else if (statusData.status === "error") { - clearInterval(pollInterval); - clearInterval(interval); - matrixLogs.innerHTML += `
> CRITICAL ERROR: ${statusData.message}
`; - // Feed error state to AI - sessionStorage.setItem("portfolio_context", JSON.stringify({ - status: "failed", - error_log: statusData.message || "Unknown internal error", - timestamp: new Date().toISOString() - })); - setTimeout(() => { matrixLoader.style.display = 'none'; }, 4000); - } - } catch (err) { - // Ignore network blips during polling - } - }, 2000); - - } catch(err) { - clearInterval(interval); - matrixLogs.innerHTML += `
> CRITICAL ERROR: Network failure.
`; - setTimeout(() => { matrixLoader.style.display = 'none'; }, 4000); - } -} - - -// --- WIZARD LOGIC --- -window.nextWizardStep = function(step) { - document.querySelectorAll('.wizard-step').forEach(el => el.style.display = 'none'); - const target = document.getElementById('wizardStep' + step); - if(target) { - target.style.display = 'block'; - } -}; - -window.runWizard = async function() { - const macro = document.getElementById('wizardMacro').value; - const reaction = document.getElementById('wizardReaction').value; - const basket = document.getElementById('wizardBasket').value; - - // Auto-fill the Sandbox form under the hood - document.getElementById('tickers').value = basket; - - let risk = 5; - if (reaction === 'buy') risk = 2; - if (reaction === 'hold') risk = 5; - if (reaction === 'sell') risk = 8; - - document.getElementById('risk').value = risk; - document.getElementById('riskVal').textContent = risk; - - let model = 5; // XGBoost default - if (macro === 'growth') model = 4; // Fama-French - if (macro === 'recession') model = 7; // HMM - if (macro === 'inflation') model = 3; // Bayesian Shrinkage - - document.getElementById('model').value = model.toString(); - - // Switch to Sandbox view - const modal = document.getElementById('wizardOverlay'); - if(modal) modal.style.display = 'none'; - - switchView('sandbox'); - - // Trigger the full report generation automatically - await generateFullReport(); -}; - -// --- VANTA JS BACKGROUND --- -function initVantaBackground() { - const container = document.getElementById('vanta-bg'); - if (!container) return; - - try { - window.vantaEffect = VANTA.NET({ - el: "#vanta-bg", - mouseControls: true, - touchControls: true, - gyroControls: false, - minHeight: 200.00, - minWidth: 200.00, - scale: 1.00, - scaleMobile: 1.00, - color: 0x3b82f6, - backgroundColor: 0x050814, - points: 12.00, - maxDistance: 22.00, - spacing: 16.00 - }); - - // Ensure Vanta resizes correctly on window resize - window.addEventListener('resize', () => { - if (window.vantaEffect) { - window.vantaEffect.resize(); - } - }); - } catch (e) { - console.warn("Vanta JS failed to initialize:", e); - } -} - -// --- REPORT FRAME LOGIC --- -window.openReportFrame = async function() { - const reportContainer = document.getElementById('reportContainer'); - const reportView = document.getElementById('report-view'); - - // Check if report actually exists before opening iframe - try { - const checkRes = await fetch('/report'); - if (!checkRes.ok) { - alert("Report generation failed or returned a blank response. Check server logs."); - return; - } - } catch(e) { - alert("Error fetching report."); - return; - } - - document.querySelector('.main-content').style.display = 'none'; - document.querySelector('nav').style.display = 'none'; - document.querySelector('.market-ticker-bar').style.display = 'none'; - - reportContainer.style.display = 'block'; - reportView.src = '/report?t=' + new Date().getTime(); -}; - -window.closeReport = function() { - document.getElementById('reportContainer').style.display = 'none'; - document.querySelector('.main-content').style.display = 'block'; - document.querySelector('nav').style.display = 'flex'; - document.querySelector('.market-ticker-bar').style.display = 'flex'; -}; - -// Ambient Background relies entirely on Vanta JS now. - -// --- AUTHENTICATION FLOW --- -window.logout = async function() { - sessionStorage.removeItem('accessKey'); - sessionStorage.removeItem('isMaster'); - try { - await fetch('/api/logout', { method: 'POST' }); - } catch(e) {} - window.location.href = '/'; -}; - -// --- AI CHAT WIDGET LOGIC --- -document.addEventListener('DOMContentLoaded', () => { - // Clear stale optimization context on fresh page load so the AI doesn't hallucinate previous sessions - sessionStorage.removeItem("portfolio_context"); - - // Pre-populate some assets to make the UI look aliveviously orphaned - if (typeof initHeroRadar === 'function') { - initHeroRadar(); - } - - const chatToggleBtn = document.getElementById('chat-toggle-btn'); - const chatWindow = document.getElementById('chat-window'); - const chatCloseBtn = document.getElementById('chat-close-btn'); - const chatForm = document.getElementById('chat-form'); - const chatInput = document.getElementById('chat-input'); - const chatMessages = document.getElementById('chat-messages'); - - // Maintain conversation history locally - let chatHistory = []; - - if (chatToggleBtn && chatWindow) { - chatToggleBtn.addEventListener('click', () => { - chatWindow.style.display = 'flex'; - chatToggleBtn.style.transform = 'scale(0)'; - }); - - chatCloseBtn.addEventListener('click', () => { - chatWindow.style.display = 'none'; - chatToggleBtn.style.transform = 'scale(1)'; - }); - - chatForm.addEventListener('submit', async (e) => { - e.preventDefault(); - const msg = chatInput.value.trim(); - if (!msg) return; - - // Display user message - const userMsg = document.createElement('div'); - userMsg.style.cssText = "background: rgba(255,255,255,0.1); padding: 10px 14px; border-radius: 12px; border-top-right-radius: 4px; align-self: flex-end; max-width: 85%; color: white;"; - userMsg.innerText = msg; - chatMessages.appendChild(userMsg); - - chatInput.value = ''; - chatMessages.scrollTop = chatMessages.scrollHeight; - - // Save user message to history - chatHistory.push({ role: "user", content: msg }); - - // Display loading - const loadingMsg = document.createElement('div'); - loadingMsg.style.cssText = "color: #94a3b8; font-size: 0.9rem; margin-top: 4px; font-style: italic;"; - loadingMsg.innerHTML = `Thinking...`; - chatMessages.appendChild(loadingMsg); - chatMessages.scrollTop = chatMessages.scrollHeight; - - // Get context - let ctx = {}; - try { - ctx = JSON.parse(sessionStorage.getItem("portfolio_context") || "{}"); - } catch(e) {} - - try { - const res = await fetch('/api/chat', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'X-Access-Key': sessionStorage.getItem('accessKey') || '' - }, - body: JSON.stringify({ message: msg, history: chatHistory.slice(-10), portfolio_context: ctx }) - }); - - const data = await res.json(); - chatMessages.removeChild(loadingMsg); - - const responseText = data.response || data.reply || (data.detail ? "Error: " + data.detail : "No response from AI."); - chatHistory.push({ role: "assistant", content: responseText }); - - const aiMsg = document.createElement('div'); - aiMsg.style.cssText = "background: rgba(59, 130, 246, 0.1); padding: 12px 16px; border-radius: 12px; border-top-left-radius: 4px; align-self: flex-start; max-width: 85%; color: #e2e8f0; line-height: 1.5;"; - aiMsg.innerText = responseText; - chatMessages.appendChild(aiMsg); - chatMessages.scrollTop = chatMessages.scrollHeight; - } catch (err) { - chatMessages.removeChild(loadingMsg); - const errMsg = document.createElement('div'); - errMsg.style.cssText = "color: #ef4444; font-size: 0.9rem;"; - errMsg.innerText = "Connection failed. Please try again."; - chatMessages.appendChild(errMsg); - } - }); - } -}); - -// --- AI STRATEGY GENERATOR --- -window.generateAIStrategy = async function() { - const inputEl = document.getElementById('ai-strategy-input'); - const btn = document.getElementById('ai-strategy-btn'); - const query = inputEl.value.trim(); - if (!query) return; - - btn.disabled = true; - const origText = btn.innerText; - btn.innerText = "Thinking..."; - - try { - const accessKey = sessionStorage.getItem('accessKey'); - const res = await fetch('/api/generate_strategy', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'X-Access-Key': accessKey || '' - }, - body: JSON.stringify({ query: query }) - }); - - const data = await res.json(); - if (data.status === "success" && data.config) { - const config = data.config; - if (config.tickers) document.getElementById('tickers').value = config.tickers; - if (config.capital) document.getElementById('capital').value = config.capital; - if (config.risk) { - document.getElementById('risk').value = config.risk; - document.getElementById('riskVal').textContent = config.risk; - } - if (config.model) document.getElementById('model').value = config.model; - if (config.allocation_engine) document.getElementById('allocation_engine').value = config.allocation_engine; - - if (config.allow_shorting !== undefined) document.getElementById('allow_shorting').checked = config.allow_shorting; - if (config.tax_enabled !== undefined) document.getElementById('tax_enabled').checked = config.tax_enabled; - if (config.garch_enabled !== undefined) document.getElementById('garch_enabled').checked = config.garch_enabled; - - // Trigger animation or feedback - inputEl.style.borderColor = "#10b981"; - setTimeout(() => inputEl.style.borderColor = "rgba(255, 255, 255, 0.1)", 2000); - } else { - alert(data.detail || "Failed to generate strategy."); - } - } catch (e) { - console.error("AI Strategy Generator failed:", e); - alert("Network error."); - } - - btn.disabled = false; - btn.innerText = origText; -} - -// --- REPORT FRAME LOGIC --- -window.openReportFrame = async function() { - const reportContainer = document.getElementById('reportContainer'); - const reportView = document.getElementById('report-view'); - - // Check if report actually exists before opening iframe - try { - const checkRes = await fetch('/report'); - if (!checkRes.ok) { - alert("Report generation failed or returned a blank response. Check server logs."); - return; - } - } catch(e) { - alert("Error fetching report."); - return; - } - - document.querySelector('.main-content').style.display = 'none'; - document.querySelector('nav').style.display = 'none'; - document.querySelector('.market-ticker-bar').style.display = 'none'; - - reportContainer.style.display = 'block'; - reportView.src = '/report?t=' + new Date().getTime(); -}; - -window.closeReport = function() { - document.getElementById('reportContainer').style.display = 'none'; - document.querySelector('.main-content').style.display = 'block'; - document.querySelector('nav').style.display = 'flex'; - document.querySelector('.market-ticker-bar').style.display = 'flex'; -}; - -// Ambient Background relies entirely on Vanta JS now. - -// --- AUTHENTICATION FLOW --- -window.logout = function() { - sessionStorage.removeItem('accessKey'); - window.location.href = '/'; -}; - -// --- AI CHAT WIDGET LOGIC --- -document.addEventListener('DOMContentLoaded', () => { - // Clear stale optimization context on fresh page load so the AI doesn't hallucinate previous sessions - sessionStorage.removeItem("portfolio_context"); - - // Pre-populate some assets to make the UI look aliveviously orphaned - if (typeof initHeroRadar === 'function') { - initHeroRadar(); - } - - const chatToggleBtn = document.getElementById('chat-toggle-btn'); - const chatWindow = document.getElementById('chat-window'); - const chatCloseBtn = document.getElementById('chat-close-btn'); - const chatForm = document.getElementById('chat-form'); - const chatInput = document.getElementById('chat-input'); - const chatMessages = document.getElementById('chat-messages'); - - // Maintain conversation history locally - let chatHistory = []; - - if (chatToggleBtn && chatWindow) { - chatToggleBtn.addEventListener('click', () => { - chatWindow.style.display = 'flex'; - chatToggleBtn.style.transform = 'scale(0)'; - }); - - chatCloseBtn.addEventListener('click', () => { - chatWindow.style.display = 'none'; - chatToggleBtn.style.transform = 'scale(1)'; - }); - - chatForm.addEventListener('submit', async (e) => { - e.preventDefault(); - const msg = chatInput.value.trim(); - if (!msg) return; - - // Display user message - const userMsg = document.createElement('div'); - userMsg.style.cssText = "background: rgba(255,255,255,0.1); padding: 10px 14px; border-radius: 12px; border-top-right-radius: 4px; align-self: flex-end; max-width: 85%; color: white;"; - userMsg.innerText = msg; - chatMessages.appendChild(userMsg); - - chatInput.value = ''; - chatMessages.scrollTop = chatMessages.scrollHeight; - - // Save user message to history - chatHistory.push({ role: "user", content: msg }); - - // Display loading - const loadingMsg = document.createElement('div'); - loadingMsg.style.cssText = "color: #94a3b8; font-size: 0.9rem; margin-top: 4px; font-style: italic;"; - loadingMsg.innerHTML = `Thinking...`; - chatMessages.appendChild(loadingMsg); - chatMessages.scrollTop = chatMessages.scrollHeight; - - // Get context - let ctx = {}; - try { - ctx = JSON.parse(sessionStorage.getItem("portfolio_context") || "{}"); - } catch(e) {} - - try { - const res = await fetch('/api/chat', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'X-Access-Key': sessionStorage.getItem('accessKey') || '' - }, - body: JSON.stringify({ message: msg, history: chatHistory.slice(-10), portfolio_context: ctx }) - }); - - const data = await res.json(); - chatMessages.removeChild(loadingMsg); - - const responseText = data.response || data.reply || (data.detail ? "Error: " + data.detail : "No response from AI."); - chatHistory.push({ role: "assistant", content: responseText }); - - const aiMsg = document.createElement('div'); - aiMsg.style.cssText = "background: rgba(59, 130, 246, 0.1); padding: 12px 16px; border-radius: 12px; border-top-left-radius: 4px; align-self: flex-start; max-width: 85%; color: #e2e8f0; line-height: 1.5;"; - aiMsg.innerText = responseText; - chatMessages.appendChild(aiMsg); - chatMessages.scrollTop = chatMessages.scrollHeight; - } catch (err) { - chatMessages.removeChild(loadingMsg); - const errMsg = document.createElement('div'); - errMsg.style.cssText = "color: #ef4444; font-size: 0.9rem;"; - errMsg.innerText = "Connection failed. Please try again."; - chatMessages.appendChild(errMsg); - } - }); - } -}); - -// --- AI STRATEGY GENERATOR --- -window.generateAIStrategy = async function() { - const inputEl = document.getElementById('ai-strategy-input'); - const btn = document.getElementById('ai-strategy-btn'); - const query = inputEl.value.trim(); - if (!query) return; - - btn.disabled = true; - const origText = btn.innerText; - btn.innerText = "Thinking..."; - - try { - const accessKey = sessionStorage.getItem('accessKey'); - const res = await fetch('/api/generate_strategy', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'X-Access-Key': accessKey || '' - }, - body: JSON.stringify({ query: query }) - }); - - const data = await res.json(); - if (data.status === "success" && data.config) { - const config = data.config; - if (config.tickers) document.getElementById('tickers').value = config.tickers; - if (config.capital) document.getElementById('capital').value = config.capital; - if (config.risk) { - document.getElementById('risk').value = config.risk; - document.getElementById('riskVal').textContent = config.risk; - } - if (config.model) document.getElementById('model').value = config.model; - if (config.allocation_engine) document.getElementById('allocation_engine').value = config.allocation_engine; - - if (config.allow_shorting !== undefined) document.getElementById('allow_shorting').checked = config.allow_shorting; - if (config.tax_enabled !== undefined) document.getElementById('tax_enabled').checked = config.tax_enabled; - if (config.garch_enabled !== undefined) document.getElementById('garch_enabled').checked = config.garch_enabled; - - // Trigger animation or feedback - inputEl.style.borderColor = "#10b981"; - setTimeout(() => inputEl.style.borderColor = "rgba(255, 255, 255, 0.1)", 2000); - } else { - alert(data.detail || "Failed to generate strategy."); - } - } catch (e) { - console.error("AI Strategy Generator failed:", e); - alert("Network error."); - } - - btn.disabled = false; - btn.innerText = origText; -}; - -// --- HISTORY MODAL --- -window.viewHistory = function() { - let hist = []; - try { - hist = JSON.parse(localStorage.getItem('portfolio_history') || '[]'); - } catch(e) {} - - let html = '
Compare previous institutional backtests side-by-side.
'; - if (hist.length === 0) { - html += '
No history found. Run an optimization first.
'; - } else { - html += '
'; - hist.forEach(run => { - const ret = run.return ? (run.return * 100).toFixed(2) + '%' : 'N/A'; - const vol = run.volatility ? (run.volatility * 100).toFixed(2) + '%' : 'N/A'; - const sharpe = run.sharpe ? run.sharpe.toFixed(2) : 'N/A'; - - html += ` -
-
- ID: ${run.id.substring(0,8)} - ${run.date.split(',')[0]} -
-
${ret}
-
- Volatility: - ${vol} -
-
- Sharpe Ratio: - ${sharpe} -
-
`; - }); - html += '
'; - } - - document.getElementById('modalTitle').innerText = "Backtest History & Comparison"; - document.getElementById('modalBody').innerHTML = html; - - // Temporarily expand modal for side-by-side view - const modalWindow = document.querySelector('#globalModal .modal-window'); - modalWindow.setAttribute('data-orig-max-width', modalWindow.style.maxWidth); - modalWindow.style.maxWidth = "900px"; - - document.getElementById('globalModal').classList.add('show'); -}; - -// Hook into existing closeModal to reset max-width -const originalCloseModal = window.closeModal; -window.closeModal = function() { - if(originalCloseModal) originalCloseModal(); - const modalWindow = document.querySelector('#globalModal .modal-window'); - setTimeout(() => { - if(modalWindow.hasAttribute('data-orig-max-width')) { - modalWindow.style.maxWidth = modalWindow.getAttribute('data-orig-max-width'); - } - }, 300); -}; - -const getHeaders = () => { - // 'accessKey' is stored at login (camelCase) - fixed from snake_case mismatch - const access_key = sessionStorage.getItem('accessKey') || localStorage.getItem('accessKey') || ''; - return { - 'Content-Type': 'application/json', - 'X-Access-Key': access_key - }; -}; -}; - -async function loadSavedPortfolios() { - try { - const response = await fetch('/api/portfolios', { headers: getHeaders() }); - const data = await response.json(); - const grid = document.getElementById('portfolio-grid'); - grid.innerHTML = ''; - if (data.length === 0) { - grid.innerHTML = '
No saved portfolios yet.
'; - return; - } - data.forEach(p => { - const weightsPreview = Object.entries(p.weights).map(([k,v]) => `${k}: ${(v*100).toFixed(1)}%`).join(', '); - grid.innerHTML += `
-

${p.name}

-

${new Date(p.created_at).toLocaleDateString()}

-

${weightsPreview}

-
- - -
-
`; - }); - } catch (err) { - console.error('Error loading saved portfolios:', err); - } -} - -async function deleteSavedPortfolio(id) { - if(!confirm("Are you sure you want to delete this saved portfolio?")) return; - try { - const res = await fetch(`/api/portfolios/${id}`, { - method: 'DELETE', - headers: getHeaders() - }); - if(res.ok) { - loadSavedPortfolios(); - } else { - alert('Failed to delete.'); - } - } catch(e) { console.error(e); } -} - -function loadPortfolioIntoSandbox(tickers) { - document.getElementById('tickers').value = tickers.join(', '); - switchView('sandbox', false); - document.getElementById('tickers').focus(); -} - -async function loadBacktestHistory() { - try { - const response = await fetch('/api/backtests', { headers: getHeaders() }); - const data = await response.json(); - const tbody = document.getElementById('backtest-table-body'); - tbody.innerHTML = ''; - - if (data.length === 0) { - tbody.innerHTML = 'No history available'; - return; - } - - data.forEach(run => { - const retClass = run.return_pct >= 0 ? 'color: #10b981;' : 'color: #ef4444;'; - tbody.innerHTML += ` - - ${new Date(run.executed_at).toLocaleString()} - ${run.model_used} - ${run.return_pct.toFixed(2)}% - ${run.sharpe_ratio.toFixed(2)} - - `; - }); - } catch (err) { - console.error('Error loading backtest history:', err); - } -} - -// Hook up webhook form -document.addEventListener('DOMContentLoaded', () => { - const form = document.getElementById('webhook-form'); - if(form) { - form.addEventListener('submit', async (e) => { - e.preventDefault(); - const url = document.getElementById('webhook-url').value; - try { - const res = await fetch('/api/webhooks/config', { - method: 'POST', - headers: getHeaders(), - body: JSON.stringify({url: url}) - }); - const data = await res.json(); - if(data.status === 'success') { - document.getElementById('webhook-status').style.display = 'block'; - document.getElementById('webhook-secret').value = data.api_secret_key; - setTimeout(() => { document.getElementById('webhook-status').style.display = 'none'; }, 3000); - } - } catch(err) { - console.error(err); - alert('Failed to save webhook config'); - } - }); - } -}); - - -async function saveCurrentPortfolio() { - const context = sessionStorage.getItem('portfolio_context'); - if (!context) { - alert('No portfolio configuration found to save. Generate a portfolio first.'); - return; - } - - let weights = {}; - try { - weights = JSON.parse(context); - } catch(e) { - alert('Invalid portfolio data.'); - return; - } - - const name = prompt('Enter a name for this portfolio:'); - if (!name || name.trim() === '') return; - - const accessKey = document.getElementById('accessKeyInput') ? document.getElementById('accessKeyInput').value : ''; - const tickers = Object.keys(weights); - - try { - const res = await fetch('/api/portfolios', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'X-Access-Key': accessKey - }, - body: JSON.stringify({ - name: name.trim(), - tickers: tickers, - weights: weights - }) - }); - - if (res.ok) { - alert('Portfolio saved successfully!'); - // Refresh list if open - loadSavedPortfolios(); - } else { - const err = await res.json(); - alert('Failed to save portfolio: ' + (err.detail || 'Unknown error')); - } - } catch (e) { - console.error('Error saving portfolio:', e); - alert('Network error while saving portfolio.'); - } -} - -// AI Sassiness logic and Logout override -document.addEventListener('DOMContentLoaded', () => { - if (sessionStorage.getItem('isMaster') === 'true') { - const chatMessages = document.getElementById('chat-messages'); - if (chatMessages && chatMessages.firstElementChild) { - chatMessages.firstElementChild.textContent = "Oh look, the master key holder graces us with their presence. Please try not to break the space-time continuum."; - } - } -}); - -// isMaster cleanup merged into main logout function +// Global Variables +let debounceTimer; +// --- INITIALIZATION --- +document.addEventListener('DOMContentLoaded', () => { + // Expandable Cards to Modal Logic + document.addEventListener('click', (e) => { + const card = e.target.closest('.expandable-card'); + const isExpandBtn = e.target.closest('.card-expand-btn'); + if (card && (isExpandBtn || e.target.tagName === 'H3' || e.target.closest('.card-header-flex'))) { + // Extract content + const title = card.querySelector('h3').innerText; + const bodyHtml = card.querySelector('.card-body').innerHTML; + // Populate modal + document.getElementById('modalTitle').innerText = title; + document.getElementById('modalBody').innerHTML = bodyHtml; + // Show modal + document.getElementById('globalModal').classList.add('show'); + } + }); +window.closeModal = function() { + document.getElementById('globalModal').classList.remove('show'); +}; +window.toggleSidebar = function() { + const sidebar = document.getElementById('appSidebar'); + const mainContent = document.querySelector('.main-content'); + if(sidebar) sidebar.classList.toggle('open'); + if(mainContent) mainContent.classList.toggle('sidebar-open'); +}; + // Mouse Glow Tracking + document.addEventListener("mousemove", (e) => { + document.querySelectorAll(".mouse-glow, .glass-panel, .expandable-card").forEach((el) => { + const rect = el.getBoundingClientRect(); + el.style.setProperty("--mouse-x", `${e.clientX - rect.left}px`); + el.style.setProperty("--mouse-y", `${e.clientY - rect.top}px`); + el.classList.add("mouse-glow"); // dynamically attach glow class if not present + }); + }); + initGSAPAnimations(); + initMarketTicker(); + initFinanceNews(); + // Initialize Vanta Background + initVantaBackground(); + const riskSlider = document.getElementById('risk'); + const riskVal = document.getElementById('riskVal'); + if (riskSlider && riskVal) { + riskSlider.addEventListener('input', (e) => { + riskVal.textContent = e.target.value; + // GSAP tactical feedback animation + gsap.fromTo(riskVal, + { scale: 1.5, color: '#3b82f6', textShadow: '0 0 20px #3b82f6' }, + { scale: 1, color: '#f8fafc', textShadow: 'none', duration: 0.4, ease: "back.out(1.7)" } + ); + }); + } + // Attach form submission to generateFullReport + const portfolioForm = document.getElementById('portfolioForm'); + if (portfolioForm) { + portfolioForm.addEventListener('submit', async (e) => { + e.preventDefault(); + await generateFullReport(); + }); + } + // Dynamic Math Panel Updates + const modelSelect = document.getElementById('model'); + if (modelSelect) { + modelSelect.addEventListener('change', (e) => { + const mathFormula = document.getElementById('active-math-formula'); + const mathDesc = document.getElementById('active-math-desc'); + const val = e.target.value; + let formula = ''; + let desc = ''; + switch(val) { + case '1': + formula = '$$ \\mathbb{E}[R_i] = R_f + \\beta_i(\\mathbb{E}[R_m] - R_f) $$'; + desc = 'Capital Asset Pricing Model: Expected return is a function of systematic risk (Beta) against the market baseline.'; + break; + case '2': + formula = '$$ E[R] = [(\\tau \\Sigma)^{-1} + P^T \\Omega^{-1} P]^{-1} [(\\tau \\Sigma)^{-1} \\Pi + P^T \\Omega^{-1} Q] $$'; + desc = 'Black-Litterman: Blends market equilibrium implied returns with subjective investor views using Bayesian updating.'; + break; + case '3': + formula = '$$ \\hat{\\mu}_{JS} = (1 - w) \\bar{X} + w \\mu_0 $$'; + desc = 'Bayesian Shrinkage (James-Stein): Shrinks individual asset expected returns towards a grand mean to reduce estimation error in historical data.'; + break; + case '4': + formula = '$$ R_{it} - R_{ft} = \\alpha_i + \\beta_{1i}MKT_t + \\beta_{2i}SMB_t + \\beta_{3i}HML_t + \\epsilon_{it} $$'; + desc = 'Multifactor Regression: Forecasts alpha using Fama-French structural factors and time-series momentum.'; + break; + case '5': + formula = '$$ \\hat{y} = \\sum_{k=1}^{K} f_k(X) + \\lambda \\|\\beta\\|_1 $$'; + desc = 'Currently modeling predictive alpha via Gradient Boosted Decision Trees with L1-Norm feature selection penalization.'; + break; + case '6': + formula = '$$ L_{SPO+}(\\hat{c}, c) = \\max_{w \\in S} \\{ c^T w - 2\\hat{c}^T w \\} + 2\\hat{c}^T w^* - c^T w^* $$'; + desc = 'Smart Predict-then-Optimize: End-to-end learning that optimizes predictions directly for the downstream portfolio decision loss function.'; + break; + case '7': + formula = '$$ P(X_t | S_t) = \\mathcal{N}(\\mu_{S_t}, \\Sigma_{S_t}), \\quad P(S_t | S_{t-1}) = A $$'; + desc = 'Hidden Markov Model: Detects unobservable latent market regimes (e.g. Bull vs Bear) to dynamically switch alpha models.'; + break; + } + if (mathFormula && mathDesc) { + mathFormula.innerHTML = formula; + mathDesc.innerHTML = desc; + if (window.MathJax) { + MathJax.typesetPromise([mathFormula]); + } + } + }); + } + // Suite Tabs logic + document.querySelectorAll('.suite-tab').forEach(tab => { + tab.addEventListener('click', (e) => { + document.querySelectorAll('.suite-tab').forEach(t => t.classList.remove('active')); + e.target.classList.add('active'); + // Currently all tabs just show the "View Comprehensive Report" button + }); + }); + // (Duplicate form listener removed β€” already attached above) + // Router History Listener + window.addEventListener('popstate', (e) => { + if (e.state && e.state.viewId) { + switchView(e.state.viewId, false); + } else { + // Handle hash fallback or default home + const hash = window.location.hash.replace('#', ''); + if (hash) { + switchView(hash, false); + } else { + switchView('hero', false); + } + } + }); + // Check initial hash + const initialHash = window.location.hash.replace('#', ''); + if (initialHash) { + switchView(initialHash, false); + } +}); +// --- NAVIGATION ROUTER --- +window.switchView = function(viewId, pushHistory = true) { + document.querySelectorAll('.view-section').forEach(el => { + el.classList.remove('active'); + el.style.opacity = 0; + }); + document.querySelectorAll('.sidebar-link').forEach(el => el.classList.remove('active')); + const targetView = document.getElementById('view-' + viewId); + if(targetView) { + targetView.classList.add('active'); + // Elegant GSAP fade in + if (window.gsap) { + gsap.fromTo(targetView, + { opacity: 0, y: 30 }, + { opacity: 1, y: 0, duration: 0.6, ease: "power2.out" } + ); + } else { + targetView.style.opacity = 1; + } + } + const link = document.querySelector(`.sidebar-link[data-target="${viewId}"]`); + if(link) link.classList.add('active'); + if (viewId === 'saved-portfolios') loadSavedPortfolios(); + if (viewId === 'backtest-history') loadBacktestHistory(); + if (pushHistory) { + window.history.pushState({ viewId: viewId }, '', '#' + viewId); + } +}; +// --- GSAP ANIMATIONS --- +function initGSAPAnimations() { + if (typeof gsap === 'undefined') return; + gsap.registerPlugin(ScrollTrigger); + // Staggered entry for Model Zoo Cards + document.querySelectorAll('.zoo-grid').forEach(grid => { + const cards = grid.querySelectorAll('.expandable-card'); + if (cards.length === 0) return; + gsap.fromTo(cards, + { opacity: 0, y: 50 }, + { + opacity: 1, + y: 0, + duration: 0.8, + stagger: 0.15, + ease: "power3.out", + scrollTrigger: { + trigger: grid, + start: "top 85%" + } + } + ); + }); +} +// --- MARKET TICKER --- +async function initMarketTicker() { + const container = document.getElementById('liveTickerContent'); + if (!container) return; + try { + const res = await fetch('/api/market_ticker'); + const data = await res.json(); + if(data && Array.isArray(data) && data.length > 0) { + let html = ''; + // Duplicate array for seamless infinite scrolling + const displayData = [...data, ...data, ...data]; + displayData.forEach(item => { + let colorClass = item.change >= 0 ? 'color: #10b981;' : 'color: #ef4444;'; + let sign = item.change >= 0 ? '+' : ''; + html += `
+ ${item.name} + $${item.price} + ${sign}${(item.change*100).toFixed(2)}% +
`; + }); + container.innerHTML = html; + } else { + container.innerHTML = "Market data temporarily unavailable β€” refresh in a moment"; + } + } catch(e) { + console.error("Ticker fetch failed:", e); + container.innerHTML = "Market data temporarily unavailable"; + } +} +// --- FINANCE NEWS MOCKUP --- +async function initFinanceNews() { + try { + const res = await fetch('/api/finance_news'); + const data = await res.json(); + const container = document.getElementById('financeNewsContent'); + if (container && data && data.length > 0) { + let html = ''; + data.forEach(item => { + html += ` +
+
+
${item.source} • ${item.time}
+
${item.title}
+
+
`; + }); + container.innerHTML = html; + } + } catch(e) { + console.error("News fetch failed:", e); + } +} +// --- PAYLOAD GENERATOR --- +function getPayload() { + let custom_constraints = []; + const advInput = document.getElementById('custom_constraints_input'); + if (advInput && advInput.value.trim() !== '') { + const lines = advInput.value.split('\n'); + lines.forEach(line => { + const parts = line.split(',').map(p => p.trim()); + if (parts.length === 3) { + let asset = parts[0]; + let direction = parts[1].toLowerCase(); + let limit = parseFloat(parts[2]); + if (!isNaN(limit)) { + custom_constraints.push({ + asset: asset, + direction: direction, + limit: limit / 100.0 + }); + } + } + }); + } + return { + tickers: document.getElementById('tickers').value.split(',').map(t => t.trim()).filter(t => t), + capital: parseFloat(document.getElementById('capital').value) || 100000, + risk_input: parseInt(document.getElementById('risk').value), + model: parseInt(document.getElementById('model').value), + allocation_engine: parseInt(document.getElementById('allocation_engine').value), + allow_shorting: document.getElementById('allow_shorting').checked, + tax_enabled: document.getElementById('tax_enabled').checked, + garch_enabled: document.getElementById('garch_enabled').checked, + custom_constraints: custom_constraints + }; +} +// --- HERO RADAR CHART --- +function initHeroRadar() { + const ctx = document.getElementById('heroRadarChart'); + if (!ctx) return; + const data = { + labels: ['Value', 'Momentum', 'Quality', 'Low Volatility', 'Yield'], + datasets: [{ + label: 'Current Regime Exposure', + data: [65, 85, 40, 70, 50], + backgroundColor: 'rgba(96, 165, 250, 0.2)', + borderColor: 'rgba(96, 165, 250, 1)', + pointBackgroundColor: 'rgba(96, 165, 250, 1)', + pointBorderColor: '#fff', + pointHoverBackgroundColor: '#fff', + pointHoverBorderColor: 'rgba(96, 165, 250, 1)' + }] + }; + const config = { + type: 'radar', + data: data, + options: { + responsive: true, + maintainAspectRatio: false, + scales: { + r: { + angleLines: { color: 'rgba(255, 255, 255, 0.1)' }, + grid: { color: 'rgba(255, 255, 255, 0.1)' }, + pointLabels: { color: '#94a3b8', font: { size: 11, family: 'Inter' } }, + ticks: { display: false, max: 100, min: 0 } + } + }, + plugins: { + legend: { display: false } + } + } + }; + const chart = new Chart(ctx, config); + // Simulate dynamic factor shifting + setInterval(() => { + chart.data.datasets[0].data = chart.data.datasets[0].data.map(val => { + let shift = (Math.random() - 0.5) * 15; + return Math.max(10, Math.min(100, val + shift)); + }); + chart.update('active'); + }, 3000); +} +// --- FULL REPORT GENERATION --- +async function generateFullReport() { + const payload = getPayload(); + const accessKey = sessionStorage.getItem('accessKey') || ""; + // Trigger Cinematic Matrix Loader + const matrixLoader = document.getElementById('matrix-loader'); + const matrixLogs = document.getElementById('matrix-logs'); + const matrixProgress = document.getElementById('matrix-progress'); + const matrixProgressText = document.getElementById('matrix-progress-text'); + matrixLoader.style.display = 'flex'; + matrixLogs.innerHTML = ''; + matrixProgress.style.width = '0%'; + const steps = [ + "Initializing quantitative core engine...", + "Fetching raw market time-series...", + "Inverting Covariance Matrix (Handling non-positive definiteness)...", + "Calculating Principal Components for factor extraction...", + "Solving constrained optimization via interior point method...", + "Executing probabilistic stress tests (Monte Carlo)...", + "Applying L1/L2 shrinkage penalties and bounds...", + "Converging Global Minimum / Maximum Sharpe targets...", + "Compiling mathematical HTML portfolio report..." + ]; + let logIdx = 0; + const interval = setInterval(() => { + if(logIdx < steps.length) { + const el = document.createElement('div'); + el.style.margin = "2px 0"; + el.innerHTML = `> ${steps[logIdx]}`; + matrixLogs.appendChild(el); + // Auto scroll to bottom + matrixLogs.scrollTop = matrixLogs.scrollHeight; + // Progress bar (capping at 99% until fully complete) + let pct = Math.floor(Math.min(((logIdx + 1) / steps.length) * 99, 99)); + matrixProgress.style.width = `${pct}%`; + if(matrixProgressText) matrixProgressText.innerText = `${pct}%`; + logIdx++; + } + }, 600); // Fast cinematic log streaming + try { + const res = await fetch('/api/generate', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-Access-Key': accessKey + }, + body: JSON.stringify(payload) + }); + if (!res.ok) { + clearInterval(interval); + let errTxt = "Generation Failed"; + try { + const errData = await res.json(); + errTxt = errData.detail || errTxt; + } catch (e) {} + matrixLogs.innerHTML += `
> ERROR: ${errTxt}
`; + setTimeout(() => { matrixLoader.style.display = 'none'; }, 4000); + return; + } + const data = await res.json(); + if (data.status !== "queued") { + clearInterval(interval); + matrixLogs.innerHTML += `
> Unexpected server response.
`; + setTimeout(() => { matrixLoader.style.display = 'none'; }, 3000); + return; + } + const taskId = data.task_id; + matrixLogs.innerHTML += `
> Job ${taskId.substring(0,8)} queued. Polling compute engine...
`; + const pollInterval = setInterval(async () => { + try { + const statusRes = await fetch(`/api/status/${taskId}`, { + headers: { 'X-Access-Key': accessKey } + }); + if (!statusRes.ok) { + clearInterval(pollInterval); + clearInterval(interval); + matrixLogs.innerHTML += `
> Polling failed.
`; + setTimeout(() => { matrixLoader.style.display = 'none'; }, 3000); + return; + } + const statusData = await statusRes.json(); + if (statusData.status === "completed") { + clearInterval(pollInterval); + clearInterval(interval); + matrixProgress.style.width = '100%'; + if(matrixProgressText) matrixProgressText.innerText = '100%'; + if (statusData.target_weights) { + sessionStorage.setItem("portfolio_context", JSON.stringify(statusData.target_weights)); + // Save run to local history + try { + const hist = JSON.parse(localStorage.getItem('portfolio_history') || '[]'); + hist.unshift({ + id: taskId.substring(0,8), + date: new Date().toLocaleString(), + return: statusData.expected_return, + volatility: statusData.volatility, + sharpe: statusData.sharpe_ratio, + tickers: Object.keys(statusData.target_weights).length + }); + localStorage.setItem('portfolio_history', JSON.stringify(hist.slice(0, 10))); + } catch(e) {} + } + matrixLogs.innerHTML += `
> OPTIMIZATION COMPLETE. REDIRECTING...
`; + setTimeout(() => { + matrixLoader.style.display = 'none'; + window.openReportFrame(); + }, 1500); + } else if (statusData.status === "error") { + clearInterval(pollInterval); + clearInterval(interval); + matrixLogs.innerHTML += `
> CRITICAL ERROR: ${statusData.message}
`; + // Feed error state to AI + sessionStorage.setItem("portfolio_context", JSON.stringify({ + status: "failed", + error_log: statusData.message || "Unknown internal error", + timestamp: new Date().toISOString() + })); + setTimeout(() => { matrixLoader.style.display = 'none'; }, 4000); + } + } catch (err) { + // Ignore network blips during polling + } + }, 2000); + } catch(err) { + clearInterval(interval); + matrixLogs.innerHTML += `
> CRITICAL ERROR: Network failure.
`; + setTimeout(() => { matrixLoader.style.display = 'none'; }, 4000); + } +} +// --- WIZARD LOGIC --- +window.nextWizardStep = function(step) { + document.querySelectorAll('.wizard-step').forEach(el => el.style.display = 'none'); + const target = document.getElementById('wizardStep' + step); + if(target) { + target.style.display = 'block'; + } +}; +window.runWizard = async function() { + const macro = document.getElementById('wizardMacro').value; + const reaction = document.getElementById('wizardReaction').value; + const basket = document.getElementById('wizardBasket').value; + // Auto-fill the Sandbox form under the hood + document.getElementById('tickers').value = basket; + let risk = 5; + if (reaction === 'buy') risk = 2; + if (reaction === 'hold') risk = 5; + if (reaction === 'sell') risk = 8; + document.getElementById('risk').value = risk; + document.getElementById('riskVal').textContent = risk; + let model = 5; // XGBoost default + if (macro === 'growth') model = 4; // Fama-French + if (macro === 'recession') model = 7; // HMM + if (macro === 'inflation') model = 3; // Bayesian Shrinkage + document.getElementById('model').value = model.toString(); + // Switch to Sandbox view + const modal = document.getElementById('wizardOverlay'); + if(modal) modal.style.display = 'none'; + switchView('sandbox'); + // Trigger the full report generation automatically + await generateFullReport(); +}; +// --- VANTA JS BACKGROUND --- +function initVantaBackground() { + const container = document.getElementById('vanta-bg'); + if (!container) return; + try { + window.vantaEffect = VANTA.NET({ + el: "#vanta-bg", + mouseControls: true, + touchControls: true, + gyroControls: false, + minHeight: 200.00, + minWidth: 200.00, + scale: 1.00, + scaleMobile: 1.00, + color: 0x3b82f6, + backgroundColor: 0x050814, + points: 12.00, + maxDistance: 22.00, + spacing: 16.00 + }); + // Ensure Vanta resizes correctly on window resize + window.addEventListener('resize', () => { + if (window.vantaEffect) { + window.vantaEffect.resize(); + } + }); + } catch (e) { + console.warn("Vanta JS failed to initialize:", e); + } +} +// --- REPORT FRAME LOGIC --- +window.openReportFrame = async function() { + const reportContainer = document.getElementById('reportContainer'); + const reportView = document.getElementById('report-view'); + // Check if report actually exists before opening iframe + try { + const checkRes = await fetch('/report'); + if (!checkRes.ok) { + alert("Report generation failed or returned a blank response. Check server logs."); + return; + } + } catch(e) { + alert("Error fetching report."); + return; + } + document.querySelector('.main-content').style.display = 'none'; + document.querySelector('nav').style.display = 'none'; + document.querySelector('.market-ticker-bar').style.display = 'none'; + reportContainer.style.display = 'block'; + reportView.src = '/report?t=' + new Date().getTime(); +}; +window.closeReport = function() { + document.getElementById('reportContainer').style.display = 'none'; + document.querySelector('.main-content').style.display = 'block'; + document.querySelector('nav').style.display = 'flex'; + document.querySelector('.market-ticker-bar').style.display = 'flex'; +}; +// Ambient Background relies entirely on Vanta JS now. +// --- AUTHENTICATION FLOW --- +window.logout = async function() { + sessionStorage.removeItem('accessKey'); + sessionStorage.removeItem('isMaster'); + try { + await fetch('/api/logout', { method: 'POST' }); + } catch(e) {} + window.location.href = '/'; +}; +// --- AI CHAT WIDGET LOGIC --- +document.addEventListener('DOMContentLoaded', () => { + // Clear stale optimization context on fresh page load so the AI doesn't hallucinate previous sessions + sessionStorage.removeItem("portfolio_context"); + // Pre-populate some assets to make the UI look aliveviously orphaned + if (typeof initHeroRadar === 'function') { + initHeroRadar(); + } + const chatToggleBtn = document.getElementById('chat-toggle-btn'); + const chatWindow = document.getElementById('chat-window'); + const chatCloseBtn = document.getElementById('chat-close-btn'); + const chatForm = document.getElementById('chat-form'); + const chatInput = document.getElementById('chat-input'); + const chatMessages = document.getElementById('chat-messages'); + // Maintain conversation history locally + let chatHistory = []; + if (chatToggleBtn && chatWindow) { + chatToggleBtn.addEventListener('click', () => { + chatWindow.style.display = 'flex'; + chatToggleBtn.style.transform = 'scale(0)'; + }); + chatCloseBtn.addEventListener('click', () => { + chatWindow.style.display = 'none'; + chatToggleBtn.style.transform = 'scale(1)'; + }); + chatForm.addEventListener('submit', async (e) => { + e.preventDefault(); + const msg = chatInput.value.trim(); + if (!msg) return; + // Display user message + const userMsg = document.createElement('div'); + userMsg.style.cssText = "background: rgba(255,255,255,0.1); padding: 10px 14px; border-radius: 12px; border-top-right-radius: 4px; align-self: flex-end; max-width: 85%; color: white;"; + userMsg.innerText = msg; + chatMessages.appendChild(userMsg); + chatInput.value = ''; + chatMessages.scrollTop = chatMessages.scrollHeight; + // Save user message to history + chatHistory.push({ role: "user", content: msg }); + // Display loading + const loadingMsg = document.createElement('div'); + loadingMsg.style.cssText = "color: #94a3b8; font-size: 0.9rem; margin-top: 4px; font-style: italic;"; + loadingMsg.innerHTML = `Thinking...`; + chatMessages.appendChild(loadingMsg); + chatMessages.scrollTop = chatMessages.scrollHeight; + // Get context + let ctx = {}; + try { + ctx = JSON.parse(sessionStorage.getItem("portfolio_context") || "{}"); + } catch(e) {} + try { + const res = await fetch('/api/chat', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-Access-Key': sessionStorage.getItem('accessKey') || '' + }, + body: JSON.stringify({ message: msg, history: chatHistory.slice(-10), portfolio_context: ctx }) + }); + const data = await res.json(); + chatMessages.removeChild(loadingMsg); + const responseText = data.response || data.reply || (data.detail ? "Error: " + data.detail : "No response from AI."); + + const actMatch = responseText.match(/<<>>/s); + let cleanText = responseText; + if (actMatch) { + cleanText = responseText.replace(actMatch[0], '').trim(); + try { + const actData = JSON.parse(actMatch[1]); + window.showAIActionModal(actData); + } catch(e) { console.error("Failed to parse ACT block", e); } + } + + chatHistory.push({ role: "assistant", content: cleanText }); + const aiMsg = document.createElement('div'); + aiMsg.style.cssText = "background: rgba(59, 130, 246, 0.1); padding: 12px 16px; border-radius: 12px; border-top-left-radius: 4px; align-self: flex-start; max-width: 85%; color: #e2e8f0; line-height: 1.5;"; + aiMsg.innerText = cleanText; + chatMessages.appendChild(aiMsg); + chatMessages.scrollTop = chatMessages.scrollHeight; + } catch (err) { + chatMessages.removeChild(loadingMsg); + const errMsg = document.createElement('div'); + errMsg.style.cssText = "color: #ef4444; font-size: 0.9rem;"; + errMsg.innerText = "Connection failed. Please try again."; + chatMessages.appendChild(errMsg); + } + }); + } +}); +// --- AI STRATEGY GENERATOR --- +window.generateAIStrategy = async function() { + const inputEl = document.getElementById('ai-strategy-input'); + const btn = document.getElementById('ai-strategy-btn'); + const query = inputEl.value.trim(); + if (!query) return; + btn.disabled = true; + const origText = btn.innerText; + btn.innerText = "Thinking..."; + try { + const accessKey = sessionStorage.getItem('accessKey'); + const res = await fetch('/api/generate_strategy', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-Access-Key': accessKey || '' + }, + body: JSON.stringify({ query: query }) + }); + const data = await res.json(); + if (data.status === "success" && data.config) { + const config = data.config; + if (config.tickers) document.getElementById('tickers').value = config.tickers; + if (config.capital) document.getElementById('capital').value = config.capital; + if (config.risk) { + document.getElementById('risk').value = config.risk; + document.getElementById('riskVal').textContent = config.risk; + } + if (config.model) document.getElementById('model').value = config.model; + if (config.allocation_engine) document.getElementById('allocation_engine').value = config.allocation_engine; + if (config.allow_shorting !== undefined) document.getElementById('allow_shorting').checked = config.allow_shorting; + if (config.tax_enabled !== undefined) document.getElementById('tax_enabled').checked = config.tax_enabled; + if (config.garch_enabled !== undefined) document.getElementById('garch_enabled').checked = config.garch_enabled; + // Trigger animation or feedback + inputEl.style.borderColor = "#10b981"; + setTimeout(() => inputEl.style.borderColor = "rgba(255, 255, 255, 0.1)", 2000); + } else { + alert(data.detail || "Failed to generate strategy."); + } + } catch (e) { + console.error("AI Strategy Generator failed:", e); + alert("Network error."); + } + btn.disabled = false; + btn.innerText = origText; +} +// --- REPORT FRAME LOGIC --- +window.openReportFrame = async function() { + const reportContainer = document.getElementById('reportContainer'); + const reportView = document.getElementById('report-view'); + // Check if report actually exists before opening iframe + try { + const checkRes = await fetch('/report'); + if (!checkRes.ok) { + alert("Report generation failed or returned a blank response. Check server logs."); + return; + } + } catch(e) { + alert("Error fetching report."); + return; + } + document.querySelector('.main-content').style.display = 'none'; + document.querySelector('nav').style.display = 'none'; + document.querySelector('.market-ticker-bar').style.display = 'none'; + reportContainer.style.display = 'block'; + reportView.src = '/report?t=' + new Date().getTime(); +}; +window.closeReport = function() { + document.getElementById('reportContainer').style.display = 'none'; + document.querySelector('.main-content').style.display = 'block'; + document.querySelector('nav').style.display = 'flex'; + document.querySelector('.market-ticker-bar').style.display = 'flex'; +}; +// Ambient Background relies entirely on Vanta JS now. +// --- AUTHENTICATION FLOW --- +window.logout = function() { + sessionStorage.removeItem('accessKey'); + window.location.href = '/'; +}; +// --- AI CHAT WIDGET LOGIC --- +document.addEventListener('DOMContentLoaded', () => { + // Clear stale optimization context on fresh page load so the AI doesn't hallucinate previous sessions + sessionStorage.removeItem("portfolio_context"); + // Pre-populate some assets to make the UI look aliveviously orphaned + if (typeof initHeroRadar === 'function') { + initHeroRadar(); + } + const chatToggleBtn = document.getElementById('chat-toggle-btn'); + const chatWindow = document.getElementById('chat-window'); + const chatCloseBtn = document.getElementById('chat-close-btn'); + const chatForm = document.getElementById('chat-form'); + const chatInput = document.getElementById('chat-input'); + const chatMessages = document.getElementById('chat-messages'); + // Maintain conversation history locally + let chatHistory = []; + if (chatToggleBtn && chatWindow) { + chatToggleBtn.addEventListener('click', () => { + chatWindow.style.display = 'flex'; + chatToggleBtn.style.transform = 'scale(0)'; + }); + chatCloseBtn.addEventListener('click', () => { + chatWindow.style.display = 'none'; + chatToggleBtn.style.transform = 'scale(1)'; + }); + chatForm.addEventListener('submit', async (e) => { + e.preventDefault(); + const msg = chatInput.value.trim(); + if (!msg) return; + // Display user message + const userMsg = document.createElement('div'); + userMsg.style.cssText = "background: rgba(255,255,255,0.1); padding: 10px 14px; border-radius: 12px; border-top-right-radius: 4px; align-self: flex-end; max-width: 85%; color: white;"; + userMsg.innerText = msg; + chatMessages.appendChild(userMsg); + chatInput.value = ''; + chatMessages.scrollTop = chatMessages.scrollHeight; + // Save user message to history + chatHistory.push({ role: "user", content: msg }); + // Display loading + const loadingMsg = document.createElement('div'); + loadingMsg.style.cssText = "color: #94a3b8; font-size: 0.9rem; margin-top: 4px; font-style: italic;"; + loadingMsg.innerHTML = `Thinking...`; + chatMessages.appendChild(loadingMsg); + chatMessages.scrollTop = chatMessages.scrollHeight; + // Get context + let ctx = {}; + try { + ctx = JSON.parse(sessionStorage.getItem("portfolio_context") || "{}"); + } catch(e) {} + try { + const res = await fetch('/api/chat', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-Access-Key': sessionStorage.getItem('accessKey') || '' + }, + body: JSON.stringify({ message: msg, history: chatHistory.slice(-10), portfolio_context: ctx }) + }); + const data = await res.json(); + chatMessages.removeChild(loadingMsg); + const responseText = data.response || data.reply || (data.detail ? "Error: " + data.detail : "No response from AI."); + + const actMatch = responseText.match(/<<>>/s); + let cleanText = responseText; + if (actMatch) { + cleanText = responseText.replace(actMatch[0], '').trim(); + try { + const actData = JSON.parse(actMatch[1]); + window.showAIActionModal(actData); + } catch(e) { console.error("Failed to parse ACT block", e); } + } + + chatHistory.push({ role: "assistant", content: cleanText }); + const aiMsg = document.createElement('div'); + aiMsg.style.cssText = "background: rgba(59, 130, 246, 0.1); padding: 12px 16px; border-radius: 12px; border-top-left-radius: 4px; align-self: flex-start; max-width: 85%; color: #e2e8f0; line-height: 1.5;"; + aiMsg.innerText = cleanText; + chatMessages.appendChild(aiMsg); + chatMessages.scrollTop = chatMessages.scrollHeight; + } catch (err) { + chatMessages.removeChild(loadingMsg); + const errMsg = document.createElement('div'); + errMsg.style.cssText = "color: #ef4444; font-size: 0.9rem;"; + errMsg.innerText = "Connection failed. Please try again."; + chatMessages.appendChild(errMsg); + } + }); + } +}); +// --- AI STRATEGY GENERATOR --- +window.generateAIStrategy = async function() { + const inputEl = document.getElementById('ai-strategy-input'); + const btn = document.getElementById('ai-strategy-btn'); + const query = inputEl.value.trim(); + if (!query) return; + btn.disabled = true; + const origText = btn.innerText; + btn.innerText = "Thinking..."; + try { + const accessKey = sessionStorage.getItem('accessKey'); + const res = await fetch('/api/generate_strategy', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-Access-Key': accessKey || '' + }, + body: JSON.stringify({ query: query }) + }); + const data = await res.json(); + if (data.status === "success" && data.config) { + const config = data.config; + if (config.tickers) document.getElementById('tickers').value = config.tickers; + if (config.capital) document.getElementById('capital').value = config.capital; + if (config.risk) { + document.getElementById('risk').value = config.risk; + document.getElementById('riskVal').textContent = config.risk; + } + if (config.model) document.getElementById('model').value = config.model; + if (config.allocation_engine) document.getElementById('allocation_engine').value = config.allocation_engine; + if (config.allow_shorting !== undefined) document.getElementById('allow_shorting').checked = config.allow_shorting; + if (config.tax_enabled !== undefined) document.getElementById('tax_enabled').checked = config.tax_enabled; + if (config.garch_enabled !== undefined) document.getElementById('garch_enabled').checked = config.garch_enabled; + // Trigger animation or feedback + inputEl.style.borderColor = "#10b981"; + setTimeout(() => inputEl.style.borderColor = "rgba(255, 255, 255, 0.1)", 2000); + } else { + alert(data.detail || "Failed to generate strategy."); + } + } catch (e) { + console.error("AI Strategy Generator failed:", e); + alert("Network error."); + } + btn.disabled = false; + btn.innerText = origText; +}; +// --- HISTORY MODAL --- +window.viewHistory = function() { + let hist = []; + try { + hist = JSON.parse(localStorage.getItem('portfolio_history') || '[]'); + } catch(e) {} + let html = '
Compare previous institutional backtests side-by-side.
'; + if (hist.length === 0) { + html += '
No history found. Run an optimization first.
'; + } else { + html += '
'; + hist.forEach(run => { + const ret = run.return ? (run.return * 100).toFixed(2) + '%' : 'N/A'; + const vol = run.volatility ? (run.volatility * 100).toFixed(2) + '%' : 'N/A'; + const sharpe = run.sharpe ? run.sharpe.toFixed(2) : 'N/A'; + html += ` +
+
+ ID: ${run.id.substring(0,8)} + ${run.date.split(',')[0]} +
+
${ret}
+
+ Volatility: + ${vol} +
+
+ Sharpe Ratio: + ${sharpe} +
+
`; + }); + html += '
'; + } + document.getElementById('modalTitle').innerText = "Backtest History & Comparison"; + document.getElementById('modalBody').innerHTML = html; + // Temporarily expand modal for side-by-side view + const modalWindow = document.querySelector('#globalModal .modal-window'); + modalWindow.setAttribute('data-orig-max-width', modalWindow.style.maxWidth); + modalWindow.style.maxWidth = "900px"; + document.getElementById('globalModal').classList.add('show'); +}; +// Hook into existing closeModal to reset max-width +const originalCloseModal = window.closeModal; +window.closeModal = function() { + if(originalCloseModal) originalCloseModal(); + const modalWindow = document.querySelector('#globalModal .modal-window'); + setTimeout(() => { + if(modalWindow.hasAttribute('data-orig-max-width')) { + modalWindow.style.maxWidth = modalWindow.getAttribute('data-orig-max-width'); + } + }, 300); +}; +const getHeaders = () => { + // 'accessKey' is stored at login (camelCase) - fixed from snake_case mismatch + const access_key = sessionStorage.getItem('accessKey') || localStorage.getItem('accessKey') || ''; + return { + 'Content-Type': 'application/json', + 'X-Access-Key': access_key + }; +}; +; +async function loadSavedPortfolios() { + try { + const response = await fetch('/api/portfolios', { headers: getHeaders() }); + const data = await response.json(); + const grid = document.getElementById('portfolio-grid'); + grid.innerHTML = ''; + if (data.length === 0) { + grid.innerHTML = '
No saved portfolios yet.
'; + return; + } + data.forEach(p => { + const weightsPreview = Object.entries(p.weights).map(([k,v]) => `${k}: ${(v*100).toFixed(1)}%`).join(', '); + grid.innerHTML += `
+

${p.name}

+

${new Date(p.created_at).toLocaleDateString()}

+

${weightsPreview}

+
+ + +
+
`; + }); + } catch (err) { + console.error('Error loading saved portfolios:', err); + } +} +async function deleteSavedPortfolio(id) { + if(!confirm("Are you sure you want to delete this saved portfolio?")) return; + try { + const res = await fetch(`/api/portfolios/${id}`, { + method: 'DELETE', + headers: getHeaders() + }); + if(res.ok) { + loadSavedPortfolios(); + } else { + alert('Failed to delete.'); + } + } catch(e) { console.error(e); } +} +function loadPortfolioIntoSandbox(tickers) { + document.getElementById('tickers').value = tickers.join(', '); + switchView('sandbox', false); + document.getElementById('tickers').focus(); +} +async function loadBacktestHistory() { + try { + const response = await fetch('/api/backtests', { headers: getHeaders() }); + const data = await response.json(); + const tbody = document.getElementById('backtest-table-body'); + tbody.innerHTML = ''; + if (data.length === 0) { + tbody.innerHTML = 'No history available'; + return; + } + data.forEach(run => { + const retClass = run.return_pct >= 0 ? 'color: #10b981;' : 'color: #ef4444;'; + tbody.innerHTML += ` + + ${new Date(run.executed_at).toLocaleString()} + ${run.model_used} + ${run.return_pct.toFixed(2)}% + ${run.sharpe_ratio.toFixed(2)} + + `; + }); + } catch (err) { + console.error('Error loading backtest history:', err); + } +} +// Hook up webhook form +document.addEventListener('DOMContentLoaded', () => { + const form = document.getElementById('webhook-form'); + if(form) { + form.addEventListener('submit', async (e) => { + e.preventDefault(); + const url = document.getElementById('webhook-url').value; + try { + const res = await fetch('/api/webhooks/config', { + method: 'POST', + headers: getHeaders(), + body: JSON.stringify({url: url}) + }); + const data = await res.json(); + if(data.status === 'success') { + document.getElementById('webhook-status').style.display = 'block'; + document.getElementById('webhook-secret').value = data.api_secret_key; + setTimeout(() => { document.getElementById('webhook-status').style.display = 'none'; }, 3000); + } + } catch(err) { + console.error(err); + alert('Failed to save webhook config'); + } + }); + } +}); +async function saveCurrentPortfolio() { + const context = sessionStorage.getItem('portfolio_context'); + if (!context) { + alert('No portfolio configuration found to save. Generate a portfolio first.'); + return; + } + let weights = {}; + try { + weights = JSON.parse(context); + } catch(e) { + alert('Invalid portfolio data.'); + return; + } + const name = prompt('Enter a name for this portfolio:'); + if (!name || name.trim() === '') return; + const accessKey = document.getElementById('accessKeyInput') ? document.getElementById('accessKeyInput').value : ''; + const tickers = Object.keys(weights); + try { + const res = await fetch('/api/portfolios', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-Access-Key': accessKey + }, + body: JSON.stringify({ + name: name.trim(), + tickers: tickers, + weights: weights + }) + }); + if (res.ok) { + alert('Portfolio saved successfully!'); + // Refresh list if open + loadSavedPortfolios(); + } else { + const err = await res.json(); + alert('Failed to save portfolio: ' + (err.detail || 'Unknown error')); + } + } catch (e) { + console.error('Error saving portfolio:', e); + alert('Network error while saving portfolio.'); + } +} +// AI Sassiness logic and Logout override +document.addEventListener('DOMContentLoaded', () => { + if (sessionStorage.getItem('isMaster') === 'true') { + const chatMessages = document.getElementById('chat-messages'); + if (chatMessages && chatMessages.firstElementChild) { + chatMessages.firstElementChild.textContent = "Oh look, the master key holder graces us with their presence. Please try not to break the space-time continuum."; + } + } +}); +// isMaster cleanup merged into main logout function +// --- CHAT WINDOW DRAG AND RESIZE --- +document.addEventListener('DOMContentLoaded', () => { + const chatWin = document.getElementById('chat-window'); + const dragHandle = document.getElementById('chat-drag-handle'); + const resizeHandle = document.getElementById('chat-resize-handle'); + if(!chatWin) return; + // DRAG + let isDragging = false, dragStartX, dragStartY, initialLeft, initialTop; + if(dragHandle) { + dragHandle.style.cursor = 'move'; + dragHandle.addEventListener('mousedown', (e) => { + isDragging = true; + const rect = chatWin.getBoundingClientRect(); + chatWin.style.right = 'auto'; + chatWin.style.bottom = 'auto'; + chatWin.style.margin = '0'; + initialLeft = rect.left; + initialTop = rect.top; + chatWin.style.left = initialLeft + 'px'; + chatWin.style.top = initialTop + 'px'; + dragStartX = e.clientX; + dragStartY = e.clientY; + e.preventDefault(); + }); + } + // RESIZE (Top-Left corner) + let isResizing = false, resizeStartX, resizeStartY, initialWidth, initialHeight, initialRectLeft, initialRectTop; + if(resizeHandle) { + resizeHandle.addEventListener('mousedown', (e) => { + isResizing = true; + const rect = chatWin.getBoundingClientRect(); + chatWin.style.right = 'auto'; + chatWin.style.bottom = 'auto'; + chatWin.style.margin = '0'; + initialRectLeft = rect.left; + initialRectTop = rect.top; + chatWin.style.left = initialRectLeft + 'px'; + chatWin.style.top = initialRectTop + 'px'; + resizeStartX = e.clientX; + resizeStartY = e.clientY; + initialWidth = rect.width; + initialHeight = rect.height; + e.preventDefault(); + }); + } + document.addEventListener('mousemove', (e) => { + if(isDragging) { + const dx = e.clientX - dragStartX; + const dy = e.clientY - dragStartY; + chatWin.style.left = (initialLeft + dx) + 'px'; + chatWin.style.top = (initialTop + dy) + 'px'; + } + if(isResizing) { + const dx = resizeStartX - e.clientX; + const dy = resizeStartY - e.clientY; + chatWin.style.width = (initialWidth + dx) + 'px'; + chatWin.style.height = (initialHeight + dy) + 'px'; + chatWin.style.left = (initialRectLeft - dx) + 'px'; + chatWin.style.top = (initialRectTop - dy) + 'px'; + } + }); + document.addEventListener('mouseup', () => { + isDragging = false; + isResizing = false; + }); +}); + + +window.toggleChatExpand = function() { + const chatWin = document.getElementById('chat-window'); + if (!chatWin) return; + if (chatWin.style.width === '80vw') { + chatWin.style.width = '380px'; + chatWin.style.height = '500px'; + } else { + chatWin.style.width = '80vw'; + chatWin.style.height = '80vh'; + chatWin.style.left = '10vw'; + chatWin.style.top = '10vh'; + } +}; +window.toggleChatWindow = function() { + const chatWin = document.getElementById('chat-window'); + const toggleBtn = document.getElementById('chat-toggle-btn'); + if (!chatWin) return; + if (chatWin.style.display === 'none' || chatWin.style.display === '') { + chatWin.style.display = 'flex'; + toggleBtn.style.transform = 'scale(0.8)'; + } else { + chatWin.style.display = 'none'; + toggleBtn.style.transform = 'scale(1)'; + } +}; + + +window.showAIActionModal = function(actData) { + const overlay = document.createElement('div'); + overlay.style.cssText = 'position:fixed; top:0; left:0; right:0; bottom:0; background:rgba(0,0,0,0.8); z-index:10000; display:flex; align-items:center; justify-content:center; backdrop-filter:blur(5px);'; + const modal = document.createElement('div'); + modal.style.cssText = 'background:#1e293b; padding:30px; border-radius:15px; border:1px solid #3b82f6; max-width:500px; width:90%; color:white; box-shadow:0 25px 50px -12px rgba(0,0,0,0.5); font-family:"Inter",sans-serif;'; + + modal.innerHTML = ` +

+ NOVA Action Required +

+

NOVA wants to update your portfolio with the following parameters:

+
${JSON.stringify(actData, null, 2)}
+
+ + +
+ `; + overlay.appendChild(modal); + document.body.appendChild(overlay); + + document.getElementById('nova-cancel').onclick = () => { document.body.removeChild(overlay); }; + document.getElementById('nova-confirm').onclick = () => { + document.body.removeChild(overlay); + if(actData.tickers) document.getElementById('tickers').value = actData.tickers; + if(actData.capital) document.getElementById('capital').value = actData.capital; + if(actData.risk) { document.getElementById('risk').value = actData.risk; document.getElementById('riskVal').textContent = actData.risk; } + if(actData.model) document.getElementById('model').value = actData.model; + if(actData.currency) document.getElementById('currency').value = actData.currency; + const engineBtn = document.getElementById('run-btn') || document.querySelector('button[onclick="runEngine()"]'); + if(engineBtn) engineBtn.click(); + else if (window.runEngine) window.runEngine(); + }; +}; + +document.addEventListener('DOMContentLoaded', () => { + const closeBtn = document.getElementById('chat-close-btn'); + if(closeBtn) closeBtn.onclick = window.toggleChatWindow; +});