Download assets/static/script.js from droov/opt: direct link, hf CLI and curl.
- Browser
- Download file 31.6 kB
-
https://huggingface.co/spaces/droov/opt/resolve/main/assets/static/script.js
- Command line
-
hf download hf://spaces/droov/opt/assets/static/script.js
-
curl -L -o script.js https://huggingface.co/spaces/droov/opt/resolve/main/assets/static/script.js
31.6 kB
| // 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 = '<div class="stat-value">Metrics</div>'; | |
| statsRow.appendChild(emptyHeader); | |
| // Create strategy headers | |
| strategies.forEach(strategy => { | |
| const strategyHeader = document.createElement('div'); | |
| strategyHeader.className = `stat-card strategy-header ${strategy.class}`; | |
| strategyHeader.innerHTML = `<div class="stat-value">${strategy.name}</div>`; | |
| statsRow.appendChild(strategyHeader); | |
| }); | |
| // Add metric rows | |
| metricTypes.forEach(metric => { | |
| // Add row label | |
| const metricLabel = document.createElement('div'); | |
| metricLabel.className = 'stat-card metric-label'; | |
| metricLabel.innerHTML = `<div class="stat-value">${metric.name}</div>`; | |
| statsRow.appendChild(metricLabel); | |
| // Add values for each strategy | |
| strategies.forEach(strategy => { | |
| // Get the value safely | |
| const value = metrics[strategy.id][metric.id]; | |
| let formattedValue = '-'; | |
| // Only format the value if it's not null or undefined | |
| if (value !== null && value !== undefined) { | |
| // Handle cumulative return differently | |
| if (metric.id === 'cumulative_return') { | |
| formattedValue = ((value - 1) * 100).toFixed(2) + '%'; | |
| } else if (metric.format === 'percent') { | |
| formattedValue = (value * 100).toFixed(2) + '%'; | |
| } else { | |
| formattedValue = formatValue(value, metric.format); | |
| } | |
| } | |
| const metricCell = document.createElement('div'); | |
| metricCell.className = `stat-card metric-row ${strategy.class}`; | |
| metricCell.setAttribute('data-metric', metric.name); | |
| metricCell.innerHTML = `<div class="stat-value">${formattedValue}</div>`; | |
| statsRow.appendChild(metricCell); | |
| }); | |
| }); | |
| // Log grid creation | |
| console.log('Metrics grid created with', strategies.length * metricTypes.length, 'cells'); | |
| // Update legacy hidden elements | |
| try { | |
| // Legacy element ID updates for backward compatibility | |
| const ogdSharpe = document.getElementById('ogdSharpeRatio'); | |
| if (ogdSharpe) ogdSharpe.textContent = formatValue(metrics.ogd.sharpe); | |
| const ogdDrawdown = document.getElementById('ogdMaxDrawdown'); | |
| if (ogdDrawdown) ogdDrawdown.textContent = formatValue(metrics.ogd.max_drawdown, 'percent'); | |
| const ogdReturn = document.getElementById('ogdReturn'); | |
| if (ogdReturn && metrics.ogd.cumulative_return !== null) { | |
| ogdReturn.textContent = ((metrics.ogd.cumulative_return - 1) * 100).toFixed(2) + '%'; | |
| } else if (ogdReturn) { | |
| ogdReturn.textContent = '-'; | |
| } | |
| const ewSharpe = document.getElementById('ewSharpeRatio'); | |
| if (ewSharpe) ewSharpe.textContent = formatValue(metrics.equal_weight.sharpe); | |
| const ewDrawdown = document.getElementById('ewMaxDrawdown'); | |
| if (ewDrawdown) ewDrawdown.textContent = formatValue(metrics.equal_weight.max_drawdown, 'percent'); | |
| const ewReturn = document.getElementById('ewReturn'); | |
| if (ewReturn && metrics.equal_weight.cumulative_return !== null) { | |
| ewReturn.textContent = ((metrics.equal_weight.cumulative_return - 1) * 100).toFixed(2) + '%'; | |
| } else if (ewReturn) { | |
| ewReturn.textContent = '-'; | |
| } | |
| const randomSharpe = document.getElementById('randomSharpeRatio'); | |
| if (randomSharpe) randomSharpe.textContent = formatValue(metrics.random.sharpe); | |
| const randomDrawdown = document.getElementById('randomMaxDrawdown'); | |
| if (randomDrawdown) randomDrawdown.textContent = formatValue(metrics.random.max_drawdown, 'percent'); | |
| const randomReturn = document.getElementById('randomReturn'); | |
| if (randomReturn && metrics.random.cumulative_return !== null) { | |
| randomReturn.textContent = ((metrics.random.cumulative_return - 1) * 100).toFixed(2) + '%'; | |
| } else if (randomReturn) { | |
| randomReturn.textContent = '-'; | |
| } | |
| // Update factor alphas if available | |
| if (metrics.ogd.capm_alpha !== undefined) { | |
| const ogdCAPM = document.getElementById('ogdCAPMAlpha'); | |
| if (ogdCAPM) ogdCAPM.textContent = formatValue(metrics.ogd.capm_alpha, 'percent'); | |
| const ogdFF3 = document.getElementById('ogdFF3Alpha'); | |
| if (ogdFF3) ogdFF3.textContent = formatValue(metrics.ogd.ff3_alpha, 'percent'); | |
| const ewCAPM = document.getElementById('ewCAPMAlpha'); | |
| if (ewCAPM) ewCAPM.textContent = formatValue(metrics.equal_weight.capm_alpha, 'percent'); | |
| const ewFF3 = document.getElementById('ewFF3Alpha'); | |
| if (ewFF3) ewFF3.textContent = formatValue(metrics.equal_weight.ff3_alpha, 'percent'); | |
| const randomCAPM = document.getElementById('randomCAPMAlpha'); | |
| if (randomCAPM) randomCAPM.textContent = formatValue(metrics.random.capm_alpha, 'percent'); | |
| const randomFF3 = document.getElementById('randomFF3Alpha'); | |
| if (randomFF3) randomFF3.textContent = formatValue(metrics.random.ff3_alpha, 'percent'); | |
| } | |
| } catch (e) { | |
| console.warn('Error updating legacy elements:', e); | |
| // Continue execution even if legacy element update fails | |
| } | |
| } | |
| // Create a fake simulation for visual feedback during loading | |
| function runFakeSimulation() { | |
| const loadingOverlay = document.getElementById('loadingOverlay'); | |
| if (loadingOverlay.style.display === 'none') return; | |
| // Get ENP min/max values | |
| const enpMin = parseFloat(document.getElementById('enpMin').value) || 5.0; | |
| const enpMax = parseFloat(document.getElementById('enpMax').value) || 20.0; | |
| // Generate fake return data | |
| const dates = []; | |
| const today = new Date(); | |
| for (let i = 365; i >= 0; i--) { | |
| const date = new Date(today); | |
| date.setDate(today.getDate() - i); | |
| dates.push(date.toISOString().split('T')[0]); | |
| } | |
| // Generate fake returns | |
| const generateFakeData = (volatility, bias = 0) => { | |
| let value = 1.0; | |
| return dates.map(date => { | |
| value *= (1 + (Math.random() - 0.45 + bias) * volatility); | |
| return { date, value }; | |
| }); | |
| }; | |
| const fakeReturns = { | |
| ogd: generateFakeData(0.015, 0.02), | |
| equal_weight: generateFakeData(0.01, 0.01), | |
| random: generateFakeData(0.02, 0) | |
| }; | |
| // Generate fake weights | |
| const fakeWeights = []; | |
| const fakeTickers = ['AAPL', 'MSFT', 'GOOGL', 'AMZN', 'META', 'TSLA', 'NVDA', 'JPM', 'JNJ', 'V']; | |
| dates.forEach(date => { | |
| const weights = {}; | |
| let totalWeight = 0; | |
| fakeTickers.forEach(ticker => { | |
| // Random weight but normalized later | |
| weights[ticker] = Math.random(); | |
| totalWeight += weights[ticker]; | |
| }); | |
| // Normalize weights to sum to 1 | |
| Object.keys(weights).forEach(ticker => { | |
| weights[ticker] = weights[ticker] / totalWeight; | |
| }); | |
| fakeWeights.push({ date, weights }); | |
| }); | |
| // Generate fake concentration data | |
| const fakeConcentration = { | |
| enp: dates.map(date => { | |
| // Random ENP value that fluctuates between min and max with some noise | |
| const range = enpMax - enpMin; | |
| const center = enpMin + range / 2; | |
| const variation = range / 3; | |
| const value = center + (Math.random() * 2 - 1) * variation; | |
| return { date, value }; | |
| }), | |
| hhi: dates.map(date => { | |
| return { date, value: Math.random() * 0.3 + 0.1 }; | |
| }) | |
| }; | |
| // Update charts with fake data | |
| updateCharts(fakeReturns, fakeWeights, fakeConcentration, enpMin, enpMax); | |
| // Update metrics with fake data | |
| updateMetrics({ | |
| ogd: { | |
| sharpe: 1.2 + Math.random() * 0.5, | |
| max_drawdown: 0.12 + Math.random() * 0.1, | |
| cumulative_return: 1.3 + Math.random() * 0.5, | |
| capm_alpha: 2.5 + Math.random() * 1.0, // Annual % | |
| ff3_alpha: 1.8 + Math.random() * 0.8 // Annual % | |
| }, | |
| equal_weight: { | |
| sharpe: 0.9 + Math.random() * 0.3, | |
| max_drawdown: 0.15 + Math.random() * 0.1, | |
| cumulative_return: 1.2 + Math.random() * 0.3, | |
| capm_alpha: 1.2 + Math.random() * 0.5, | |
| ff3_alpha: 0.8 + Math.random() * 0.4 | |
| }, | |
| random: { | |
| sharpe: 0.6 + Math.random() * 0.3, | |
| max_drawdown: 0.2 + Math.random() * 0.15, | |
| cumulative_return: 1.1 + Math.random() * 0.2, | |
| capm_alpha: 0.4 + Math.random() * 0.6, | |
| ff3_alpha: 0.2 + Math.random() * 0.4 | |
| } | |
| }); | |
| } | |
| // Generate a color from a ticker symbol | |
| function getColor(ticker) { | |
| // Use cached color if already assigned | |
| if (colorMap[ticker]) { | |
| return colorMap[ticker]; | |
| } | |
| // Predefined colors for common tickers | |
| const predefinedColors = { | |
| 'AAPL': 'rgba(63, 136, 226, 0.7)', // Blue | |
| 'MSFT': 'rgba(76, 175, 80, 0.7)', // Green | |
| 'GOOGL': 'rgba(226, 181, 63, 0.7)', // Yellow | |
| 'AMZN': 'rgba(226, 77, 63, 0.7)', // Red | |
| 'META': 'rgba(156, 39, 176, 0.7)', // Purple | |
| 'TSLA': 'rgba(0, 188, 212, 0.7)', // Cyan | |
| 'NVDA': 'rgba(255, 235, 59, 0.7)', // Bright Yellow | |
| 'JPM': 'rgba(121, 85, 72, 0.7)', // Brown | |
| 'V': 'rgba(96, 125, 139, 0.7)', // Blue Grey | |
| 'JNJ': 'rgba(233, 30, 99, 0.7)' // Pink | |
| }; | |
| if (predefinedColors[ticker]) { | |
| colorMap[ticker] = predefinedColors[ticker]; | |
| return colorMap[ticker]; | |
| } | |
| // Generate a pseudo-random color based on the ticker string | |
| let hash = 0; | |
| for (let i = 0; i < ticker.length; i++) { | |
| hash = ticker.charCodeAt(i) + ((hash << 5) - hash); | |
| } | |
| // Convert to RGB color with proper range to ensure readability | |
| const r = (hash & 0xFF) % 200 + 55; // Range: 55-255 (avoid too dark) | |
| const g = ((hash >> 8) & 0xFF) % 200 + 55; | |
| const b = ((hash >> 16) & 0xFF) % 200 + 55; | |
| // Convert to rgba color string with 70% opacity for better visibility | |
| const color = `rgba(${r}, ${g}, ${b}, 0.7)`; | |
| // Cache the color | |
| colorMap[ticker] = color; | |
| return color; | |
| } | |
| // Initialize the application | |
| async function initialize() { | |
| try { | |
| // Set active navigation link | |
| setActiveNavLink(); | |
| // Initialize charts | |
| initializeCharts(); | |
| // Set default dates to match the data range | |
| document.getElementById('startDate').value = '2007-01-01'; // Jan 1, 2007 | |
| document.getElementById('endDate').value = '2025-04-01'; // Apr 1, 2025 | |
| // Fetch tickers and populate stock list | |
| sectorData = await fetchTickersBySector(); | |
| populateStockList(sectorData); | |
| // Add event listener to run button | |
| document.getElementById('runButton').addEventListener('click', runOptimization); | |
| // Create initial metrics grid | |
| updateMetrics({ | |
| ogd: { | |
| sharpe: 1.2, | |
| max_drawdown: 0.12, | |
| cumulative_return: 1.3, | |
| capm_alpha: 2.5, | |
| ff3_alpha: 1.8 | |
| }, | |
| equal_weight: { | |
| sharpe: 0.9, | |
| max_drawdown: 0.15, | |
| cumulative_return: 1.2, | |
| capm_alpha: 1.2, | |
| ff3_alpha: 0.8 | |
| }, | |
| random: { | |
| sharpe: 0.6, | |
| max_drawdown: 0.2, | |
| cumulative_return: 1.1, | |
| capm_alpha: 0.4, | |
| ff3_alpha: 0.2 | |
| } | |
| }); | |
| // Run fake simulation for initial visual | |
| runFakeSimulation(); | |
| } catch (error) { | |
| console.error('Error initializing application:', error); | |
| } | |
| } | |
| // Run the initialization when the document is loaded | |
| document.addEventListener('DOMContentLoaded', initialize); |