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