Spaces:
Runtime error
Runtime error
| // Safe storage wrappers moved to index.html and options.html <head> tags | |
| // Global Variables | |
| let debounceTimer; | |
| // --- 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); | |
| } else { | |
| // Handle hash fallback or default home | |
| const hash = window.location.hash.replace('#', '').replace(/^view-/, ''); | |
| if (hash) { | |
| switchView(hash, false); | |
| } else if (!window.location.pathname.includes('/options')) { | |
| switchView('hero', false); | |
| } | |
| } | |
| }); | |
| // Check initial hash | |
| const initialHash = window.location.hash.replace('#', '').replace(/^view-/, ''); | |
| 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) { | |
| 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 = ''; // Reset to let class dictate | |
| // 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 = ''; | |
| 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); | |
| } | |
| }; | |
| // --- 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 --- | |
| 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() * 8; | |
| } else if (simProgress < 85) { | |
| simProgress += Math.random() * 3; | |
| } else if (simProgress < 98) { | |
| simProgress += Math.random() * 0.5; | |
| } | |
| 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); | |
| 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))); | |
| } 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 style="color: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 style="color: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'); | |
| // Check if report actually exists before opening iframe | |
| try { | |
| const checkRes = await fetch('/report'); | |
| if (!checkRes.ok) { | |
| alert("Report generation failed or returned a blank response. Check server logs."); | |
| return; | |
| } | |
| } catch (e) { | |
| alert("Error fetching report."); | |
| return; | |
| } | |
| document.querySelector('.main-content').style.display = 'none'; | |
| document.querySelector('nav').style.display = 'none'; | |
| document.querySelector('.market-ticker-bar').style.display = 'none'; | |
| reportContainer.style.display = 'block'; | |
| reportView.src = '/report?t=' + new Date().getTime(); | |
| }; | |
| window.closeReport = function () { | |
| document.getElementById('reportContainer').style.display = 'none'; | |
| document.querySelector('.main-content').style.display = 'block'; | |
| document.querySelector('nav').style.display = 'flex'; | |
| document.querySelector('.market-ticker-bar').style.display = 'flex'; | |
| }; | |
| // Ambient Background relies entirely on Vanta JS now. | |
| // --- AUTHENTICATION FLOW --- | |
| window.logout = async function () { | |
| 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 = []; | |
| 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.style.cssText = "background: rgba(255,255,255,0.1); padding: 10px 14px; border-radius: 12px; border-top-right-radius: 4px; align-self: flex-end; max-width: 85%; color: white;"; | |
| 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]" : "") }); | |
| 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 | |
| }) | |
| }); | |
| chatMessages.removeChild(loadingMsg); | |
| const aiMsgContainer = document.createElement('div'); | |
| aiMsgContainer.style.cssText = "background: rgba(59, 130, 246, 0.1); padding: 12px 16px; border-radius: 12px; border-top-left-radius: 4px; align-self: flex-start; max-width: 85%; color: #e2e8f0; line-height: 1.5; display: flex; flex-direction: column; gap: 8px;"; | |
| 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; | |
| } | |
| } | |
| // --- REPORT FRAME LOGIC --- | |
| window.openReportFrame = async function () { | |
| const reportContainer = document.getElementById('reportContainer'); | |
| const reportView = document.getElementById('report-view'); | |
| // Check if report actually exists before opening iframe | |
| try { | |
| const checkRes = await fetch('/report'); | |
| if (!checkRes.ok) { | |
| alert("Report generation failed or returned a blank response. Check server logs."); | |
| return; | |
| } | |
| } catch (e) { | |
| alert("Error fetching report."); | |
| return; | |
| } | |
| document.querySelector('.main-content').style.display = 'none'; | |
| document.querySelector('nav').style.display = 'none'; | |
| document.querySelector('.market-ticker-bar').style.display = 'none'; | |
| reportContainer.style.display = 'block'; | |
| reportView.src = '/report?t=' + new Date().getTime(); | |
| }; | |
| window.closeReport = function () { | |
| document.getElementById('reportContainer').style.display = 'none'; | |
| document.querySelector('.main-content').style.display = 'block'; | |
| document.querySelector('nav').style.display = 'flex'; | |
| document.querySelector('.market-ticker-bar').style.display = 'flex'; | |
| }; | |
| // Ambient Background relies entirely on Vanta JS now. | |
| // --- AUTHENTICATION FLOW --- | |
| window.logout = function () { | |
| window.safeSessionRem('accessKey'); | |
| window.location.href = '/'; | |
| }; | |
| // --- 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 style="color: #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); | |
| 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) benchmarkChartInstance.destroy(); | |
| 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 }) | |
| }); | |
| 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 | |
| // ───────────────────────────────────────────── | |
| let statArbChartInstance = null; | |
| async function runStatArbScan() { | |
| const tickersStr = document.getElementById('statarb-tickers').value; | |
| const btn = document.getElementById('btn-run-statarb'); | |
| if (!tickersStr) return; | |
| const tickers = tickersStr.split(',').map(t => t.trim().toUpperCase()).filter(t => t); | |
| if (tickers.length < 2) { | |
| alert("Please enter at least two tickers"); | |
| return; | |
| } | |
| btn.disabled = true; | |
| btn.textContent = "Scanning..."; | |
| try { | |
| const response = await fetch('/api/statarb/scan', { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| 'X-Access-Key': window.safeSessionGet('accessKey') || window.safeLocalGet('accessKey') | |
| }, | |
| body: JSON.stringify({ tickers: tickers, run_backtest: true }) | |
| }); | |
| if (!response.ok) { | |
| const err = await response.json(); | |
| throw new Error(err.detail || "Failed to scan pairs"); | |
| } | |
| const data = await response.json(); | |
| document.getElementById('statarb-results-container').style.display = 'block'; | |
| // Render Pairs Table | |
| const pairsTbody = document.querySelector('#pairs-table tbody'); | |
| pairsTbody.innerHTML = ''; | |
| data.pairs.forEach(p => { | |
| const tr = document.createElement('tr'); | |
| tr.innerHTML = ` | |
| <td style="font-weight: bold;">${p.pair[0]} / ${p.pair[1]}</td> | |
| <td style="color: ${p.p_value < 0.01 ? '#4ade80' : '#facc15'};">${p.p_value.toFixed(4)}</td> | |
| <td>${p.half_life.toFixed(1)} days</td> | |
| <td>${p.hedge_ratio.toFixed(3)}</td> | |
| `; | |
| pairsTbody.appendChild(tr); | |
| }); | |
| // Render Top Pair Backtest | |
| const bt = data.top_pair_backtest; | |
| if (bt && !bt.error) { | |
| document.getElementById('statarb-top-pair').textContent = bt.pair; | |
| document.getElementById('statarb-ret').textContent = (bt.total_return * 100).toFixed(2) + '%'; | |
| document.getElementById('statarb-vol').textContent = (bt.annualized_volatility * 100).toFixed(2) + '%'; | |
| document.getElementById('statarb-sharpe').textContent = bt.sharpe_ratio.toFixed(2); | |
| if (statArbChartInstance) { | |
| statArbChartInstance.destroy(); | |
| } | |
| const ctx = document.getElementById('statarb-chart').getContext('2d'); | |
| statArbChartInstance = new Chart(ctx, { | |
| type: 'line', | |
| data: { | |
| labels: bt.dates, | |
| datasets: [ | |
| { | |
| label: 'Z-Score', | |
| data: bt.z_scores, | |
| borderColor: '#8b5cf6', | |
| borderWidth: 1.5, | |
| pointRadius: 0, | |
| yAxisID: 'y' | |
| }, | |
| { | |
| label: 'Equity Curve', | |
| data: bt.equity_curve.map(x => (x - 1) * 100), | |
| borderColor: '#4ade80', | |
| borderWidth: 2, | |
| pointRadius: 0, | |
| yAxisID: 'y1' | |
| } | |
| ] | |
| }, | |
| options: { | |
| responsive: true, | |
| maintainAspectRatio: false, | |
| interaction: { mode: 'index', intersect: false }, | |
| scales: { | |
| y: { type: 'linear', display: true, position: 'left', title: {display: true, text: 'Z-Score'} }, | |
| y1: { type: 'linear', display: true, position: 'right', grid: {drawOnChartArea: false}, title: {display: true, text: 'PnL %'} }, | |
| x: { display: false } | |
| }, | |
| plugins: { legend: { labels: { color: '#cbd5e1' } } } | |
| } | |
| }); | |
| } | |
| } catch (err) { | |
| alert("Error: " + err.message); | |
| } finally { | |
| btn.disabled = false; | |
| btn.textContent = "Scan Pairs"; | |
| } | |
| } | |
| // ───────────────────────────────────────────── | |
| // CRYPTO ARBITRAGE | |
| // ───────────────────────────────────────────── | |
| let cryptoArbDepthChartInstance = null; | |
| let cryptoArbSpreadChartInstance = null; | |
| let spreadHistory = []; | |
| async function runCryptoArbScan() { | |
| const symbol = document.getElementById('cryptoarb-symbol').value; | |
| const capital = document.getElementById('cryptoarb-capital').value; | |
| const btn = document.getElementById('btn-run-cryptoarb'); | |
| if (!symbol) return; | |
| btn.disabled = true; | |
| btn.textContent = "Scanning..."; | |
| try { | |
| const response = await fetch('/api/cryptoarb/scan', { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| 'X-Access-Key': window.safeSessionGet('accessKey') || window.safeLocalGet('accessKey') | |
| }, | |
| body: JSON.stringify({ symbol: symbol.toUpperCase(), capital: parseFloat(capital) || 10000.0 }) | |
| }); | |
| if (!response.ok) { | |
| const err = await response.json(); | |
| throw new Error(err.detail || "Failed to scan order books"); | |
| } | |
| const data = await response.json(); | |
| document.getElementById('cryptoarb-results-container').style.display = 'block'; | |
| // Render Prices | |
| const pricesDiv = document.getElementById('cryptoarb-prices'); | |
| pricesDiv.innerHTML = ''; | |
| for (const [exchange, p] of Object.entries(data.prices)) { | |
| const box = document.createElement('div'); | |
| box.style = 'background: rgba(0,0,0,0.2); border: 1px solid rgba(255,255,255,0.05); padding: 1rem; border-radius: 4px;'; | |
| box.innerHTML = ` | |
| <div style="font-weight: bold; color: #cbd5e1; margin-bottom: 0.5rem; text-transform: capitalize;">${exchange}</div> | |
| <div style="display: flex; justify-content: space-between; font-size: 0.9rem;"> | |
| <span style="color: #4ade80;">Bid: $${(p.bid || 0).toFixed(2)}</span> | |
| <span style="color: #f87171;">Ask: $${(p.ask || 0).toFixed(2)}</span> | |
| </div> | |
| `; | |
| pricesDiv.appendChild(box); | |
| } | |
| // Render Opportunities | |
| const oppsTbody = document.querySelector('#cryptoarb-opps-table tbody'); | |
| if (oppsTbody) { | |
| oppsTbody.innerHTML = ''; | |
| if (data.opportunities && data.opportunities.length > 0) { | |
| data.opportunities.forEach(opp => { | |
| const tr = document.createElement('tr'); | |
| tr.innerHTML = ` | |
| <td style="font-weight: bold;">Buy ${opp.buy_exchange} / Sell ${opp.sell_exchange}</td> | |
| <td style="color: #f87171;">$${opp.buy_price.toFixed(2)}</td> | |
| <td style="color: #4ade80;">$${opp.sell_price.toFixed(2)}</td> | |
| <td>${(opp.gross_spread_pct * 100).toFixed(3)}%</td> | |
| <td style="color: ${opp.net_spread_pct > 0 ? '#4ade80' : '#facc15'};">${(opp.net_spread_pct * 100).toFixed(3)}%</td> | |
| `; | |
| oppsTbody.appendChild(tr); | |
| }); | |
| } | |
| } | |
| // Setup Charts | |
| const depthCtx = document.getElementById('cryptoarb-depth-chart').getContext('2d'); | |
| const spreadCtx = document.getElementById('cryptoarb-spread-chart').getContext('2d'); | |
| if (cryptoArbDepthChartInstance) cryptoArbDepthChartInstance.destroy(); | |
| if (cryptoArbSpreadChartInstance) cryptoArbSpreadChartInstance.destroy(); | |
| // 1. Order Book Depth Chart | |
| if (data.opportunities && data.opportunities.length > 0) { | |
| const bestOpp = data.opportunities[0]; | |
| const buyEx = bestOpp.buy_exchange; | |
| const sellEx = bestOpp.sell_exchange; | |
| const exBids = data.prices[sellEx]?.bids || []; | |
| const exAsks = data.prices[buyEx]?.asks || []; | |
| let allPrices = [...exBids.map(b => b.price), ...exAsks.map(a => a.price)]; | |
| allPrices.sort((a,b) => a - b); | |
| allPrices = [...new Set(allPrices)]; | |
| const labels = allPrices.map(p => p.toFixed(2)); | |
| const bids = allPrices.map(p => { | |
| const b = exBids.find(x => x.price === p); | |
| return b ? b.qty : 0; | |
| }); | |
| const asks = allPrices.map(p => { | |
| const a = exAsks.find(x => x.price === p); | |
| return a ? a.qty : 0; | |
| }); | |
| cryptoArbDepthChartInstance = new Chart(depthCtx, { | |
| type: 'bar', | |
| data: { | |
| labels: labels, | |
| datasets: [ | |
| { label: `Bids (${sellEx})`, data: bids, backgroundColor: 'rgba(74, 222, 128, 0.7)' }, | |
| { label: `Asks (${buyEx})`, data: asks, backgroundColor: 'rgba(248, 113, 113, 0.7)' } | |
| ] | |
| }, | |
| options: { | |
| responsive: true, maintainAspectRatio: false, | |
| scales: { | |
| x: { stacked: true, ticks: { color: '#94a3b8' }, grid: { display: false } }, | |
| y: { stacked: true, ticks: { color: '#94a3b8' }, grid: { color: 'rgba(255,255,255,0.05)' } } | |
| }, | |
| plugins: { legend: { display: false } } | |
| } | |
| }); | |
| // 2. Spread Tracking Chart | |
| spreadHistory.push({ time: new Date().toLocaleTimeString(), spread: bestOpp.net_spread_pct * 100 }); | |
| if(spreadHistory.length > 20) spreadHistory.shift(); | |
| cryptoArbSpreadChartInstance = new Chart(spreadCtx, { | |
| type: 'line', | |
| data: { | |
| labels: spreadHistory.map(h => h.time), | |
| datasets: [{ | |
| label: 'Net Spread (%)', | |
| data: spreadHistory.map(h => h.spread), | |
| borderColor: '#60a5fa', | |
| backgroundColor: 'rgba(96, 165, 250, 0.1)', | |
| fill: true, | |
| tension: 0.4 | |
| }] | |
| }, | |
| options: { | |
| responsive: true, maintainAspectRatio: false, | |
| scales: { | |
| x: { ticks: { color: '#94a3b8', maxRotation: 0 }, grid: { display: false } }, | |
| y: { ticks: { color: '#94a3b8' }, grid: { color: 'rgba(255,255,255,0.05)' } } | |
| }, | |
| plugins: { legend: { display: false } } | |
| } | |
| }); | |
| } | |
| // Render Execution | |
| const execDiv = document.getElementById('cryptoarb-execution'); | |
| const costDiv = document.getElementById('cryptoarb-cost-breakdown'); | |
| if (data.execution && !data.execution.error && data.opportunities && data.opportunities.length > 0) { | |
| const e = data.execution; | |
| execDiv.style.borderColor = e.status === 'FILLED' ? 'rgba(74, 222, 128, 0.3)' : 'rgba(248, 113, 113, 0.3)'; | |
| execDiv.innerHTML = `[${new Date().toISOString()}] EXECUTION SIMULATION\n\n` + | |
| `Status: <span style="color: ${e.status === 'FILLED' ? '#4ade80' : '#f87171'}; font-weight: bold;">${e.status}</span>\n` + | |
| `Capital Deployed: $${e.capital_deployed.toFixed(2)}\n` + | |
| `Target Spread: ${(data.opportunities[0].net_spread_pct * 100).toFixed(3)}%\n` + | |
| `Realized Net %: <span style="color: ${e.net_profit_pct > 0 ? '#4ade80' : '#f87171'};">${(e.net_profit_pct * 100).toFixed(3)}%</span>\n` + | |
| `Realized Net USD: <span style="color: ${e.net_profit_usd > 0 ? '#4ade80' : '#f87171'};">$${e.net_profit_usd.toFixed(2)}</span>\n`; | |
| costDiv.innerHTML = ` | |
| <div style="display: flex; justify-content: space-between; margin-bottom: 0.5rem;"> | |
| <span>Exchange Fees:</span> <span style="color: #facc15;">-0.200% (Two legs)</span> | |
| </div> | |
| <div style="display: flex; justify-content: space-between; margin-bottom: 0.5rem;"> | |
| <span>Latency Decay (50ms):</span> <span style="color: #f87171;">-${(e.latency_penalty_pct * 100).toFixed(3)}%</span> | |
| </div> | |
| <div style="display: flex; justify-content: space-between; margin-bottom: 0.5rem;"> | |
| <span>Market Impact (Slippage):</span> <span style="color: #f87171;">-${(e.impact_penalty_pct * 100).toFixed(3)}%</span> | |
| </div> | |
| <hr style="border: 0; border-top: 1px solid rgba(255,255,255,0.1); margin: 0.5rem 0;"> | |
| <div style="display: flex; justify-content: space-between; font-weight: bold;"> | |
| <span>Total Lost to Inefficiencies:</span> <span style="color: #f87171;">-${((0.002 + e.latency_penalty_pct + e.impact_penalty_pct)*100).toFixed(3)}%</span> | |
| </div> | |
| `; | |
| } else { | |
| execDiv.style.borderColor = 'rgba(255,255,255,0.05)'; | |
| execDiv.innerHTML = "No profitable opportunities found after fees."; | |
| costDiv.innerHTML = "No execution attempted."; | |
| } | |
| } catch (err) { | |
| alert("Error: " + err.message); | |
| } finally { | |
| btn.disabled = false; | |
| btn.textContent = "Scan Order Books"; | |
| } | |
| } | |
| document.addEventListener('DOMContentLoaded', () => { | |
| // pending_options are kept separate from the main Engine Room's 'tickers' input | |
| // so they do not pollute the equities portfolio. | |
| }); | |
| function renderVolatilitySurface(data) { | |
| const strikes = data.calls.map(c => c.strike); | |
| const ivs = data.calls.map(c => c.impliedVolatility || 0.2); | |
| const exp_days = [7, 14, 30, 60, 90]; | |
| const z_data = []; | |
| for(let i=0; i<exp_days.length; i++) { | |
| let row = []; | |
| for(let j=0; j<strikes.length; j++) { | |
| let iv = ivs[j]; | |
| let baseline = 0.15; | |
| let time_effect = (exp_days[i] / 30.0); | |
| let synth_iv = baseline + (iv - baseline) / Math.sqrt(time_effect); | |
| row.push(synth_iv * 100); | |
| } | |
| z_data.push(row); | |
| } | |
| var surface_trace = { | |
| z: z_data, | |
| x: strikes, | |
| y: exp_days, | |
| type: 'surface', | |
| colorscale: 'Viridis', | |
| showscale: false | |
| }; | |
| var layout = { | |
| title: 'Implied Volatility Surface (Synthetic Expirations)', | |
| paper_bgcolor: 'rgba(0,0,0,0)', | |
| plot_bgcolor: 'rgba(0,0,0,0)', | |
| font: { color: '#f8fafc' }, | |
| scene: { | |
| xaxis: { title: 'Strike ($)', gridcolor: 'rgba(255,255,255,0.1)' }, | |
| yaxis: { title: 'Days to Expiry', gridcolor: 'rgba(255,255,255,0.1)' }, | |
| zaxis: { title: 'Implied Volatility (%)', gridcolor: 'rgba(255,255,255,0.1)' } | |
| }, | |
| margin: { l: 0, r: 0, b: 0, t: 40 } | |
| }; | |
| Plotly.newPlot('volatility-surface-plot', [surface_trace], layout, {responsive: true}); | |
| } | |
| async function generateOptionsStrategy() { | |
| const ticker = document.getElementById('options-ticker').value; | |
| if (!ticker) { | |
| alert('Please fetch an option chain first.'); | |
| return; | |
| } | |
| const out = document.getElementById('options-ai-output'); | |
| out.innerHTML = '<span style="color: #60a5fa; animation: pulse 1s infinite;">NOVA is analyzing the volatility surface...</span>'; | |
| try { | |
| const prompt = "You are a quantitative options trader. Analyze the ticker " + ticker.toUpperCase() + ". Suggest 2 advanced options strategies (e.g., Iron Condor, Straddle). You must include: 1. Correct max risk and max reward calculations based on assumed credit/debit. 2. Probability of profit (e.g. using 1 std dev). 3. Expected Value (EV) calculation. 4. Exact Breakeven points. 5. Clear Exit Strategy (take profit/stop loss levels). 6. Adjustment plan if the trade goes against us. Explain the Greeks logic. Format nicely."; | |
| const response = await fetch('/api/options/generate', { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| 'X-Access-Key': window.safeSessionGet('accessKey') || window.safeLocalGet('accessKey') | |
| }, | |
| body: JSON.stringify({ query: prompt }) | |
| }); | |
| if (!response.ok) throw new Error('Failed to generate strategy'); | |
| const data = await response.json(); | |
| if (data.status === 'error') throw new Error(data.detail); | |
| let replyHtml = ''; | |
| if (typeof marked !== 'undefined') { | |
| replyHtml = marked.parse(data.reply); | |
| } else { | |
| replyHtml = data.reply; | |
| } | |
| // Post-process [RECOMMENDED] tags | |
| replyHtml = replyHtml.replace(/\[RECOMMENDED\]/g, '<span class="recommended-badge">Recommended</span>'); | |
| out.innerHTML = replyHtml; | |
| // Apply configuration if returned | |
| if (data.configuration) { | |
| const conf = data.configuration; | |
| if (conf.opt_capital) document.getElementById('opt-capital').value = conf.opt_capital; | |
| if (conf.opt_goal) document.getElementById('opt-goal').value = conf.opt_goal; | |
| if (conf.opt_risk) { | |
| const el = document.getElementById('opt-risk'); | |
| if (el) { | |
| el.value = conf.opt_risk; | |
| document.getElementById('opt-risk-val').innerText = conf.opt_risk; | |
| } | |
| } | |
| if (conf.tickers) { | |
| // Automatically stage the generated options to pending_options so they will be available in Options Explorer | |
| let saved = window.safeLocalGet('pending_options') || ''; | |
| let vals = saved ? saved.split(',') : []; | |
| let newTickers = conf.tickers.split(',').map(x => x.trim()).filter(x => x); | |
| newTickers.forEach(t => { | |
| if (!vals.includes(t)) vals.push(t); | |
| }); | |
| window.safeLocalSet('pending_options', vals.join(',')); | |
| alert(`NOVA has configured your Options Matrix and staged ${newTickers.length} option(s) in your Options Sandbox!`); | |
| } | |
| } | |
| } catch (err) { | |
| out.innerHTML = '<span style="color: #ef4444;">Error: ' + err.message + '</span>'; | |
| } | |
| } | |
| // Custom Alert to prevent iframe blocking | |
| window.alert = function(msg) { | |
| const toast = document.createElement('div'); | |
| toast.style.position = 'fixed'; | |
| toast.style.bottom = '20px'; | |
| toast.style.right = '20px'; | |
| toast.style.background = 'rgba(15, 23, 42, 0.9)'; | |
| toast.style.color = '#f8fafc'; | |
| toast.style.padding = '15px 25px'; | |
| toast.style.borderRadius = '8px'; | |
| toast.style.boxShadow = '0 10px 25px rgba(0,0,0,0.5)'; | |
| toast.style.borderLeft = '4px solid #3b82f6'; | |
| toast.style.zIndex = '10000'; | |
| toast.style.fontFamily = 'monospace'; | |
| toast.style.transition = 'opacity 0.4s ease'; | |
| toast.innerText = msg; | |
| document.body.appendChild(toast); | |
| setTimeout(() => { toast.style.opacity = '0'; setTimeout(() => toast.remove(), 400); }, 4000); | |
| }; | |
| // Custom Prompt | |
| window.asyncPrompt = function(msg, defaultVal) { | |
| return new Promise(resolve => { | |
| const bg = document.createElement('div'); | |
| bg.style.position = 'fixed'; bg.style.top='0'; bg.style.left='0'; bg.style.width='100vw'; bg.style.height='100vh'; bg.style.background='rgba(0,0,0,0.8)'; bg.style.zIndex='10001'; bg.style.display='flex'; bg.style.alignItems='center'; bg.style.justifyContent='center'; | |
| const box = document.createElement('div'); | |
| box.style.background='#1e293b'; box.style.padding='20px'; box.style.borderRadius='8px'; box.style.color='#fff'; box.style.display='flex'; box.style.flexDirection='column'; box.style.gap='10px'; | |
| const text = document.createElement('div'); text.innerText = msg; | |
| const inp = document.createElement('input'); inp.value = defaultVal||''; inp.style.padding='8px'; inp.style.background='#0f172a'; inp.style.color='#fff'; inp.style.border='1px solid #3b82f6'; inp.style.outline='none'; | |
| const btn = document.createElement('button'); btn.innerText='Submit'; btn.style.padding='8px'; btn.style.background='#3b82f6'; btn.style.color='#fff'; btn.style.border='none'; btn.style.cursor='pointer'; | |
| btn.onclick = () => { bg.remove(); resolve(inp.value); }; | |
| box.appendChild(text); box.appendChild(inp); box.appendChild(btn); bg.appendChild(box); document.body.appendChild(bg); | |
| inp.focus(); | |
| }); | |
| }; | |
| // Custom Confirm | |
| window.asyncConfirm = function(msg) { | |
| return new Promise(resolve => { | |
| const bg = document.createElement('div'); | |
| bg.style.position = 'fixed'; bg.style.top='0'; bg.style.left='0'; bg.style.width='100vw'; bg.style.height='100vh'; bg.style.background='rgba(0,0,0,0.8)'; bg.style.zIndex='10001'; bg.style.display='flex'; bg.style.alignItems='center'; bg.style.justifyContent='center'; | |
| const box = document.createElement('div'); | |
| box.style.background='#1e293b'; box.style.padding='20px'; box.style.borderRadius='8px'; box.style.color='#fff'; box.style.display='flex'; box.style.flexDirection='column'; box.style.gap='15px'; box.style.minWidth='300px'; | |
| const text = document.createElement('div'); text.innerText = msg; | |
| const row = document.createElement('div'); row.style.display='flex'; row.style.gap='10px'; row.style.justifyContent='flex-end'; | |
| const btnYes = document.createElement('button'); btnYes.innerText='Yes'; btnYes.style.padding='8px 16px'; btnYes.style.background='#ef4444'; btnYes.style.color='#fff'; btnYes.style.border='none'; btnYes.style.cursor='pointer'; btnYes.style.borderRadius='4px'; | |
| const btnNo = document.createElement('button'); btnNo.innerText='Cancel'; btnNo.style.padding='8px 16px'; btnNo.style.background='#3b82f6'; btnNo.style.color='#fff'; btnNo.style.border='none'; btnNo.style.cursor='pointer'; btnNo.style.borderRadius='4px'; | |
| btnYes.onclick = () => { bg.remove(); resolve(true); }; | |
| btnNo.onclick = () => { bg.remove(); resolve(false); }; | |
| row.appendChild(btnNo); row.appendChild(btnYes); box.appendChild(text); box.appendChild(row); bg.appendChild(box); document.body.appendChild(bg); | |
| }); | |
| }; | |
| // --- EASTER EGG: THE QUANT'S TERMINAL V2 --- | |
| (function() { | |
| const konamiCode = [38, 38, 40, 40, 37, 39, 37, 39, 66, 65]; | |
| let konamiIndex = 0; | |
| const allTrades = [ | |
| "[1929-10-29 15:59:59] SELL DJIA 900x @ 230.07 ██████ BLACK TUESDAY", | |
| "[1987-10-19 09:31:02] SELL SPX 500x @ 282.70 ██████ PORTFOLIO INSURANCE TRIGGERED", | |
| "[1992-09-16 08:00:00] SHORT GBP 10B @ 1.95 ██████ SOROS BREAKS BOE", | |
| "[1998-08-17 14:22:11] BUY RUBL 10M @ 0.0041 ██████ LTCM CONVERGENCE TRADE", | |
| "[2000-03-10 16:00:00] SELL NDX 200x @ 5048.62 ██████ DOT COM PEAK", | |
| "[2008-09-15 06:00:00] SHORT LEH 999x @ 3.65 ██████ LEHMAN BROTHERS — FINAL BELL", | |
| "[2010-05-06 14:42:44] BUY ES 1x @ 1056.00 ██████ FLASH CRASH BOTTOM TICK", | |
| "[2015-01-15 09:30:00] BUY CHF 50M @ 1.20 ██████ SNB UNPEGS FRANCS", | |
| "[2020-04-20 14:08:00] BUY WTI 100x @ -37.63 ██████ OIL GOES NEGATIVE", | |
| "[2021-01-28 09:30:01] BUY GME 420x @ 347.51 ██████ DIAMOND HANDS PROTOCOL", | |
| "[2022-11-08 10:00:00] SHORT FTT 500x @ 15.00 ██████ FTX LIQUIDITY CRISIS", | |
| "[2026-07-01 NOW ] RUN WEALTH_ENGINE ██████ YOU FOUND THE TERMINAL." | |
| ]; | |
| const asciiArt = `╔══════════════════════════════════════════╗ | |
| ║ "The market can stay irrational longer ║ | |
| ║ than you can stay solvent." — Keynes ║ | |
| ║ ║ | |
| ║ Welcome to the inner circle. ║ | |
| ╚══════════════════════════════════════════╝`; | |
| const quotes = [ | |
| "Risk comes from not knowing what you're doing. - Warren Buffett", | |
| "In investing, what is comfortable is rarely profitable. - Robert Arnott", | |
| "Amateurs think about how much money they can make. Professionals think about how much money they could lose. - Jack Schwager", | |
| "Bulls make money, bears make money, pigs get slaughtered. - Unknown", | |
| "The elements of good trading are: 1. Cutting losses, 2. Cutting losses, and 3. Cutting losses. - Ed Seykota" | |
| ]; | |
| let audioCtx = null; | |
| function initAudio() { | |
| if (!audioCtx) { | |
| audioCtx = new (window.AudioContext || window.webkitAudioContext)(); | |
| } | |
| if (audioCtx.state === 'suspended') { | |
| audioCtx.resume(); | |
| } | |
| } | |
| function playTone(freq, type, duration, vol) { | |
| if (!audioCtx) return; | |
| const osc = audioCtx.createOscillator(); | |
| const gain = audioCtx.createGain(); | |
| osc.type = type; | |
| osc.frequency.setValueAtTime(freq, audioCtx.currentTime); | |
| gain.gain.setValueAtTime(vol, audioCtx.currentTime); | |
| gain.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + duration); | |
| osc.connect(gain); | |
| gain.connect(audioCtx.destination); | |
| osc.start(); | |
| osc.stop(audioCtx.currentTime + duration); | |
| } | |
| function playKeyClick() { | |
| playTone(400 + Math.random() * 200, 'square', 0.03, 0.05); | |
| } | |
| document.addEventListener('keydown', function(e) { | |
| if (e.keyCode === konamiCode[konamiIndex]) { | |
| konamiIndex++; | |
| if (konamiIndex === konamiCode.length) { | |
| konamiIndex = 0; | |
| activateTerminal(); | |
| } | |
| } else { | |
| konamiIndex = 0; | |
| } | |
| }); | |
| function shuffle(array) { | |
| let currentIndex = array.length, randomIndex; | |
| while (currentIndex !== 0) { | |
| randomIndex = Math.floor(Math.random() * currentIndex); | |
| currentIndex--; | |
| [array[currentIndex], array[randomIndex]] = [array[randomIndex], array[currentIndex]]; | |
| } | |
| return array; | |
| } | |
| function activateTerminal() { | |
| if (window.WealthEngineEggs) window.WealthEngineEggs.discover('terminal'); | |
| if (document.querySelector('.crt-overlay') && !document.querySelector('.crt-flash')) { | |
| return; | |
| } | |
| initAudio(); | |
| if (audioCtx) { | |
| const hum = audioCtx.createOscillator(); | |
| const humGain = audioCtx.createGain(); | |
| hum.type = 'sine'; | |
| hum.frequency.setValueAtTime(50, audioCtx.currentTime); | |
| humGain.gain.setValueAtTime(0.05, audioCtx.currentTime); | |
| hum.connect(humGain); | |
| humGain.connect(audioCtx.destination); | |
| hum.start(); | |
| window.terminalHum = { osc: hum, gain: humGain }; | |
| } | |
| const overlay = document.createElement('div'); | |
| overlay.className = 'crt-overlay'; | |
| const canvas = document.createElement('canvas'); | |
| canvas.className = 'crt-matrix-canvas'; | |
| overlay.appendChild(canvas); | |
| const scanlines = document.createElement('div'); | |
| scanlines.className = 'crt-scanlines'; | |
| const vignette = document.createElement('div'); | |
| vignette.className = 'crt-vignette'; | |
| const curve = document.createElement('div'); | |
| curve.className = 'crt-curve'; | |
| const contentContainer = document.createElement('div'); | |
| contentContainer.className = 'crt-content-container'; | |
| const content = document.createElement('div'); | |
| content.id = 'crt-content'; | |
| contentContainer.appendChild(content); | |
| curve.appendChild(contentContainer); | |
| overlay.appendChild(scanlines); | |
| overlay.appendChild(vignette); | |
| overlay.appendChild(curve); | |
| document.body.appendChild(overlay); | |
| canvas.width = window.innerWidth; | |
| canvas.height = window.innerHeight; | |
| const ctx = canvas.getContext('2d'); | |
| const matrixChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789$¥€£%αβσΔΓΘ".split(""); | |
| const fontSize = 16; | |
| const columns = canvas.width / fontSize; | |
| const drops = []; | |
| for (let x = 0; x < columns; x++) drops[x] = 1; | |
| let matrixInterval = setInterval(() => { | |
| ctx.fillStyle = "rgba(0, 0, 0, 0.05)"; | |
| ctx.fillRect(0, 0, canvas.width, canvas.height); | |
| ctx.fillStyle = "#0F0"; | |
| ctx.font = fontSize + "px monospace"; | |
| for (let i = 0; i < drops.length; i++) { | |
| const text = matrixChars[Math.floor(Math.random() * matrixChars.length)]; | |
| ctx.fillText(text, i * fontSize, drops[i] * fontSize); | |
| if (drops[i] * fontSize > canvas.height && Math.random() > 0.975) drops[i] = 0; | |
| drops[i]++; | |
| } | |
| }, 33); | |
| overlay.style.display = 'flex'; | |
| overlay.classList.add('crt-flash'); | |
| let tradesCopy = shuffle([...allTrades]).slice(0, 8); | |
| tradesCopy.unshift("WEALTH ENGINE TERMINAL v2.0 — CLASSIFIED\n"); | |
| let currentLine = 0; | |
| let currentChar = 0; | |
| function appendLine(text, cssClass = '') { | |
| const lineDiv = document.createElement('div'); | |
| lineDiv.className = 'crt-text-line ' + cssClass; | |
| lineDiv.innerHTML = text; | |
| content.appendChild(lineDiv); | |
| contentContainer.scrollTop = contentContainer.scrollHeight; | |
| return lineDiv; | |
| } | |
| let glitchTimer; | |
| function triggerGlitch() { | |
| curve.classList.add('crt-glitch'); | |
| setTimeout(() => { | |
| curve.classList.remove('crt-glitch'); | |
| }, 200 + Math.random() * 300); | |
| } | |
| function typeChar() { | |
| if (currentLine >= tradesCopy.length) { | |
| setTimeout(() => { | |
| initInteractivePrompt(); | |
| }, 400); | |
| return; | |
| } | |
| const text = tradesCopy[currentLine]; | |
| if (currentChar === 0) { | |
| const lineDiv = document.createElement('div'); | |
| lineDiv.className = 'crt-text-line'; | |
| lineDiv.id = 'crt-line-' + currentLine; | |
| content.appendChild(lineDiv); | |
| if (Math.random() < 0.15) triggerGlitch(); | |
| } | |
| const lineEl = document.getElementById('crt-line-' + currentLine); | |
| if (currentChar < text.length) { | |
| lineEl.textContent += text[currentChar]; | |
| currentChar++; | |
| playKeyClick(); | |
| contentContainer.scrollTop = contentContainer.scrollHeight; | |
| setTimeout(typeChar, 10 + Math.random() * 30); | |
| } else { | |
| currentLine++; | |
| currentChar = 0; | |
| setTimeout(typeChar, 200); | |
| } | |
| } | |
| setTimeout(() => { | |
| overlay.classList.remove('crt-flash'); | |
| typeChar(); | |
| }, 800); | |
| function initInteractivePrompt() { | |
| const promptLine = document.createElement('div'); | |
| promptLine.className = 'crt-prompt-line'; | |
| const promptLabel = document.createElement('span'); | |
| promptLabel.className = 'crt-prompt'; | |
| promptLabel.textContent = 'WEALTH_ENGINE>'; | |
| const inputEl = document.createElement('input'); | |
| inputEl.type = 'text'; | |
| inputEl.className = 'crt-input'; | |
| inputEl.spellcheck = false; | |
| promptLine.appendChild(promptLabel); | |
| promptLine.appendChild(inputEl); | |
| content.appendChild(promptLine); | |
| inputEl.focus(); | |
| contentContainer.scrollTop = contentContainer.scrollHeight; | |
| inputEl.addEventListener('keydown', function(e) { | |
| initAudio(); | |
| playKeyClick(); | |
| if (e.key === 'Enter') { | |
| const cmd = this.value.trim().toLowerCase(); | |
| const rawCmd = this.value; | |
| this.disabled = true; | |
| this.parentElement.innerHTML = '<span class="crt-prompt">WEALTH_ENGINE></span><span style="color:#33ff33;">' + rawCmd + '</span>'; | |
| processCommand(cmd); | |
| } | |
| }); | |
| document.addEventListener('click', () => { | |
| if(document.contains(inputEl)) inputEl.focus(); | |
| }); | |
| } | |
| let pnlInterval; | |
| function processCommand(cmd) { | |
| if (cmd === '') { | |
| initInteractivePrompt(); | |
| return; | |
| } | |
| if (cmd.startsWith('decrypt ')) { | |
| const key = cmd.split(' ')[1].toUpperCase(); | |
| if (key === (window.MASTER_KEY || "7F4B29A1E8C6")) { | |
| appendLine("[SYSTEM] ACCESS GRANTED. VAULT UNLOCKED.", "highlight"); | |
| if (typeof triggerGlitch === 'function') triggerGlitch(); | |
| const overlay = document.querySelector('.crt-overlay'); | |
| if (overlay) overlay.style.backgroundColor = "rgba(0, 255, 0, 0.1)"; | |
| setTimeout(() => { | |
| if (overlay) overlay.style.backgroundColor = ""; | |
| if (window.WealthEngineEggs) window.WealthEngineEggs.unlockVault(); | |
| executeExit(); | |
| setTimeout(() => { if (window.switchView) window.switchView('view-innercircle'); if (typeof renderInnerCircle === 'function') renderInnerCircle(); }, 1000); | |
| }, 2000); | |
| } else { | |
| appendLine("[SYSTEM] ACCESS DENIED. INVALID HASH SEQUENCE.", "highlight"); | |
| } | |
| setTimeout(initInteractivePrompt, 500); | |
| return; | |
| } | |
| switch(cmd) { | |
| case 'help': | |
| appendLine("Available commands:"); | |
| appendLine(" help - Show this message"); | |
| appendLine(" whoami - Display current user context"); | |
| appendLine(" trades - Show legendary trade log"); | |
| appendLine(" fortune - Wisdom from the masters"); | |
| appendLine(" status - Live PnL monitor"); | |
| appendLine(" hack - Establish uplink to The Fed"); | |
| appendLine(" clear - Clear terminal screen"); | |
| appendLine(" exit - Disconnect from terminal"); | |
| appendLine(" decrypt - Unlock restricted sectors"); | |
| initInteractivePrompt(); | |
| break; | |
| case 'whoami': | |
| appendLine("Rogue Quant. Access Level: CLASSIFIED.", "highlight"); | |
| initInteractivePrompt(); | |
| break; | |
| case 'trades': | |
| tradesCopy.forEach(t => appendLine(t)); | |
| initInteractivePrompt(); | |
| break; | |
| case 'fortune': | |
| const q = quotes[Math.floor(Math.random() * quotes.length)]; | |
| appendLine('"' + q + '"', "highlight"); | |
| initInteractivePrompt(); | |
| break; | |
| case 'status': | |
| appendLine("Connecting to portfolio stream..."); | |
| const pnlLine = appendLine("PnL: <span style='color:cyan'>$0.00</span>"); | |
| let pnl = 0; | |
| if(pnlInterval) clearInterval(pnlInterval); | |
| pnlInterval = setInterval(() => { | |
| pnl += (Math.random() * 1000) - 400; | |
| if(document.contains(pnlLine)) { | |
| pnlLine.innerHTML = "PnL: <span style='color:cyan'>$" + pnl.toFixed(2) + "</span>"; | |
| } else { | |
| clearInterval(pnlInterval); | |
| } | |
| }, 500); | |
| setTimeout(() => { | |
| appendLine("Stream detached."); | |
| clearInterval(pnlInterval); | |
| initInteractivePrompt(); | |
| }, 5000); | |
| break; | |
| case 'hack': | |
| appendLine("Initiating bypass sequence..."); | |
| const progressLine = document.createElement('div'); | |
| progressLine.className = 'crt-text-line'; | |
| progressLine.innerHTML = `Exploiting JPow API: <div class="crt-progress-bar"><div class="crt-progress-fill"></div></div>`; | |
| content.appendChild(progressLine); | |
| const fill = progressLine.querySelector('.crt-progress-fill'); | |
| let w = 0; | |
| const hackInt = setInterval(() => { | |
| w += Math.random() * 15; | |
| if (w > 100) w = 100; | |
| fill.style.width = w + '%'; | |
| playKeyClick(); | |
| if (w >= 100) { | |
| clearInterval(hackInt); | |
| setTimeout(() => { | |
| appendLine("ACCESS GRANTED. MONEY PRINTER GO BRRRRR.", "highlight"); | |
| triggerGlitch(); | |
| initInteractivePrompt(); | |
| }, 500); | |
| } | |
| }, 200); | |
| break; | |
| case 'clear': | |
| content.innerHTML = ''; | |
| initInteractivePrompt(); | |
| break; | |
| case 'exit': | |
| executeExit(); | |
| break; | |
| default: | |
| appendLine(`Command not found: ${cmd}. Type 'help' for available commands.`); | |
| initInteractivePrompt(); | |
| } | |
| } | |
| const escHandler = (e) => { | |
| if (e.key === 'Escape') { | |
| document.removeEventListener('keydown', escHandler); | |
| executeExit(); | |
| } | |
| }; | |
| document.addEventListener('keydown', escHandler); | |
| function executeExit() { | |
| if (pnlInterval) clearInterval(pnlInterval); | |
| clearInterval(matrixInterval); | |
| content.innerHTML = ''; | |
| canvas.style.opacity = '0'; | |
| appendLine("<br><br><br>"); | |
| const artLine = appendLine(asciiArt, "highlight"); | |
| artLine.style.textAlign = "center"; | |
| artLine.style.width = "100%"; | |
| setTimeout(() => { | |
| powerOff(); | |
| }, 5000); | |
| } | |
| function powerOff() { | |
| if (window.terminalHum) { | |
| window.terminalHum.gain.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + 0.5); | |
| setTimeout(() => window.terminalHum.osc.stop(), 500); | |
| } | |
| playTone(800, 'sawtooth', 0.6, 0.2); | |
| overlay.classList.add('crt-poweroff'); | |
| setTimeout(() => { | |
| overlay.remove(); | |
| window.safeSessionSet('terminal_unlocked', 'true'); | |
| const logoTexts = document.querySelectorAll('.logo-text'); | |
| logoTexts.forEach(el => { | |
| if (!el.parentNode.querySelector('.crt-badge')) { | |
| const badge = document.createElement('span'); | |
| badge.className = 'crt-badge'; | |
| badge.textContent = '🎮'; | |
| el.parentNode.appendChild(badge); | |
| } | |
| }); | |
| }, 600); | |
| } | |
| } | |
| })(); | |
| /* ========================================================================== | |
| EASTER EGG SYSTEM (PHASES 1-4) | |
| ========================================================================== */ | |
| const MASTER_KEY = "7F4B29A1E8C6"; | |
| const FRAGMENTS = { | |
| 'terminal': { char: '7', pos: 1 }, | |
| 'secret_message': { char: 'F', pos: 2 }, | |
| 'shiller_pe': { char: '4', pos: 3 }, | |
| 'black_swan': { char: 'B', pos: 4 }, | |
| 'random_walk': { char: '2', pos: 5 }, | |
| 'fibonacci': { char: '9', pos: 6 }, | |
| 'dow_theory': { char: 'A', pos: 7 }, | |
| 'quote_day': { char: '1', pos: 8 }, | |
| 'phantom_chart': { char: 'E', pos: 9 }, | |
| 'oracle': { char: '8', pos: 10 }, | |
| 'architect': { char: 'C', pos: 11 }, | |
| 'the_map': { char: '6', pos: 12 } | |
| }; | |