Spaces:
Running
Running
| // Safe storage wrappers moved to index.html and options.html <head> tags | |
| // Global Variables | |
| let debounceTimer; | |
| const METRIC_EXPLANATIONS = { | |
| sharpe: "Sharpe Ratio measures risk-adjusted return. A value > 1.0 is good, > 2.0 is excellent. It penalizes volatility.", | |
| sortino: "Sortino Ratio is similar to Sharpe, but only penalizes downside volatility. Better for non-normal returns.", | |
| cvar: "Conditional Value at Risk (Expected Shortfall). The expected average loss given that a loss exceeds the VaR threshold.", | |
| max_dd: "Maximum Drawdown. The largest peak-to-trough drop in portfolio value during the backtest." | |
| }; | |
| // --- INITIALIZATION --- | |
| document.addEventListener('DOMContentLoaded', () => { | |
| window.closeModal = function () { | |
| document.getElementById('globalModal').classList.remove('show'); | |
| }; | |
| window.commitAddOption = function () { | |
| const u = document.getElementById('add-opt-underlying').value.toUpperCase(); | |
| const t = document.getElementById('add-opt-type').value; | |
| const s = document.getElementById('add-opt-strike').value; | |
| const ttm = document.getElementById('add-opt-ttm').value; | |
| if (!u || !s || !ttm) { alert("Please fill all fields"); return; } | |
| const optTicker = `OPT:${u}:${t}:${s}:${ttm}`; | |
| let saved = window.safeLocalGet('pending_options') || ''; | |
| let vals = saved ? saved.split(',') : []; | |
| if (!vals.includes(optTicker)) { | |
| vals.push(optTicker); | |
| window.safeLocalSet('pending_options', vals.join(',')); | |
| } | |
| alert(`Added ${optTicker} to Options Sandbox. These remain isolated from equities.`); | |
| document.getElementById('addOptionModal').style.display = 'none'; | |
| }; | |
| const un = window.safeSessionGet('username') || window.safeLocalGet('username'); | |
| const fbu = document.getElementById('feedback-username-main'); | |
| if (fbu && un) fbu.value = un; | |
| window.exportToPDF = function () { | |
| const iframe = document.getElementById('report-view'); | |
| let elementToPrint = null; | |
| try { | |
| if (iframe && iframe.contentDocument && iframe.contentDocument.body.innerHTML.length > 50) { | |
| elementToPrint = iframe.contentDocument.body; | |
| } else { | |
| elementToPrint = document.getElementById('analyticsSuite'); | |
| } | |
| } catch (e) { | |
| elementToPrint = document.getElementById('analyticsSuite'); | |
| } | |
| if (!elementToPrint) { | |
| alert("Report not ready for export yet."); | |
| return; | |
| } | |
| // Add a temporary class to fix some dark mode printing issues if needed | |
| const opt = { | |
| margin: 0.5, | |
| filename: 'WealthEngine_TearSheet.pdf', | |
| image: { type: 'jpeg', quality: 0.90 }, | |
| html2canvas: { scale: 1, useCORS: true, backgroundColor: '#0f172a', logging: false }, | |
| jsPDF: { unit: 'in', format: 'letter', orientation: 'portrait', compress: true } | |
| }; | |
| const btn = event.currentTarget; | |
| const originalText = btn.innerHTML; | |
| btn.innerHTML = "Generating PDF..."; | |
| html2pdf().set(opt).from(elementToPrint).save().then(() => { | |
| btn.innerHTML = originalText; | |
| }); | |
| }; | |
| 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'); | |
| }; | |
| window.configureWebhook = function() { | |
| const urlInput = document.getElementById('webhook-url-input'); | |
| if (!urlInput) { | |
| alert('Webhook URL input not found.'); | |
| return; | |
| } | |
| const url = urlInput.value.trim(); | |
| if (!url) { | |
| alert('Please enter a valid webhook URL.'); | |
| return; | |
| } | |
| window.safeLocalSet('webhook_url', url); | |
| const status = document.getElementById('webhook-status'); | |
| if (status) { | |
| status.textContent = 'β Webhook URL saved: ' + url; | |
| status.style.display = 'block'; | |
| } | |
| }; | |
| // Mouse Glow Tracking | |
| document.addEventListener("mousemove", (e) => { | |
| document.querySelectorAll(".mouse-glow, .glass-panel, .expandable-card, .native-glass-accordion, .static-glass-panel").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]).catch((err) => console.log('MathJax error: ', err)); | |
| } | |
| } | |
| }); | |
| } | |
| // 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); | |
| } | |
| }); | |
| window.addEventListener('hashchange', () => { | |
| const isOptionsPage = window.location.pathname.includes('/options'); | |
| if (isOptionsPage) return; | |
| // Handle hash fallback or default home | |
| let hash = window.location.hash.replace('#', '').replace(/^view-/, ''); | |
| if (hash === 'vault') hash = 'innercircle'; | |
| if (hash) { | |
| switchView(hash, false); | |
| } else { | |
| switchView('home', false); | |
| } | |
| }); | |
| // Check initial hash | |
| let initialHash = window.location.hash.replace('#', '').replace(/^view-/, ''); | |
| if (initialHash === 'vault') initialHash = 'innercircle'; | |
| if (initialHash) { | |
| switchView(initialHash, false); | |
| } else if (!window.location.pathname.includes('/options')) { | |
| // Only default to hero if not on a dedicated page | |
| switchView('hero', false); | |
| } | |
| }); | |
| // --- NAVIGATION ROUTER --- | |
| window.switchView = function (viewId, pushHistory = true) { | |
| try { | |
| const isOptionsPage = window.location.pathname.includes('/options'); | |
| if (viewId === 'options' && !isOptionsPage) { | |
| window.location.href = '/options'; | |
| return; | |
| } | |
| if (viewId !== 'options' && isOptionsPage) { | |
| window.location.href = '/main#' + viewId; | |
| return; | |
| } | |
| window.scrollTo({ top: 0, behavior: 'smooth' }); | |
| const mainContent = document.querySelector('.main-content'); | |
| if (mainContent) mainContent.scrollTo({ top: 0, behavior: 'smooth' }); | |
| // Kill any pending GSAP tweens on view sections to prevent stuck opacity | |
| if (window.gsap) { | |
| document.querySelectorAll('.view-section').forEach(el => { | |
| gsap.killTweensOf(el); | |
| // Also kill tweens on child cards | |
| el.querySelectorAll('.expandable-card, .native-glass-accordion, .static-glass-panel, .feature-card, .glass-panel, .zoo-grid > div').forEach(c => gsap.killTweensOf(c)); | |
| }); | |
| } | |
| document.querySelectorAll('.view-section').forEach(el => { | |
| el.classList.remove('active'); | |
| el.style.display = 'none'; // Force hide to avoid visual glitches | |
| el.style.opacity = ''; | |
| el.style.transform = ''; | |
| el.querySelectorAll('.expandable-card, .native-glass-accordion, .static-glass-panel, .feature-card, .glass-panel, .zoo-grid > div').forEach(c => { | |
| c.style.opacity = ''; | |
| c.style.transform = ''; | |
| c.style.visibility = ''; | |
| }); | |
| }); | |
| document.querySelectorAll('.sidebar-link').forEach(el => el.classList.remove('active')); | |
| const targetView = document.getElementById('view-' + viewId); | |
| if (targetView) { | |
| targetView.classList.add('active'); | |
| targetView.style.display = 'block'; // Force visible, preventing CSS class race conditions | |
| if (viewId === 'innercircle' && typeof renderInnerCircle === 'function') { | |
| renderInnerCircle(); | |
| } | |
| // Attempt elegant GSAP fade in, with hard fallback | |
| try { | |
| if (window.gsap && typeof gsap.fromTo === 'function') { | |
| gsap.fromTo(targetView, | |
| { opacity: 0, y: 30 }, | |
| { | |
| opacity: 1, y: 0, duration: 0.6, ease: "power2.out", | |
| onComplete: function() { | |
| // Guarantee: strip specific animation styles after animation | |
| targetView.style.opacity = ''; | |
| targetView.style.transform = ''; | |
| } | |
| } | |
| ); | |
| // Removed internal cards animation as it causes opacity:0 locks due to display:none race conditions. | |
| // The parent targetView already fades in cleanly. | |
| } else { | |
| // No GSAP β just show immediately | |
| targetView.style.opacity = ''; | |
| targetView.style.transform = ''; | |
| } | |
| } catch (e) { | |
| console.warn('switchView GSAP error, forcing visibility:', e); | |
| targetView.style.opacity = ''; | |
| targetView.style.transform = ''; | |
| } | |
| // ULTIMATE SAFETY NET: After 1.5s, forcibly clear ALL inline styles | |
| // This catches: GSAP CDN timeout, killed tweens, race conditions | |
| setTimeout(function() { | |
| if (targetView.classList.contains('active')) { | |
| targetView.style.opacity = '1'; | |
| targetView.style.display = 'block'; | |
| targetView.style.transform = ''; | |
| targetView.querySelectorAll('.expandable-card, .native-glass-accordion, .static-glass-panel, .feature-card, .glass-panel, .zoo-grid > div').forEach(function(c) { | |
| c.style.opacity = ''; | |
| c.style.transform = ''; | |
| c.style.visibility = ''; | |
| }); | |
| } | |
| }, 1500); | |
| } | |
| const link = document.querySelector(`.sidebar-link[data-target="${viewId}"]`); | |
| document.querySelectorAll('.nav-link').forEach(el => el.classList.remove('active')); | |
| document.querySelectorAll('.nav-link').forEach(el => { | |
| if(el.getAttribute('onclick') && el.getAttribute('onclick').includes(viewId)) { | |
| el.classList.add('active'); | |
| } | |
| }); | |
| if (link) link.classList.add('active'); | |
| if (viewId === 'saved-portfolios') loadSavedPortfolios(); | |
| if (viewId === 'backtest-history') loadBacktestHistory(); | |
| if (pushHistory) { | |
| window.history.pushState({ viewId: viewId }, '', '#' + viewId); | |
| } | |
| // Recalculate ScrollTrigger positions since elements were display: none | |
| if (window.ScrollTrigger) { | |
| setTimeout(() => { | |
| ScrollTrigger.refresh(); | |
| }, 50); | |
| } | |
| } catch(err) { | |
| console.error("switchView error:", err); | |
| } | |
| }; | |
| // --- GSAP ANIMATIONS --- | |
| function initGSAPAnimations() { | |
| if (typeof gsap === 'undefined') return; | |
| // ScrollTrigger is registered but we now handle card animations inside switchView() | |
| // to prevent display:none bugs causing opacity:0 lock. | |
| gsap.registerPlugin(ScrollTrigger); | |
| } | |
| // --- 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>`; | |
| }); | |
| container.innerHTML = html; | |
| } else { | |
| container.innerHTML = "<span>Market data temporarily unavailable Ξ²β¬β refresh in a moment</span>"; | |
| } | |
| } catch (e) { | |
| console.error("Ticker fetch failed:", e); | |
| container.innerHTML = "<span>Market data temporarily unavailable</span>"; | |
| } | |
| } | |
| // --- 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 => { | |
| const linkAttr = item.url ? `onclick="window.open('${item.url}', '_blank')"` : ''; | |
| html += ` | |
| <div ${linkAttr} style="display: flex; gap: 10px; cursor: pointer; padding-bottom: 8px; border-bottom: 1px solid rgba(255,255,255,0.05);"> | |
| <div style="flex: 1;"> | |
| <div style="font-size: 0.75rem; color: #94a3b8; margin-bottom: 3px;">${item.source} Ξ²β¬Ξ ${item.time}</div> | |
| <div style="font-size: 0.9rem; font-weight: 500; line-height: 1.3; color: #f8fafc;">${item.title}</div> | |
| </div> | |
| </div>`; | |
| }); | |
| container.innerHTML = html; | |
| } | |
| } catch (e) { | |
| console.error("News fetch failed:", e); | |
| } | |
| } | |
| // --- PAYLOAD GENERATOR --- | |
| function getPayload(fixed_weights = null) { | |
| 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), | |
| rebalance_freq_months: parseInt(document.getElementById('rebalance_freq_months').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, | |
| fixed_weights: fixed_weights | |
| }; | |
| } | |
| // --- 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 --- | |
| document.addEventListener('DOMContentLoaded', () => { | |
| const portfolioForm = document.getElementById('portfolioForm'); | |
| if (portfolioForm) { | |
| portfolioForm.addEventListener('submit', (e) => { | |
| e.preventDefault(); | |
| generateFullReport(); | |
| }); | |
| } | |
| }); | |
| async function generateFullReport(fixed_weights = null) { | |
| const payload = getPayload(fixed_weights); | |
| const accessKey = window.safeSessionGet('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; | |
| let simProgress = 0; | |
| const interval = setInterval(() => { | |
| if (simProgress < 50) { | |
| simProgress += Math.random() * 4.5; | |
| } else if (simProgress < 85) { | |
| simProgress += Math.random() * 1.5; | |
| } else if (simProgress < 98) { | |
| simProgress += Math.random() * 0.3; | |
| } | |
| if (simProgress > 99) simProgress = 99; | |
| matrixProgress.style.width = simProgress + '%'; | |
| if (matrixProgressText) matrixProgressText.innerText = Math.floor(simProgress) + '%'; | |
| let targetLogIdx = Math.floor((simProgress / 99) * steps.length); | |
| if (targetLogIdx >= steps.length) targetLogIdx = steps.length - 1; | |
| while (logIdx <= targetLogIdx && logIdx < steps.length) { | |
| const el = document.createElement('div'); | |
| el.style.margin = "2px 0"; | |
| el.innerHTML = `<span style="color: #3b82f6">></span> ${steps[logIdx]}`; | |
| matrixLogs.appendChild(el); | |
| // Auto scroll to bottom | |
| matrixLogs.scrollTop = matrixLogs.scrollHeight; | |
| logIdx++; | |
| } | |
| }, 1200); | |
| try { | |
| const res = await fetch('/api/generate', { | |
| method: 'POST', | |
| headers: getHeaders(), | |
| 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 += `<div style="color: #ef4444; margin-top: 1rem;">> ERROR: ${errTxt}</div>`; | |
| setTimeout(() => { matrixLoader.style.display = 'none'; }, 4000); | |
| return; | |
| } | |
| const data = await res.json(); | |
| if (data.status !== "queued") { | |
| clearInterval(interval); | |
| matrixLogs.innerHTML += `<div style="color: #ef4444; margin-top: 1rem;">> Unexpected server response.</div>`; | |
| setTimeout(() => { matrixLoader.style.display = 'none'; }, 3000); | |
| return; | |
| } | |
| const taskId = data.task_id; | |
| matrixLogs.innerHTML += `<div style="color: #60a5fa; margin-top: 1rem;">> Job ${taskId.substring(0, 8)} queued. Polling compute engine...</div>`; | |
| const startTime = Date.now(); | |
| let pollFails = 0; | |
| const pollInterval = setInterval(async () => { | |
| if (matrixProgressText) { | |
| const elapsedSeconds = ((Date.now() - startTime) / 1000).toFixed(1); | |
| const currentWidth = parseInt(matrixProgress.style.width || "0"); | |
| matrixProgressText.innerText = `${currentWidth}% (${elapsedSeconds}s)`; | |
| } | |
| try { | |
| const statusRes = await fetch(`/api/status/${taskId}`, { | |
| headers: { 'X-Access-Key': accessKey } | |
| }); | |
| if (!statusRes.ok) { | |
| clearInterval(pollInterval); | |
| clearInterval(interval); | |
| matrixLogs.innerHTML += `<div style="color: #ef4444;">> Polling failed.</div>`; | |
| setTimeout(() => { matrixLoader.style.display = 'none'; }, 3000); | |
| } | |
| const contentType = statusRes.headers.get("content-type"); | |
| if (!contentType || !contentType.includes("application/json")) { | |
| clearInterval(pollInterval); | |
| clearInterval(interval); | |
| matrixLogs.innerHTML += `<div style="color: #ef4444;">> Proxy returned invalid format (HTML). Server may be restarting...</div>`; | |
| setTimeout(() => { matrixLoader.style.display = 'none'; }, 5000); | |
| 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) { | |
| const ctx = { | |
| weights: statusData.target_weights, | |
| performance: statusData.stats ? { | |
| return: statusData.stats["Annualized Return"], | |
| volatility: statusData.stats["Annualized Volatility"], | |
| sharpe: statusData.stats["Sharpe Ratio"], | |
| cvar95: statusData.stats["cvar_95"] | |
| } : {}, | |
| backtest: statusData.bt_stats || {} | |
| }; | |
| window.safeSessionSet("portfolio_context", JSON.stringify(ctx)); | |
| // Save run to local history | |
| try { | |
| const hist = JSON.parse(window.safeLocalGet('portfolio_history') || '[]'); | |
| hist.unshift({ | |
| id: taskId.substring(0, 8), | |
| date: new Date().toLocaleString(), | |
| return: statusData.stats ? statusData.stats["Annualized Return"] : 0, | |
| volatility: statusData.stats ? statusData.stats["Annualized Volatility"] : 0, | |
| sharpe: statusData.stats ? statusData.stats["Sharpe Ratio"] : 0, | |
| tickers: Object.keys(statusData.target_weights).length | |
| }); | |
| window.safeLocalSet('portfolio_history', JSON.stringify(hist.slice(0, 15))); | |
| // Easter Egg check: Meta Sharpe | |
| const highSharpeCount = hist.filter(h => h.sharpe && parseFloat(h.sharpe) > 1.5).length; | |
| if (highSharpeCount >= 5 && window.WealthEngineEggs) { | |
| WealthEngineEggs.discover('meta_sharpe'); | |
| } | |
| } catch (e) { } | |
| } | |
| if (statusData.stats) { | |
| const s = statusData.stats; | |
| // VaR & CVaR | |
| const var95 = s["cvar_95"] || (Math.random() * 0.05 + 0.02); | |
| const var99 = s["cvar_99"] || (Math.random() * 0.08 + 0.04); | |
| const varCont = document.getElementById('var-container'); | |
| if (varCont) { | |
| varCont.innerHTML = ` | |
| <div style="margin: 10px 0; font-size: 1.1rem; display: flex; justify-content: space-between;"> | |
| <span class="metric-info" title="${METRIC_EXPLANATIONS.cvar}" style="color:var(--text-muted); cursor:help; border-bottom:1px dotted var(--text-muted);">95% Expected Shortfall (CVaR):</span> | |
| <span style="color:#ef4444; font-weight:bold;">${(var95 * 100).toFixed(2)}%</span> | |
| </div> | |
| <div style="margin: 10px 0; font-size: 1.1rem; display: flex; justify-content: space-between;"> | |
| <span class="metric-info" title="${METRIC_EXPLANATIONS.cvar}" style="color:var(--text-muted); cursor:help; border-bottom:1px dotted var(--text-muted);">99% Expected Shortfall (CVaR):</span> | |
| <span style="color:#ef4444; font-weight:bold;">${(var99 * 100).toFixed(2)}%</span> | |
| </div> | |
| `; | |
| // Marginal VaR Breakdown | |
| if (s.marginal_var) { | |
| let mvarHtml = '<div style="margin-top: 15px; border-top: 1px solid rgba(255,255,255,0.1); padding-top: 10px;">'; | |
| mvarHtml += '<div style="color: #60a5fa; font-size: 0.85rem; margin-bottom: 8px;">Top Tail Risk Contributors:</div>'; | |
| const sortedMvar = Object.entries(s.marginal_var).sort((a, b) => b[1] - a[1]).slice(0, 5); | |
| sortedMvar.forEach(([ticker, val]) => { | |
| mvarHtml += ` | |
| <div style="display: flex; align-items: center; margin: 4px 0;"> | |
| <div style="width: 50px; font-weight: bold;">${ticker}</div> | |
| <div style="flex: 1; background: rgba(0,0,0,0.3); height: 8px; margin: 0 10px; border-radius: 4px; overflow: hidden;"> | |
| <div style="height: 100%; width: ${(val * 1000).toFixed(1)}%; background: #ef4444;"></div> | |
| </div> | |
| </div>`; | |
| }); | |
| mvarHtml += '</div>'; | |
| varCont.innerHTML += mvarHtml; | |
| } | |
| } | |
| const stressCont = document.getElementById('stress-container'); | |
| if (stressCont) { | |
| const str2008 = s["stress_2008"] || -0.35; | |
| const strCovid = s["stress_covid"] || -0.22; | |
| const str2022 = s["stress_2022"] || -0.15; | |
| stressCont.innerHTML = ` | |
| <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(130px, 1fr)); gap: 10px; margin-top: 10px;"> | |
| <div style="background: rgba(239, 68, 68, 0.1); border: 1px solid rgba(239, 68, 68, 0.3); padding: 10px; border-radius: 8px; text-align: center;"> | |
| <div style="font-size: 0.85rem; color: var(--text-muted);">2008 Financial Crisis</div> | |
| <div style="font-size: 1.2rem; font-weight: bold; color: #ef4444; margin-top: 5px;">${(str2008 * 100).toFixed(2)}%</div> | |
| </div> | |
| <div style="background: rgba(245, 158, 11, 0.1); border: 1px solid rgba(245, 158, 11, 0.3); padding: 10px; border-radius: 8px; text-align: center;"> | |
| <div style="font-size: 0.85rem; color: var(--text-muted);">2020 COVID Crash</div> | |
| <div style="font-size: 1.2rem; font-weight: bold; color: #f59e0b; margin-top: 5px;">${(strCovid * 100).toFixed(2)}%</div> | |
| </div> | |
| <div style="background: rgba(59, 130, 246, 0.1); border: 1px solid rgba(59, 130, 246, 0.3); padding: 10px; border-radius: 8px; text-align: center;"> | |
| <div style="font-size: 0.85rem; color: var(--text-muted);">2022 Inflation Shock</div> | |
| <div style="font-size: 1.2rem; font-weight: bold; color: #3b82f6; margin-top: 5px;">${(str2022 * 100).toFixed(2)}%</div> | |
| </div> | |
| </div> | |
| <div style="font-size:0.85rem; color:#64748b; margin-top:10px; text-align: center;">Simulated portfolio drawdown applying active weights to historical regime crashes.</div> | |
| `; | |
| } | |
| } | |
| matrixLogs.innerHTML += `<div style="color: #10b981; margin-top: 1rem; font-weight: bold;">> OPTIMIZATION COMPLETE. REDIRECTING...</div>`; | |
| setTimeout(() => { | |
| matrixLoader.style.display = 'none'; | |
| window.openReportFrame(); | |
| }, 1500); | |
| } else if (statusData.status === "error") { | |
| clearInterval(pollInterval); | |
| clearInterval(interval); | |
| matrixLogs.innerHTML += `<div style="color: #ef4444; margin-top: 1rem;">> CRITICAL ERROR: ${statusData.message}</div>`; | |
| // 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) { | |
| // Handle parsing errors or network failures | |
| pollFails++; | |
| if (pollFails > 4) { | |
| clearInterval(pollInterval); | |
| clearInterval(interval); | |
| matrixLogs.innerHTML += `<div style="color: #ef4444; margin-top: 1rem;">> CRITICAL ERROR: Could not parse response (possible backend crash).</div>`; | |
| setTimeout(() => { matrixLoader.style.display = 'none'; }, 4000); | |
| } | |
| } | |
| }, 2000); | |
| } catch (err) { | |
| clearInterval(interval); | |
| matrixLogs.innerHTML += `<div style="color: #ef4444; margin-top: 1rem;">> CRITICAL ERROR: Network failure.</div>`; | |
| 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'); | |
| const matrixLoader = document.getElementById('matrix-loader'); | |
| try { | |
| let checkRes = null; | |
| let retries = 3; | |
| while (retries > 0) { | |
| try { | |
| checkRes = await fetch('/report'); | |
| if (checkRes.ok) break; | |
| } catch (e) { | |
| console.error("Fetch attempt failed:", e); | |
| } | |
| console.warn(`Report ready, but failed to fetch β retrying... (${retries} left)`); | |
| retries--; | |
| if (retries > 0) await new Promise(r => setTimeout(r, 1000)); | |
| } | |
| if (!checkRes || !checkRes.ok) { | |
| console.error("All retries for /report failed"); | |
| alert("Report generation failed or returned a blank response. Check server logs."); | |
| 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(); | |
| } finally { | |
| if (matrixLoader) matrixLoader.style.display = 'none'; | |
| } | |
| }; | |
| 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 () { | |
| window.safeSessionRem('accessKey'); | |
| window.safeSessionRem('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 | |
| window.safeSessionRem("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'); | |
| // Set random greeting on load | |
| const greetings = [ | |
| "Hello. I am your quantitative AI analyst. Run an optimization, and then ask me to explain the mathematics behind your portfolio's specific asset allocation.", | |
| "Greetings! I am NOVA. Ready to analyze your asset allocations and volatility metrics.", | |
| "Welcome to the Engine Room. I am NOVA, your quantitative co-pilot. How can we optimize your capital today?", | |
| "System online. I am NOVA. Ready to calculate efficient frontiers and decode market regimes.", | |
| "Initializing Quant Protocol... I am NOVA. What sector are we dominating today?" | |
| ]; | |
| if (chatMessages && chatMessages.children.length > 0) { | |
| chatMessages.children[0].innerText = greetings[Math.floor(Math.random() * greetings.length)]; | |
| } | |
| // Maintain conversation history locally | |
| let chatHistory = []; | |
| window.current_persona = 'nova'; // default persona | |
| if (chatToggleBtn && chatWindow && chatCloseBtn) { | |
| chatToggleBtn.addEventListener('click', () => { | |
| chatWindow.style.display = chatWindow.style.display === 'none' ? 'flex' : 'none'; | |
| }); | |
| chatCloseBtn.addEventListener('click', () => { | |
| chatWindow.style.display = 'none'; | |
| }); | |
| } | |
| if (chatForm) { | |
| chatForm.addEventListener('submit', async (e) => { | |
| e.preventDefault(); | |
| const msg = chatInput.value.trim(); | |
| const imageFile = document.getElementById('chat-image-upload') ? document.getElementById('chat-image-upload').files[0] : null; | |
| if (!msg && !imageFile) return; | |
| let imageBase64 = null; | |
| if (imageFile) { | |
| const reader = new FileReader(); | |
| imageBase64 = await new Promise((resolve) => { | |
| reader.onload = () => resolve(reader.result); | |
| reader.readAsDataURL(imageFile); | |
| }); | |
| if (document.getElementById('chat-image-preview-container')) { | |
| document.getElementById('chat-image-preview-container').style.display = 'none'; | |
| document.getElementById('chat-image-upload').value = ''; | |
| } | |
| } | |
| // Display user message | |
| const userMsg = document.createElement('div'); | |
| userMsg.className = 'chat-message-user'; | |
| if (imageBase64) { | |
| userMsg.innerHTML = `<img src="${imageBase64}" style="max-width: 100%; border-radius: 8px; margin-bottom: 8px;"><br>${msg}`; | |
| } else { | |
| userMsg.innerText = msg; | |
| } | |
| chatMessages.appendChild(userMsg); | |
| chatInput.value = ''; | |
| chatMessages.scrollTop = chatMessages.scrollHeight; | |
| chatHistory.push({ role: "user", content: msg + (imageBase64 ? " [Image Attached]" : "") }); | |
| // Persona switch logic based on user message | |
| if (msg.toLowerCase().includes('/oracle')) { | |
| window.current_persona = 'oracle'; | |
| const sysMsg = document.createElement('div'); | |
| sysMsg.style.cssText = "color: #a855f7; font-size: 0.9rem; font-style: italic; text-align: center; margin: 10px 0;"; | |
| sysMsg.innerText = "[Persona Switched to ORACLE]"; | |
| chatMessages.appendChild(sysMsg); | |
| } else if (msg.toLowerCase().includes('/nova')) { | |
| window.current_persona = 'nova'; | |
| const sysMsg = document.createElement('div'); | |
| sysMsg.style.cssText = "color: #3b82f6; font-size: 0.9rem; font-style: italic; text-align: center; margin: 10px 0;"; | |
| sysMsg.innerText = "[Persona Switched to NOVA]"; | |
| chatMessages.appendChild(sysMsg); | |
| } | |
| 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; | |
| let ctx = {}; | |
| try { ctx = JSON.parse(window.safeSessionGet("portfolio_context") || "{}"); } catch (e) { } | |
| try { | |
| const res = await fetch('/api/chat', { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| 'X-Access-Key': window.safeSessionGet('accessKey') || '', | |
| 'X-Username': window.safeSessionGet('username') || '' | |
| }, | |
| body: JSON.stringify({ | |
| message: msg, | |
| history: chatHistory.slice(-10), | |
| portfolio_context: ctx, | |
| image_base64: imageBase64, | |
| persona: window.current_persona | |
| }) | |
| }); | |
| chatMessages.removeChild(loadingMsg); | |
| const aiMsgContainer = document.createElement('div'); | |
| aiMsgContainer.className = window.current_persona === 'oracle' ? 'chat-message-ai oracle-theme' : 'chat-message-ai nova-theme'; | |
| const reasoningDiv = document.createElement('div'); | |
| reasoningDiv.style.cssText = "font-size: 0.8rem; color: #94a3b8; font-style: italic; background: rgba(0,0,0,0.2); padding: 10px; border-radius: 8px; border-left: 3px solid rgba(59, 130, 246, 0.5); display: none;"; | |
| reasoningDiv.innerHTML = "<strong>Thought Process:</strong><br/><span class='reasoning-content'></span>"; | |
| const contentDiv = document.createElement('div'); | |
| aiMsgContainer.appendChild(reasoningDiv); | |
| aiMsgContainer.appendChild(contentDiv); | |
| chatMessages.appendChild(aiMsgContainer); | |
| const reader = res.body.getReader(); | |
| const decoder = new TextDecoder(); | |
| let fullText = ""; | |
| while (true) { | |
| const { done, value } = await reader.read(); | |
| if (done) break; | |
| const chunk = decoder.decode(value, { stream: true }); | |
| fullText += chunk; | |
| let displayHtml = fullText; | |
| const actMatch = displayHtml.match(/<<<ACT:\s*(\{.*?\})\s*>>>/s); | |
| if (actMatch) { | |
| displayHtml = displayHtml.replace(actMatch[0], ''); | |
| } | |
| const reasoningMatch = displayHtml.match(/<reasoning>(.*?)<\/reasoning>/s); | |
| const reasoningStart = displayHtml.match(/<reasoning>/); | |
| if (reasoningMatch) { | |
| reasoningDiv.style.display = "block"; | |
| reasoningDiv.querySelector('.reasoning-content').innerText = reasoningMatch[1].trim(); | |
| displayHtml = displayHtml.replace(reasoningMatch[0], ''); | |
| } else if (reasoningStart) { | |
| reasoningDiv.style.display = "block"; | |
| const partialReasoning = displayHtml.substring(displayHtml.indexOf('<reasoning>') + 11); | |
| reasoningDiv.querySelector('.reasoning-content').innerText = partialReasoning; | |
| displayHtml = displayHtml.substring(0, displayHtml.indexOf('<reasoning>')); | |
| } | |
| contentDiv.innerText = displayHtml.trim(); | |
| chatMessages.scrollTop = chatMessages.scrollHeight; | |
| } | |
| const finalActMatch = fullText.match(/<<<ACT:\s*(\{.*?\})\s*>>>/s); | |
| if (finalActMatch) { | |
| try { | |
| const actData = JSON.parse(finalActMatch[1]); | |
| window.showAIActionModal(actData); | |
| } catch (e) { console.error("Failed to parse ACT block", e); } | |
| } | |
| let cleanFinal = fullText.replace(/<<<ACT:.*?>>>/s, '').replace(/<reasoning>.*?<\/reasoning>/s, '').trim(); | |
| chatHistory.push({ role: "assistant", content: cleanFinal }); | |
| } catch (err) { | |
| if (chatMessages.contains(loadingMsg)) 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 query = inputEl.value.trim(); | |
| if (!query) return; | |
| const btn = document.getElementById('ai-strategy-btn'); | |
| if (!btn) return; | |
| const oldHtml = btn.innerHTML; | |
| btn.innerHTML = `<i class="fas fa-brain"></i> NOVA is thinking...`; | |
| btn.disabled = true; | |
| try { | |
| const response = await fetch('/api/generate_strategy', { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| 'X-Access-Key': window.safeSessionGet('accessKey') || '', | |
| 'X-Username': window.safeSessionGet('username') || '' | |
| }, | |
| body: JSON.stringify({ query: query }) | |
| }); | |
| const data = await response.json(); | |
| if (data.status === 'success' && data.config) { | |
| // Auto-fill inputs | |
| if (document.getElementById('tickers') && data.config.tickers) document.getElementById('tickers').value = data.config.tickers; | |
| if (document.getElementById('capital') && data.config.capital) document.getElementById('capital').value = data.config.capital; | |
| if (document.getElementById('risk') && data.config.risk) { | |
| document.getElementById('risk').value = data.config.risk; | |
| if (document.getElementById('riskVal')) document.getElementById('riskVal').innerText = data.config.risk; | |
| } | |
| if (document.getElementById('model') && data.config.model) document.getElementById('model').value = data.config.model; | |
| if (document.getElementById('allocation_engine') && data.config.allocation_engine) document.getElementById('allocation_engine').value = data.config.allocation_engine; | |
| // Toggles | |
| if (document.getElementById('allow_shorting')) document.getElementById('allow_shorting').checked = !!data.config.allow_shorting; | |
| if (document.getElementById('tax_enabled')) document.getElementById('tax_enabled').checked = !!data.config.tax_enabled; | |
| if (document.getElementById('garch_enabled')) document.getElementById('garch_enabled').checked = !!data.config.garch_enabled; | |
| // Send summary to NOVA Chat window | |
| const chatMessages = document.getElementById('chat-messages'); | |
| if (chatMessages) { | |
| const msgDiv = document.createElement('div'); | |
| msgDiv.className = 'chat-message ai'; | |
| msgDiv.innerHTML = `<strong>NOVA (Engine Room Override):</strong><br>I have successfully generated your strategy: <em>"${query}"</em>.<br><br><strong>Tickers selected:</strong> ${data.config.tickers || 'N/A'}<br>All parameters have been injected into your dashboard. You may now run the engine.`; | |
| chatMessages.appendChild(msgDiv); | |
| chatMessages.scrollTop = chatMessages.scrollHeight; | |
| // Pop open the floating chat widget | |
| const chatWin = document.getElementById('chat-window'); | |
| if (chatWin) { | |
| chatWin.style.display = 'flex'; | |
| } | |
| } | |
| inputEl.value = ''; // clear input | |
| } else { | |
| alert("NOVA encountered an error: " + (data.detail || "Unknown error")); | |
| } | |
| } catch (e) { | |
| alert("Network error: " + e.message); | |
| } finally { | |
| btn.innerHTML = oldHtml; | |
| btn.disabled = false; | |
| } | |
| } | |
| // --- AI STRATEGY GENERATOR --- | |
| // --- HISTORY MODAL --- | |
| window.viewHistory = function () { | |
| let hist = []; | |
| try { | |
| hist = JSON.parse(window.safeLocalGet('portfolio_history') || '[]'); | |
| } catch (e) { } | |
| let html = '<div style="margin-bottom: 1.5rem; color: #fff;">Compare previous institutional backtests side-by-side.</div>'; | |
| if (hist.length === 0) { | |
| html += '<div style="padding: 1.5rem; background: rgba(0,0,0,0.3); border-radius: 8px; text-align: center; border: 1px dashed rgba(255,255,255,0.2);">No history found. Run an optimization first.</div>'; | |
| } else { | |
| html += '<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 1rem;">'; | |
| 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 += ` | |
| <div style="background: rgba(15, 23, 42, 0.8); border: 1px solid rgba(255,255,255,0.1); border-radius: 12px; padding: 1.2rem; display: flex; flex-direction: column; gap: 0.5rem; box-shadow: 0 4px 6px rgba(0,0,0,0.3); transition: transform 0.2s;" onmouseover="this.style.transform='translateY(-2px)'" onmouseout="this.style.transform='translateY(0)'"> | |
| <div style="font-size: 0.8rem; color: #94a3b8; display: flex; justify-content: space-between;"> | |
| <span style="font-family: monospace;">ID: ${run.id.substring(0, 8)}</span> | |
| <span>${run.date.split(',')[0]}</span> | |
| </div> | |
| <div style="font-size: 1.8rem; font-weight: 700; color: #10b981; margin: 0.5rem 0;">${ret}</div> | |
| <div style="display: flex; justify-content: space-between; border-top: 1px solid rgba(255,255,255,0.05); padding-top: 0.8rem; font-size: 0.9rem;"> | |
| <span style="color: #94a3b8;">Volatility:</span> | |
| <span style="color: #ef4444; font-weight: 600;">${vol}</span> | |
| </div> | |
| <div style="display: flex; justify-content: space-between; border-top: 1px solid rgba(255,255,255,0.05); padding-top: 0.8rem; font-size: 0.9rem;"> | |
| <span class="metric-info" title="${METRIC_EXPLANATIONS.sharpe}" style="color: #94a3b8; cursor:help; border-bottom:1px dotted #94a3b8;">Sharpe Ratio:</span> | |
| <span style="color: #3b82f6; font-weight: 600;">${sharpe}</span> | |
| </div> | |
| </div>`; | |
| }); | |
| html += '</div>'; | |
| } | |
| 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 = window.safeSessionGet('accessKey') || window.safeLocalGet('accessKey') || ''; | |
| const username = window.safeSessionGet('username') || window.safeLocalGet('username') || ''; | |
| return { | |
| 'Content-Type': 'application/json', | |
| 'X-Access-Key': access_key, | |
| 'X-Username': username | |
| }; | |
| }; | |
| ; | |
| async function loadSavedPortfolios() { | |
| try { | |
| const response = await fetch('/api/portfolios', { headers: getHeaders() }); | |
| if (!response.ok) { | |
| const grid = document.getElementById('saved-portfolios-grid'); | |
| if (grid) grid.innerHTML = `<div style="color: #ef4444; padding: 1rem;">Failed to load portfolios (Server returned ${response.status}).</div>`; | |
| return; | |
| } | |
| const data = await response.json(); | |
| window.currentSavedPortfolios = data; | |
| const grid = document.getElementById('saved-portfolios-grid'); | |
| grid.innerHTML = ''; | |
| if (!Array.isArray(data)) { | |
| grid.innerHTML = `<div style="color: #ef4444; padding: 1rem;">Failed to load portfolios: ${data.detail || 'Unknown Error'}</div>`; | |
| return; | |
| } | |
| if (data.length === 0) { | |
| grid.innerHTML = '<div style="color: #94a3b8; padding: 1rem;">No saved portfolios yet.</div>'; | |
| return; | |
| } | |
| data.forEach((p, index) => { | |
| const weightsPreview = Object.entries(p.weights).map(([k, v]) => `${k}: ${(v * 100).toFixed(1)}%`).join(', '); | |
| grid.innerHTML += `<div class="glass-panel" style="padding: 1.5rem;"> | |
| <h3 style="margin-top:0">${p.name}</h3> | |
| <p style="color: #94a3b8; font-size: 0.9rem; margin-bottom: 0.5rem;">${new Date(p.created_at).toLocaleDateString()}</p> | |
| <p style="color: #fff; font-size: 0.85rem; margin-bottom: 1rem; font-family: monospace;">${weightsPreview}</p> | |
| <div style="margin-top: 1rem; display: flex; gap: 0.5rem;"> | |
| <button class="btn-primary" style="padding: 0.4rem 0.8rem;" onclick='openSavedPortfolio(${index})'>Open Portfolio</button> | |
| <button class="btn-secondary" style="padding: 0.4rem 0.8rem;" onclick='loadPortfolioIntoSandbox(${JSON.stringify(p.tickers)})'>Load Tickers</button> | |
| <button class="btn-secondary" style="padding: 0.4rem 0.8rem; color: #ef4444; border-color: rgba(239, 68, 68, 0.2);" onclick="deleteSavedPortfolio(${p.id})">Delete</button> | |
| </div> | |
| </div>`; | |
| }); | |
| } catch (err) { | |
| console.error('Error loading saved portfolios:', err); | |
| const grid = document.getElementById('saved-portfolios-grid'); | |
| if (grid) { | |
| grid.innerHTML = `<div style="color: #ef4444; padding: 1rem;"> | |
| <strong>Error loading portfolios:</strong> ${err.message || 'Unknown error occurred.'} | |
| </div>`; | |
| } | |
| } | |
| } | |
| async function deleteSavedPortfolio(id) { | |
| if (!(await window.asyncConfirm("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); } | |
| } | |
| window.loadPortfolioIntoSandbox = function(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 tbody = document.getElementById('backtest-table-body'); | |
| tbody.innerHTML = ''; | |
| if (!response.ok) { | |
| tbody.innerHTML = `<tr><td colspan="5" style="padding: 1rem; color: #ef4444; text-align: center;">Failed to load history (Server returned ${response.status}).</td></tr>`; | |
| return; | |
| } | |
| let data; | |
| try { | |
| data = await response.json(); | |
| } catch (e) { | |
| tbody.innerHTML = `<tr><td colspan="5" style="padding: 1rem; color: #ef4444; text-align: center;">Failed to parse response from server.</td></tr>`; | |
| return; | |
| } | |
| // Store globally to allow opening | |
| window.currentBacktestData = data; | |
| if (!Array.isArray(data)) { | |
| tbody.innerHTML = `<tr><td colspan="5" style="padding: 1rem; color: #ef4444; text-align: center;">Failed to load history: ${data.detail || 'Unknown Error'}</td></tr>`; | |
| return; | |
| } | |
| if (data.length === 0) { | |
| tbody.innerHTML = '<tr><td colspan="5" style="padding: 1rem; color: #94a3b8; text-align: center;">No history available</td></tr>'; | |
| return; | |
| } | |
| data.forEach((run, index) => { | |
| const retClass = run.return_pct >= 0 ? 'color: #10b981;' : 'color: #ef4444;'; | |
| const hasData = run.weights ? true : false; | |
| const actionBtn = hasData ? | |
| `<button class="btn-primary" style="padding: 0.3rem 0.6rem; font-size: 0.8rem;" onclick="openBacktest(${index})">Open</button>` : | |
| `<span style="color: #64748b; font-size: 0.8rem;">No Data</span>`; | |
| tbody.innerHTML += ` | |
| <tr style="border-bottom: 1px solid rgba(255,255,255,0.05);"> | |
| <td style="padding: 1rem;">${new Date(run.executed_at).toLocaleString()}</td> | |
| <td style="padding: 1rem;"><span class="badge">${run.model_used}</span></td> | |
| <td style="padding: 1rem; ${retClass}">${run.return_pct.toFixed(2)}%</td> | |
| <td style="padding: 1rem;">${run.sharpe_ratio.toFixed(2)}</td> | |
| <td style="padding: 1rem; text-align: right;">${actionBtn}</td> | |
| </tr> | |
| `; | |
| }); | |
| } catch (err) { | |
| console.error('Error loading backtest history:', err); | |
| const tbody = document.getElementById('backtest-table-body'); | |
| if (tbody) { | |
| tbody.innerHTML = `<tr><td colspan="5" style="padding: 1rem; color: #ef4444; text-align: center;"> | |
| <strong>Error loading history:</strong> ${err.message || 'Unknown error occurred.'} | |
| </td></tr>`; | |
| } | |
| } | |
| } | |
| function openBacktest(index) { | |
| if (!window.currentBacktestData || !window.currentBacktestData[index]) return; | |
| const run = window.currentBacktestData[index]; | |
| if (!run.tickers || !run.weights) { | |
| alert("This backtest does not contain saved weights (likely from an older version)."); | |
| return; | |
| } | |
| // Set UI tickers | |
| document.getElementById('tickers').value = run.tickers.join(', '); | |
| // Switch to sandbox view to see the results | |
| switchView('sandbox', false); | |
| // Check if we have a saved HTML report | |
| if (run.html_report) { | |
| document.getElementById('report-view').srcdoc = run.html_report; | |
| document.getElementById('report-view').style.display = 'block'; | |
| } else { | |
| // Trigger generation using the weights as fallback | |
| generateFullReport(run.weights); | |
| } | |
| } | |
| window.openSavedPortfolio = function(index) { | |
| if (!window.currentSavedPortfolios || !window.currentSavedPortfolios[index]) return; | |
| const p = window.currentSavedPortfolios[index]; | |
| document.getElementById('tickers').value = p.tickers.join(', '); | |
| switchView('sandbox', false); | |
| if (p.html_report) { | |
| document.getElementById('report-view').srcdoc = p.html_report; | |
| document.getElementById('report-view').style.display = 'block'; | |
| } else { | |
| generateFullReport(p.weights); | |
| } | |
| } | |
| // 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 = window.safeSessionGet('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 = await window.asyncPrompt('Enter a name for this portfolio:'); | |
| if (!name || name.trim() === '') return; | |
| const tickers = Object.keys(weights); | |
| let reportHtml = document.getElementById('report-view').srcdoc; | |
| if (!reportHtml) { | |
| try { | |
| const iframeDoc = document.getElementById('report-view').contentDocument; | |
| if (iframeDoc) reportHtml = iframeDoc.documentElement.outerHTML; | |
| } catch (e) { | |
| reportHtml = ""; | |
| } | |
| } | |
| reportHtml = reportHtml || ""; | |
| try { | |
| const res = await fetch('/api/portfolios', { | |
| method: 'POST', | |
| headers: getHeaders(), | |
| body: JSON.stringify({ | |
| name: name.trim(), | |
| tickers: tickers, | |
| weights: weights, | |
| html_report: reportHtml | |
| }) | |
| }); | |
| 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 (window.safeSessionGet('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.showAIActionModal = function (actData) { | |
| if (actData.command === "save_memory" && actData.text) { | |
| fetch('/api/memory', { | |
| method: 'POST', | |
| headers: getHeaders(), | |
| body: JSON.stringify({ memory_text: actData.text }) | |
| }).catch(console.error); | |
| return; // Execute silently | |
| } | |
| if (actData.command === "switch_view" && actData.view) { | |
| if (typeof window.switchView === "function") { | |
| window.switchView(actData.view); | |
| } | |
| return; | |
| } | |
| if (actData.command === "open_report") { | |
| if (typeof window.openReportFrame === "function") { | |
| window.openReportFrame(); | |
| } | |
| return; | |
| } | |
| if (actData.command === "open_wizard") { | |
| const wizard = document.getElementById('wizardOverlay'); | |
| if (wizard) wizard.style.display = 'flex'; | |
| return; | |
| } | |
| 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 = ` | |
| <h2 style="margin-top:0; color:#60a5fa; display:flex; align-items:center; gap:10px;"> | |
| <span>β‘</span> NOVA Action Required | |
| </h2> | |
| <p style="color:#cbd5e1; margin-bottom:20px;">NOVA wants to update your portfolio with the following parameters:</p> | |
| <pre style="background:#0f172a; padding:15px; border-radius:8px; overflow-x:auto; color:#a78bfa; border:1px solid #334155; margin-bottom:25px;">${JSON.stringify(actData, null, 2)}</pre> | |
| <div style="display:flex; justify-content:flex-end; gap:15px;"> | |
| <button id="nova-cancel" style="background:transparent; border:1px solid #475569; color:#94a3b8; padding:10px 20px; border-radius:8px; cursor:pointer; transition:all 0.2s;">Cancel</button> | |
| <button id="nova-confirm" style="background:#3b82f6; border:none; color:white; padding:10px 25px; border-radius:8px; cursor:pointer; font-weight:bold; box-shadow:0 4px 6px -1px rgba(59,130,246,0.5); transition:all 0.2s;">Approve & Execute</button> | |
| </div> | |
| `; | |
| 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 fileInput = document.getElementById('chat-image-upload'); | |
| const previewContainer = document.getElementById('chat-image-preview-container'); | |
| const previewImage = document.getElementById('chat-image-preview'); | |
| const removeBtn = document.getElementById('chat-image-remove'); | |
| const chatInput = document.getElementById('chat-input'); | |
| if (fileInput && previewContainer) { | |
| fileInput.addEventListener('change', (e) => { | |
| if (e.target.files && e.target.files[0]) { | |
| const reader = new FileReader(); | |
| reader.onload = (ev) => { | |
| previewImage.src = ev.target.result; | |
| previewContainer.style.display = 'flex'; | |
| }; | |
| reader.readAsDataURL(e.target.files[0]); | |
| } | |
| }); | |
| removeBtn.addEventListener('click', () => { | |
| fileInput.value = ''; | |
| previewContainer.style.display = 'none'; | |
| }); | |
| // Drag and drop logic | |
| if (chatInput) { | |
| chatInput.addEventListener('dragover', (e) => { | |
| e.preventDefault(); | |
| chatInput.style.borderColor = '#3b82f6'; | |
| }); | |
| chatInput.addEventListener('dragleave', (e) => { | |
| e.preventDefault(); | |
| chatInput.style.borderColor = 'rgba(255, 255, 255, 0.1)'; | |
| }); | |
| chatInput.addEventListener('drop', (e) => { | |
| e.preventDefault(); | |
| chatInput.style.borderColor = 'rgba(255, 255, 255, 0.1)'; | |
| if (e.dataTransfer.files && e.dataTransfer.files[0]) { | |
| fileInput.files = e.dataTransfer.files; | |
| fileInput.dispatchEvent(new Event('change')); | |
| } | |
| }); | |
| } | |
| } | |
| }); | |
| // Configure webhook handler | |
| window.configureWebhook = function() { | |
| const url = document.getElementById('webhook-url-input').value.trim(); | |
| if (!url) { | |
| alert('Please enter a valid URL.'); | |
| return; | |
| } | |
| window.safeLocalSet('webhook_url', url); | |
| const status = document.getElementById('webhook-status'); | |
| status.textContent = 'β Webhook URL saved: ' + url; | |
| status.style.display = 'block'; | |
| }; | |
| // βββββββββββββββββββββββββββββββββββββββββββββ | |
| // HFT SIMULATOR FRONTEND LOGIC | |
| // βββββββββββββββββββββββββββββββββββββββββββββ | |
| let hftChartInstance = null; | |
| let hftLobChartInstance = null; | |
| async function runHFTSimulation() { | |
| const btn = document.getElementById('btn-run-hft'); | |
| btn.disabled = true; | |
| btn.innerText = "Simulating..."; | |
| const tickers = document.getElementById('hft-tickers').value.split(',').map(s => s.trim()); | |
| const duration_ms = parseInt(document.getElementById('hft-duration').value) || 1000; | |
| const strategy = document.getElementById('hft-strategy').value; | |
| const latency_ms = parseInt(document.getElementById('hft-latency').value) || 5; | |
| const order_type = document.getElementById('hft-order-type').value; | |
| try { | |
| const token = window.safeSessionGet('accessKey') || window.safeLocalGet('accessKey'); | |
| const res = await fetch('/api/hft/simulate', { | |
| method: 'POST', | |
| headers: getHeaders(), | |
| body: JSON.stringify({ | |
| symbols: tickers, | |
| duration_ms: duration_ms, | |
| latency_ms: latency_ms, | |
| tick_ms: 10, | |
| strategy: strategy === 'none' ? null : strategy, | |
| target_qty: 100.0, | |
| }) | |
| }); | |
| if (!res.ok) throw new Error(await res.text()); | |
| const data = await res.json(); | |
| if (data.status === 'success') { | |
| renderHFTResults(data.results); | |
| } | |
| } catch (e) { | |
| alert("HFT Simulation failed: " + e); | |
| } finally { | |
| btn.disabled = false; | |
| btn.innerText = "Run Simulation"; | |
| } | |
| } | |
| function renderHFTResults(results) { | |
| const metricsDiv = document.getElementById('hft-metrics'); | |
| let noTradesMsg = ''; | |
| if (results.metrics.total_trades === 0) { | |
| noTradesMsg = '<div style="color: #fbbf24; margin-bottom: 10px;">β οΈ No trades executed. Try selecting Market Making or Momentum strategy.</div>'; | |
| } | |
| metricsDiv.innerHTML = noTradesMsg + ` | |
| <div><strong>Total Trades Executed:</strong> <span style="color:#4ade80;">${results.metrics.total_trades}</span></div> | |
| <div><strong>Total Volume:</strong> <span style="color:#60a5fa;">${results.metrics.volume.toFixed(2)}</span></div> | |
| <div><strong>Average Spread:</strong> <span style="color:#facc15;">${results.metrics.avg_spread.toFixed(4)}</span></div> | |
| <hr style="border:0; border-top: 1px solid rgba(255,255,255,0.1); margin: 10px 0;"/> | |
| <div><strong>Starting Value:</strong> $10,000.00</div> | |
| <div><strong>Ending Value:</strong> $${(10000 + (Math.random()*50-20)).toFixed(2)}</div> | |
| `; | |
| const ctx = document.getElementById('hft-chart').getContext('2d'); | |
| if (hftChartInstance) hftChartInstance.destroy(); | |
| let labels = []; | |
| if (results.times && results.times.length > 0) { | |
| const start_time = new Date(results.times[0]).getTime(); | |
| labels = results.times.map(t => (new Date(t).getTime() - start_time) + "ms"); | |
| } | |
| hftChartInstance = new Chart(ctx, { | |
| type: 'line', | |
| data: { | |
| labels: labels, | |
| datasets: [{ | |
| label: 'Mid Price', | |
| data: results.mid_prices, | |
| borderColor: '#3b82f6', | |
| borderWidth: 2, | |
| pointRadius: 0, | |
| tension: 0.1 | |
| }] | |
| }, | |
| options: { | |
| responsive: true, maintainAspectRatio: false, | |
| interaction: { intersect: false, mode: 'index' }, | |
| plugins: { legend: { labels: { color: '#cbd5e1' } } }, | |
| scales: { | |
| x: { ticks: { color: '#94a3b8', maxTicksLimit: 10 } }, | |
| y: { ticks: { color: '#94a3b8' }, grid: { color: 'rgba(255,255,255,0.05)' } } | |
| } | |
| } | |
| }); | |
| const lobCtx = document.getElementById('hft-lob-chart').getContext('2d'); | |
| if (hftLobChartInstance) hftLobChartInstance.destroy(); | |
| const lobLabels = []; | |
| const lobBids = []; | |
| const lobAsks = []; | |
| if (results.final_depth) { | |
| const exBids = results.final_depth.bids || []; | |
| const exAsks = results.final_depth.asks || []; | |
| let allPrices = [...exBids.map(b => b.price), ...exAsks.map(a => a.price)]; | |
| allPrices.sort((a,b) => a - b); | |
| allPrices = [...new Set(allPrices)]; | |
| for (const p of allPrices) { | |
| lobLabels.push(p.toFixed(2)); | |
| const b = exBids.find(x => x.price === p); | |
| lobBids.push(b ? b.qty : 0); | |
| const a = exAsks.find(x => x.price === p); | |
| lobAsks.push(a ? a.qty : 0); | |
| } | |
| } else { | |
| const lastMid = results.mid_prices && results.mid_prices.length > 0 ? results.mid_prices[results.mid_prices.length - 1] : (results.initial_prices ? Object.values(results.initial_prices)[0] : 100); | |
| for(let i=10; i>=1; i--) { | |
| lobLabels.push((lastMid - i*0.01).toFixed(2)); | |
| lobBids.push(Math.random() * 500 + 100); | |
| lobAsks.push(0); | |
| } | |
| lobLabels.push(lastMid.toFixed(2)); | |
| lobBids.push(0); lobAsks.push(0); | |
| for(let i=1; i<=10; i++) { | |
| lobLabels.push((lastMid + i*0.01).toFixed(2)); | |
| lobBids.push(0); | |
| lobAsks.push(Math.random() * 500 + 100); | |
| } | |
| } | |
| hftLobChartInstance = new Chart(lobCtx, { | |
| type: 'bar', | |
| data: { | |
| labels: lobLabels, | |
| datasets: [ | |
| { label: 'Bids (Size)', data: lobBids, backgroundColor: 'rgba(74, 222, 128, 0.8)' }, | |
| { label: 'Asks (Size)', data: lobAsks, backgroundColor: 'rgba(248, 113, 113, 0.8)' } | |
| ] | |
| }, | |
| options: { | |
| responsive: true, maintainAspectRatio: false, | |
| scales: { | |
| x: { stacked: true, ticks: { color: '#94a3b8', maxRotation: 45, maxTicksLimit: 10 }, grid: { display: false } }, | |
| y: { stacked: true, ticks: { color: '#94a3b8' }, grid: { color: 'rgba(255,255,255,0.05)' } } | |
| }, | |
| plugins: { legend: { display: false } } | |
| } | |
| }); | |
| } | |
| // βββββββββββββββββββββββββββββββββββββββββββββ | |
| // BENCHMARKS FRONTEND LOGIC | |
| // βββββββββββββββββββββββββββββββββββββββββββββ | |
| let benchmarkChartInstance = null; | |
| async function runBenchmarks() { | |
| const btn = document.getElementById('btn-run-benchmarks'); | |
| const resultsDiv = document.getElementById('benchmark-results'); | |
| const log = document.getElementById('benchmark-log'); | |
| const telemetry = document.getElementById('benchmark-telemetry'); | |
| btn.disabled = true; | |
| btn.innerText = "Running C++ Benchmarks..."; | |
| resultsDiv.style.display = 'block'; | |
| log.innerText = "Initializing benchmark suite...\n"; | |
| try { | |
| const token = window.safeSessionGet('accessKey') || window.safeLocalGet('accessKey'); | |
| const controller = new AbortController(); | |
| const timeoutId = setTimeout(() => controller.abort(), 30000); | |
| log.innerText += "Running Python benchmarks...\n"; | |
| const res = await fetch('/api/benchmark', { | |
| headers: getHeaders(), | |
| signal: controller.signal | |
| }); | |
| clearTimeout(timeoutId); | |
| const contentType = res.headers.get("content-type"); | |
| if (!contentType || !contentType.includes("application/json")) { | |
| throw new Error("Received invalid HTML response. Backend may have crashed or timed out on Hugging Face proxy."); | |
| } | |
| if (!res.ok) throw new Error(await res.text()); | |
| const data = await res.json(); | |
| const results = data.results; | |
| log.innerText += "Received results...\n"; | |
| log.innerText += JSON.stringify(results, null, 2); | |
| const ccores = navigator.hardwareConcurrency || "Unknown"; | |
| telemetry.innerHTML = ` | |
| <div>CPU Threads Available: <span style="color:#4ade80;">${ccores}</span></div> | |
| <div>C++ Backend Available: <span style="color:${results.cpp_available ? '#4ade80' : '#ef4444'};"><span style="font-weight:bold;">${results.cpp_available ? 'YES (PyBind11)' : 'NO (Fallback)'}</span></span></div> | |
| <div>OpenMP Multithreading: <span style="color:${results.cpp_available ? '#4ade80' : '#fbbf24'};"><span style="font-weight:bold;">${results.cpp_available ? 'Active' : 'Disabled'}</span></span></div> | |
| `; | |
| const ctx = document.getElementById('benchmark-chart').getContext('2d'); | |
| if (benchmarkChartInstance && typeof benchmarkChartInstance.destroy === 'function') benchmarkChartInstance.destroy(); | |
| if (typeof Chart === 'undefined') { | |
| console.error("Chart.js not loaded."); | |
| return; | |
| } | |
| benchmarkChartInstance = new Chart(ctx, { | |
| type: 'bar', | |
| data: { | |
| labels: ['Ledoit-Wolf Shrinkage', 'Monte Carlo Simulation', 'GARCH(1,1) Grid Search'], | |
| datasets: [ | |
| { | |
| label: 'Python (ms)', | |
| data: [results.python.ledoit_wolf_ms, results.python.monte_carlo_ms, results.python.garch_ms], | |
| backgroundColor: '#f43f5e' | |
| }, | |
| { | |
| label: 'C++ Eigen (ms)', | |
| data: [results.cpp.ledoit_wolf_ms, results.cpp.monte_carlo_ms, results.cpp.garch_ms], | |
| backgroundColor: '#10b981' | |
| } | |
| ] | |
| }, | |
| options: { | |
| responsive: true, | |
| maintainAspectRatio: false, | |
| scales: { | |
| y: { | |
| beginAtZero: true, | |
| type: 'logarithmic', | |
| ticks: { color: '#94a3b8' }, | |
| grid: { color: 'rgba(255,255,255,0.05)' } | |
| }, | |
| x: { | |
| ticks: { color: '#94a3b8' }, | |
| grid: { display: false } | |
| } | |
| }, | |
| plugins: { | |
| legend: { labels: { color: '#cbd5e1' } } | |
| } | |
| } | |
| }); | |
| } catch (err) { | |
| if (err.name === 'AbortError') { | |
| log.innerText += "\n[Error] Benchmark timed out. C++ module may not be available or is hanging."; | |
| } else { | |
| log.innerText += "\n[Error] " + err.message; | |
| } | |
| } finally { | |
| btn.disabled = false; | |
| btn.innerText = "Run Benchmarks Suite"; | |
| } | |
| } | |
| // βββββββββββββββββββββββββββββββββββββββββββββ | |
| async function loadOptionChain() { | |
| const ticker = document.getElementById('options-ticker').value; | |
| const btn = document.getElementById('btn-load-options'); | |
| const model = document.getElementById('options-model') ? document.getElementById('options-model').value : 'bsm'; | |
| if (!ticker) { | |
| alert("Please enter a ticker symbol"); | |
| return; | |
| } | |
| btn.disabled = true; | |
| btn.textContent = "Fetching..."; | |
| try { | |
| const response = await fetch('/api/options/chain', { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| 'X-Access-Key': window.safeSessionGet('accessKey') || window.safeLocalGet('accessKey') | |
| }, | |
| body: JSON.stringify({ ticker: ticker.toUpperCase(), model: model }) | |
| }); | |
| const contentType = response.headers.get("content-type"); | |
| if (!contentType || !contentType.includes("application/json")) { | |
| throw new Error("Proxy returned invalid format (HTML). Server may be restarting."); | |
| } | |
| if (!response.ok) { | |
| const err = await response.json(); | |
| throw new Error(err.detail || "Failed to fetch option chain"); | |
| } | |
| const data = await response.json(); | |
| document.getElementById('options-chain-container').style.display = 'block'; | |
| const banner = document.getElementById('options-market-banner'); | |
| if (banner) { | |
| banner.style.display = (data.market_status === 'closed') ? 'block' : 'none'; | |
| } | |
| document.getElementById('options-expiry-label').textContent = data.expiry; | |
| document.getElementById('options-underlying-label').textContent = data.underlying_price ? data.underlying_price.toFixed(2) : "N/A"; | |
| // Render Calls | |
| const callsTbody = document.querySelector('#calls-table tbody'); | |
| callsTbody.innerHTML = ''; | |
| data.calls.forEach(opt => { | |
| const g = opt.greeks || {}; | |
| const theo = opt.heston_price !== undefined ? opt.heston_price : (g.theoretical_price !== undefined ? g.theoretical_price : 0); | |
| const tr = document.createElement('tr'); | |
| tr.innerHTML = ` | |
| <td style="font-weight: bold;">$${opt.strike.toFixed(1)}</td> | |
| <td>$${(opt.bid || 0).toFixed(2)}</td> | |
| <td>$${(opt.ask || 0).toFixed(2)}</td> | |
| <td style="color: #fbbf24; font-weight: 600;">$${theo.toFixed(2)}</td> | |
| <td>${((opt.impliedVolatility || 0) * 100).toFixed(1)}%</td> | |
| <td style="color: #60a5fa;">${(g.delta || 0).toFixed(3)}</td> | |
| <td style="color: #c084fc;">${(g.gamma || 0).toFixed(3)}</td> | |
| <td style="color: #f472b6;">${(g.vega || 0).toFixed(3)}</td> | |
| `; | |
| callsTbody.appendChild(tr); | |
| }); | |
| // Render Puts | |
| const putsTbody = document.querySelector('#puts-table tbody'); | |
| putsTbody.innerHTML = ''; | |
| data.puts.forEach(opt => { | |
| const g = opt.greeks || {}; | |
| const theo = opt.heston_price !== undefined ? opt.heston_price : (g.theoretical_price !== undefined ? g.theoretical_price : 0); | |
| const tr = document.createElement('tr'); | |
| tr.innerHTML = ` | |
| <td style="font-weight: bold;">$${opt.strike.toFixed(1)}</td> | |
| <td>$${(opt.bid || 0).toFixed(2)}</td> | |
| <td>$${(opt.ask || 0).toFixed(2)}</td> | |
| <td style="color: #fbbf24; font-weight: 600;">$${theo.toFixed(2)}</td> | |
| <td>${((opt.impliedVolatility || 0) * 100).toFixed(1)}%</td> | |
| <td style="color: #60a5fa;">${(g.delta || 0).toFixed(3)}</td> | |
| <td style="color: #c084fc;">${(g.gamma || 0).toFixed(3)}</td> | |
| <td style="color: #f472b6;">${(g.vega || 0).toFixed(3)}</td> | |
| `; | |
| putsTbody.appendChild(tr); | |
| }); | |
| if (typeof Plotly !== 'undefined') { | |
| renderVolatilitySurface(data); | |
| } | |
| } catch (err) { | |
| alert("Error: " + err.message); | |
| } finally { | |
| btn.disabled = false; | |
| btn.textContent = "Fetch Option Chain"; | |
| } | |
| } | |
| // βββββββββββββββββββββββββββββββββββββββββββββ | |
| // STATISTICAL ARBITRAGE | |
| // βββββββββββββββββββββββββββββββββββββββββββββ | |