Spaces:
Running
Running
| // 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 += `<div class="ticker-item"> | |
| <strong style="color: #f8fafc; margin-right: 8px;">${item.name}</strong> | |
| <span>$${item.price}</span> | |
| <span style="${colorClass} margin-left: 6px; font-weight: 500;">${sign}${(item.change*100).toFixed(2)}%</span> | |
| </div>`; | |
| }); | |