// Global variables let returnChart = null; let weightChart = null; let concentrationChart = null; let sectorData = null; let selectedTickers = new Set(); let colorMap = {}; // Store color assignments for tickers // Register the annotation plugin if it's available if (typeof Chart !== 'undefined' && Chart.annotation) { Chart.register(Chart.annotation); } // Fetch tickers grouped by sector async function fetchTickersBySector() { try { const response = await fetch('/api/tickers_by_sector'); return await response.json(); } catch (error) { console.error('Error fetching tickers:', error); return []; } } // Format date to YYYY-MM-DD function formatDate(date) { return date.toISOString().split('T')[0]; } // Set active navigation link function setActiveNavLink() { const currentPath = window.location.pathname; const navLinks = document.querySelectorAll('.nav-link'); navLinks.forEach(link => { const linkPath = link.getAttribute('href'); if (currentPath.endsWith(linkPath) || (currentPath.endsWith('/') && linkPath === '/index.html') || (currentPath.endsWith('/fullpage') && linkPath === '/index.html')) { link.classList.add('active'); } else { link.classList.remove('active'); } }); } // Populate the stock list with sectors and tickers function populateStockList(sectors) { const stockListElement = document.getElementById('stockList'); stockListElement.innerHTML = ''; // Setup select/deselect buttons document.getElementById('selectAllBtn').addEventListener('click', () => { const allCheckboxes = document.querySelectorAll('.stock-checkbox'); allCheckboxes.forEach(checkbox => { checkbox.checked = true; selectedTickers.add(checkbox.value); }); }); document.getElementById('deselectAllBtn').addEventListener('click', () => { const allCheckboxes = document.querySelectorAll('.stock-checkbox'); allCheckboxes.forEach(checkbox => { checkbox.checked = false; selectedTickers.delete(checkbox.value); }); }); // Create sector groups sectors.forEach(sector => { const sectorGroup = document.createElement('div'); sectorGroup.className = 'sector-group'; // Sector header const sectorHeader = document.createElement('div'); sectorHeader.className = 'sector-header'; const sectorNameContainer = document.createElement('div'); sectorNameContainer.className = 'sector-name'; const sectorArrow = document.createElement('span'); sectorArrow.className = 'sector-arrow'; sectorArrow.textContent = '▶'; sectorArrow.classList.add('open'); // Start with open sections const sectorNameText = document.createElement('span'); sectorNameText.textContent = sector.sector; sectorNameContainer.appendChild(sectorArrow); sectorNameContainer.appendChild(sectorNameText); const sectorToggle = document.createElement('button'); sectorToggle.className = 'sector-toggle'; sectorToggle.textContent = 'Toggle All'; sectorHeader.appendChild(sectorNameContainer); sectorHeader.appendChild(sectorToggle); sectorGroup.appendChild(sectorHeader); // Collapsible ticker grid const tickerGrid = document.createElement('div'); tickerGrid.className = 'ticker-grid'; // Create stock items in a grid sector.tickers.forEach(ticker => { const stockItem = document.createElement('div'); stockItem.className = 'stock-item'; const checkbox = document.createElement('input'); checkbox.type = 'checkbox'; checkbox.className = 'stock-checkbox'; checkbox.value = ticker; checkbox.id = `ticker-${ticker}`; checkbox.addEventListener('change', () => { if (checkbox.checked) { selectedTickers.add(ticker); } else { selectedTickers.delete(ticker); } }); const label = document.createElement('label'); label.className = 'stock-ticker'; label.textContent = ticker; label.htmlFor = `ticker-${ticker}`; stockItem.appendChild(checkbox); stockItem.appendChild(label); tickerGrid.appendChild(stockItem); }); sectorGroup.appendChild(tickerGrid); stockListElement.appendChild(sectorGroup); // Toggle functionality sectorToggle.addEventListener('click', () => { const checkboxes = tickerGrid.querySelectorAll('.stock-checkbox'); const allChecked = Array.from(checkboxes).every(cb => cb.checked); checkboxes.forEach(checkbox => { checkbox.checked = !allChecked; if (!allChecked) { selectedTickers.add(checkbox.value); } else { selectedTickers.delete(checkbox.value); } }); }); // Collapsible section sectorNameContainer.addEventListener('click', () => { sectorArrow.classList.toggle('open'); tickerGrid.style.display = sectorArrow.classList.contains('open') ? 'grid' : 'none'; }); }); } // Initialize charts function initializeCharts() { const returnsCtx = document.getElementById('returnsChart').getContext('2d'); const weightsCtx = document.getElementById('weightsChart').getContext('2d'); const concentrationCtx = document.getElementById('concentrationChart').getContext('2d'); // Configure Chart.js global defaults for dark theme Chart.defaults.color = '#b0b0b8'; Chart.defaults.scale.grid.color = 'rgba(56, 56, 64, 0.5)'; Chart.defaults.scale.grid.borderColor = 'rgba(56, 56, 64, 0.8)'; // Returns chart configuration returnChart = new Chart(returnsCtx, { type: 'line', data: { datasets: [ { label: 'OGD Portfolio', borderColor: '#3f88e2', backgroundColor: 'rgba(63, 136, 226, 0.1)', borderWidth: 1.5, pointRadius: 0, // Hide points completely tension: 0.1, data: [] }, { label: 'Equal Weight', borderColor: '#4caf50', backgroundColor: 'rgba(76, 175, 80, 0.1)', borderWidth: 1.5, pointRadius: 0, // Hide points completely tension: 0.1, data: [] }, { label: 'Random Portfolio', borderColor: '#e2b53f', backgroundColor: 'rgba(226, 181, 63, 0.1)', borderWidth: 1.5, pointRadius: 0, // Hide points completely tension: 0.1, data: [] } ] }, options: { responsive: true, maintainAspectRatio: false, interaction: { mode: 'index', intersect: false }, scales: { x: { type: 'time', time: { unit: 'month', displayFormats: { month: 'MMM yyyy' } }, title: { display: true, text: 'Date' } }, y: { title: { display: true, text: 'Cumulative Return' }, beginAtZero: false } }, plugins: { legend: { position: 'top', labels: { usePointStyle: true, padding: 15 } }, tooltip: { callbacks: { label: function(context) { const label = context.dataset.label || ''; const value = ((context.parsed.y - 1) * 100).toFixed(2); return `${label}: ${value}%`; } } } } } }); // Weights chart configuration weightChart = new Chart(weightsCtx, { type: 'line', data: { datasets: [] }, options: { responsive: true, maintainAspectRatio: false, interaction: { mode: 'index', intersect: false }, scales: { x: { type: 'time', time: { unit: 'month', displayFormats: { month: 'MMM yyyy' } }, title: { display: true, text: 'Date' } }, y: { title: { display: true, text: 'Asset Weight' }, min: 0, ticks: { callback: function(value) { return (value * 100).toFixed(0) + '%'; } } } }, plugins: { legend: { position: 'top', labels: { usePointStyle: true, padding: 15 } }, tooltip: { callbacks: { label: function(context) { const label = context.dataset.label || ''; const value = (context.parsed.y * 100).toFixed(2); return `${label}: ${value}%`; } } } }, elements: { line: { borderWidth: 1 // Default to thin lines }, point: { radius: 0, // Hide points by default hoverRadius: 3 // Show on hover } } } }); // Concentration chart configuration concentrationChart = new Chart(concentrationCtx, { type: 'line', data: { datasets: [ { label: 'Effective Number of Positions', borderColor: '#3f88e2', backgroundColor: 'rgba(63, 136, 226, 0.1)', borderWidth: 1.5, pointRadius: 0, // Hide points pointHoverRadius: 3, // Show points on hover tension: 0.1, data: [] } ] }, options: { responsive: true, maintainAspectRatio: false, interaction: { mode: 'index', intersect: false }, scales: { x: { type: 'time', time: { unit: 'month', displayFormats: { month: 'MMM yyyy' } }, title: { display: true, text: 'Date' } }, y: { title: { display: true, text: 'ENP' }, beginAtZero: false } }, plugins: { legend: { position: 'top', labels: { usePointStyle: true, padding: 15 } }, tooltip: { callbacks: { label: function(context) { const label = context.dataset.label || ''; const value = context.parsed.y.toFixed(2); return `${label}: ${value}`; } } }, annotation: { annotations: [] } }, elements: { line: { borderWidth: 1.5 // Default to thin lines }, point: { radius: 0, // Hide points by default hoverRadius: 3 // Show on hover } } } }); } // Run the optimization when the user clicks the button async function runOptimization() { // Get form values const startDate = document.getElementById('startDate').value; const endDate = document.getElementById('endDate').value; const learningRate = document.getElementById('learningRate').value; const windowSize = document.getElementById('windowSize').value; const alphaSortino = document.getElementById('alphaSortino').value; const alphaMaxDrawdown = document.getElementById('alphaMaxDrawdown').value; const alphaTurnover = document.getElementById('alphaTurnover').value; const alphaConcentration = document.getElementById('alphaConcentration').value; const enpMin = document.getElementById('enpMin').value; const enpMax = document.getElementById('enpMax').value; // Convert selected tickers to a comma-separated string const tickers = Array.from(selectedTickers).join(','); // Show loading overlay const loadingOverlay = document.getElementById('loadingOverlay'); loadingOverlay.style.display = 'flex'; // Backup the current state of the page for rollback if needed const statsRow = document.querySelector('.stats-row'); const originalStatsContent = statsRow ? statsRow.innerHTML : ''; // Create request data const requestData = { start_date: startDate, end_date: endDate, learning_rate: parseFloat(learningRate), window_size: parseInt(windowSize), alpha_sortino: parseFloat(alphaSortino), alpha_max_drawdown: parseFloat(alphaMaxDrawdown), alpha_turnover: parseFloat(alphaTurnover), alpha_concentration: parseFloat(alphaConcentration), enp_min: parseFloat(enpMin), enp_max: parseFloat(enpMax), tickers: tickers }; try { // Send the request to the server const response = await fetch('/api/run_optimization', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(requestData) }); // Check if the response is ok (status code 200-299) if (!response.ok) { throw new Error(`HTTP error! Status: ${response.status}`); } // Parse the response const data = await response.json(); // Hide loading overlay loadingOverlay.style.display = 'none'; // Check if there was an error if (data.error) { alert(`Optimization failed: ${data.error}`); console.error('Optimization error:', data.error); return; } // Update the charts with the new data updateCharts(data.cumulative_returns, data.weights, data.concentration, enpMin, enpMax); // Update the metrics grid updateMetrics(data.metrics); } catch (error) { console.error('Error during optimization:', error); // Hide loading overlay loadingOverlay.style.display = 'none'; // Restore original stats content if there was an error if (statsRow && originalStatsContent) { statsRow.innerHTML = originalStatsContent; } // Show error message alert(`Optimization failed: ${error.message}. Check the console for more details.`); // Run fake simulation for demo purposes if (confirm('Would you like to see a demo simulation instead?')) { runFakeSimulation(); } } } // Update charts with data from API response function updateCharts(returnsData, weightsData, concentrationData, enpMin, enpMax) { if (!returnsData || !returnChart) { console.error('Returns data or chart not available'); return; } // Filter out any data points with null values const ogdData = returnsData.ogd.filter(d => d.value !== null); const equalWeightData = returnsData.equal_weight.filter(d => d.value !== null); const randomData = returnsData.random.filter(d => d.value !== null); // Update returns chart returnChart.data.datasets[0].data = ogdData.map(d => ({ x: d.date, y: d.value })); returnChart.data.datasets[1].data = equalWeightData.map(d => ({ x: d.date, y: d.value })); returnChart.data.datasets[2].data = randomData.map(d => ({ x: d.date, y: d.value })); returnChart.update(); // Update weights chart if data is available if (weightsData && weightsData.length > 0 && weightChart) { // Get unique tickers from all dates const allTickers = new Set(); weightsData.forEach(day => { Object.keys(day.weights).forEach(ticker => allTickers.add(ticker)); }); // Create datasets for each ticker const datasets = Array.from(allTickers).map(ticker => { // Get a color for this ticker const color = getColor(ticker); // Create data points for each date const data = weightsData.map(day => ({ x: day.date, y: day.weights[ticker] || 0 })); // Filter out any null values const validData = data.filter(d => d.y !== null); return { label: ticker, data: validData, backgroundColor: color.replace('0.7', '0.2'), // Make fill more transparent borderColor: color, fill: true, tension: 0.1, borderWidth: 1, // Make line thinner pointRadius: 0, // Hide points completely pointHoverRadius: 3 // Show points on hover }; }); // Update the chart weightChart.data.datasets = datasets; weightChart.update(); } // Update concentration chart if data is available if (concentrationData && concentrationData.enp && concentrationData.enp.length > 0 && concentrationChart) { // Filter out any data points with null values const enpData = concentrationData.enp.filter(d => d.value !== null); // Update the chart concentrationChart.data.datasets[0].data = enpData.map(d => ({ x: d.date, y: d.value })); concentrationChart.data.datasets[0].borderWidth = 1.5; // Make line thinner concentrationChart.data.datasets[0].pointRadius = 0; // Hide points completely concentrationChart.data.datasets[0].pointHoverRadius = 3; // Show points on hover // Add min and max ENP target lines if (concentrationChart.options.plugins.annotation.annotations.length === 0) { concentrationChart.options.plugins.annotation.annotations.push({ type: 'line', borderColor: 'rgba(244, 67, 54, 0.5)', borderWidth: 1, borderDash: [6, 6], label: { enabled: true, content: 'Min Target', position: 'start' }, scaleID: 'y', value: enpMin || 5.0 }); concentrationChart.options.plugins.annotation.annotations.push({ type: 'line', borderColor: 'rgba(76, 175, 80, 0.5)', borderWidth: 1, borderDash: [6, 6], label: { enabled: true, content: 'Max Target', position: 'start' }, scaleID: 'y', value: enpMax || 20.0 }); } else { // Update existing annotation values concentrationChart.options.plugins.annotation.annotations[0].value = enpMin || 5.0; concentrationChart.options.plugins.annotation.annotations[1].value = enpMax || 20.0; } concentrationChart.update(); } } // Update performance metrics for all three portfolios function updateMetrics(metrics) { // Format values for display const formatValue = (value, format = 'decimal') => { if (value === undefined || value === null) return '-'; if (format === 'percent') { // For cumulative returns, we need to subtract 1 first to get the percent change if (format === 'percent' && value > 1) { // This is a cumulative return (started at 1) return ((value - 1) * 100).toFixed(2) + '%'; } return (value * 100).toFixed(2) + '%'; } else if (format === 'decimal') { return value.toFixed(2); } return value; }; // Make sure metrics objects exist to avoid errors if (!metrics) { console.error('No metrics data provided to updateMetrics'); metrics = { ogd: { sharpe: 0, max_drawdown: 0, cumulative_return: 1, capm_alpha: 0, ff3_alpha: 0 }, equal_weight: { sharpe: 0, max_drawdown: 0, cumulative_return: 1, capm_alpha: 0, ff3_alpha: 0 }, random: { sharpe: 0, max_drawdown: 0, cumulative_return: 1, capm_alpha: 0, ff3_alpha: 0 } }; } // Initialize metric objects if missing if (!metrics.ogd) metrics.ogd = {}; if (!metrics.equal_weight) metrics.equal_weight = {}; if (!metrics.random) metrics.random = {}; const statsRow = document.querySelector('.stats-row'); if (!statsRow) { console.error('Stats row element not found'); return; } // Clear the grid statsRow.innerHTML = ''; // Create strategy titles (column headers) const strategies = [ { id: 'ogd', name: 'OGD Portfolio', class: 'ogd-strategy' }, { id: 'equal_weight', name: 'Equal Weight', class: 'equal-weight-strategy' }, { id: 'random', name: 'Random Portfolio', class: 'random-strategy' } ]; // Define metrics to display (row headers) const metricTypes = [ { id: 'sharpe', name: 'Sharpe Ratio', format: 'decimal' }, { id: 'max_drawdown', name: 'Max Drawdown', format: 'percent' }, { id: 'cumulative_return', name: 'Return', format: 'percent' }, { id: 'capm_alpha', name: 'CAPM Alpha', format: 'percent' }, { id: 'ff3_alpha', name: 'FF3 Alpha', format: 'percent' } ]; // Add column headers (strategy names) // Empty cell for top-left corner const emptyHeader = document.createElement('div'); emptyHeader.className = 'stat-card metric-label strategy-header'; emptyHeader.innerHTML = '