// Safe Choices - Simulation Controller // ===== STATE ===== let currentSimType = 'single'; let currentView = 'simulation'; let simulationResults = null; let charts = {}; // ===== INITIALIZATION ===== document.addEventListener('DOMContentLoaded', () => { initializeApp(); }); function initializeApp() { setupEventListeners(); updateEstimatedTime(); initializeEmptyCharts(); } function setupEventListeners() { // Number input constraints const numSimulations = document.getElementById('numSimulations'); numSimulations.addEventListener('input', () => { numSimulations.value = Math.min(Math.max(parseInt(numSimulations.value) || 10, 10), 100); updateEstimatedTime(); }); const numFunds = document.getElementById('numFunds'); numFunds.addEventListener('input', () => { numFunds.value = Math.min(Math.max(parseInt(numFunds.value) || 2, 2), 10); updateEstimatedTime(); }); // Target return custom input toggle const targetReturn = document.getElementById('targetReturn'); const customTarget = document.getElementById('customTarget'); targetReturn.addEventListener('change', () => { if (targetReturn.value === 'custom') { customTarget.classList.add('visible'); customTarget.value = '15'; } else { customTarget.classList.remove('visible'); } }); // Validate inputs on change document.querySelectorAll('.param-input').forEach(input => { input.addEventListener('input', validateInputs); }); } // ===== VIEW SWITCHING ===== function selectView(view) { currentView = view; // Update toggle buttons document.querySelectorAll('.toggle-btn').forEach(btn => { btn.classList.toggle('active', btn.dataset.view === view); }); // Show/hide content const simContent = document.getElementById('simulationContent'); const methContent = document.getElementById('methodologyContent'); if (view === 'simulation') { simContent.style.display = 'flex'; methContent.style.display = 'none'; document.querySelectorAll('.simulation-view-only').forEach(el => { el.classList.remove('hidden'); }); } else { simContent.style.display = 'none'; methContent.style.display = 'block'; document.querySelectorAll('.simulation-view-only').forEach(el => { el.classList.add('hidden'); }); } } // ===== SIMULATION TYPE SWITCHING ===== function selectSimulation(type) { currentSimType = type; // Update strategy cards document.querySelectorAll('.strategy-card').forEach(card => { card.classList.toggle('active', card.dataset.sim === type); }); // Show/hide conditional parameters document.querySelectorAll('.threshold-only').forEach(el => { el.classList.toggle('visible', type === 'threshold'); }); document.querySelectorAll('.multi-only').forEach(el => { el.classList.toggle('visible', type === 'multi'); }); document.querySelectorAll('.kelly-only').forEach(el => { el.classList.toggle('visible', type === 'kelly'); }); updateEstimatedTime(); } // ===== VALIDATION ===== function validateInputs() { const errors = []; const minProb7d = parseFloat(document.getElementById('minProb7d').value); const minProbCurrent = parseFloat(document.getElementById('minProbCurrent').value); if (minProb7d < 50 || minProb7d > 99) { errors.push('7-day probability must be between 50% and 99%'); } if (minProbCurrent < 50 || minProbCurrent > 99) { errors.push('Current probability must be between 50% and 99%'); } const runBtn = document.getElementById('runBtn'); runBtn.disabled = errors.length > 0; return errors.length === 0; } // ===== TIME ESTIMATION ===== function updateEstimatedTime() { const numSims = parseInt(document.getElementById('numSimulations').value) || 100; const numFunds = currentSimType === 'multi' ? (parseInt(document.getElementById('numFunds').value) || 5) : 1; let baseTime = numSims * 0.05; if (currentSimType === 'multi') { baseTime *= Math.sqrt(numFunds); } const seconds = Math.max(2, Math.ceil(baseTime)); const timeText = seconds < 60 ? `~${seconds}s` : `~${Math.ceil(seconds / 60)}min`; document.getElementById('estimatedTime').textContent = timeText; } // ===== SIMULATION EXECUTION ===== async function runSimulation() { if (!validateInputs()) return; const params = getSimulationParameters(); showProgress(); disableControls(); try { const results = await callSimulationAPI(params); simulationResults = results; displayResults(results); } catch (error) { console.error('Simulation error:', error); alert('Simulation failed: ' + (error.message || 'Unknown error')); } finally { hideProgress(); enableControls(); } } function getSimulationParameters() { const params = { simType: currentSimType, startingCapital: parseFloat(document.getElementById('startingCapital').value), numSimulations: parseInt(document.getElementById('numSimulations').value), startDate: document.getElementById('startDate').value, maxDuration: parseInt(document.getElementById('maxDuration').value), minProb7d: parseFloat(document.getElementById('minProb7d').value) / 100, minProbCurrent: parseFloat(document.getElementById('minProbCurrent').value) / 100, daysBefore: parseInt(document.getElementById('daysBefore').value), investmentProbability: parseFloat(document.getElementById('investmentProbability').value), minVolume: parseFloat(document.getElementById('minVolume').value) }; if (currentSimType === 'threshold') { const targetSelect = document.getElementById('targetReturn').value; if (targetSelect === 'custom') { params.targetReturn = parseFloat(document.getElementById('customTarget').value) / 100; } else { params.targetReturn = parseFloat(targetSelect) / 100; } } if (currentSimType === 'multi') { params.numFunds = parseInt(document.getElementById('numFunds').value); } if (currentSimType === 'kelly') { params.kellyFraction = parseFloat(document.getElementById('kellyFraction').value); params.edgeEstimate = document.getElementById('edgeEstimate').value; } return params; } async function callSimulationAPI(params) { const progressFill = document.getElementById('progressFill'); const progressPercent = document.getElementById('progressPercent'); const progressText = document.getElementById('progressText'); progressText.textContent = 'Connecting to server...'; progressFill.style.width = '10%'; progressPercent.textContent = '10%'; const requestBody = { simType: params.simType, startingCapital: params.startingCapital, numSimulations: params.numSimulations, startDate: params.startDate, maxDuration: params.maxDuration, minProb7d: params.minProb7d, minProbCurrent: params.minProbCurrent, daysBefore: params.daysBefore, investmentProbability: params.investmentProbability, minVolume: params.minVolume }; if (params.targetReturn !== undefined) { requestBody.targetReturn = params.targetReturn; } if (params.numFunds !== undefined) { requestBody.numFunds = params.numFunds; } progressText.textContent = 'Running simulation...'; progressFill.style.width = '30%'; progressPercent.textContent = '30%'; const response = await fetch('/api/simulate', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(requestBody) }); if (!response.ok) { const errorData = await response.json().catch(() => ({ detail: response.statusText })); throw new Error(errorData.detail || `HTTP ${response.status}`); } progressText.textContent = 'Processing results...'; progressFill.style.width = '80%'; progressPercent.textContent = '80%'; const results = await response.json(); progressFill.style.width = '100%'; progressPercent.textContent = '100%'; progressText.textContent = 'Complete!'; return results; } // ===== RESULTS DISPLAY ===== function displayResults(results) { const { summary, parameters, runs } = results; // Show results section const resultsSection = document.getElementById('resultsSection'); resultsSection.classList.add('visible'); resultsSection.scrollIntoView({ behavior: 'smooth', block: 'start' }); // Calculate average return of survivors const survivors = runs.filter(r => !r.wentBust); const avgReturnSurvivors = survivors.length > 0 ? survivors.reduce((sum, r) => sum + r.totalReturn, 0) / survivors.length : null; // Primary metrics updateMetric('avgReturn', summary.avgReturn, true); updateMetric('avgReturnSurvivors', avgReturnSurvivors, true); updateMetric('successRate', summary.positiveReturnRate); updateMetric('bustRate', summary.bustRate, false, true); // Risk metrics document.getElementById('volatility').textContent = formatPercentage(summary.returnVolatility); document.getElementById('maxDrawdown').textContent = formatPercentage(summary.maxDrawdown); document.getElementById('percentile5').textContent = formatPercentage(summary.return5th || -1); document.getElementById('percentile95').textContent = formatPercentage(summary.return95th || 0); // Type-specific stats if (parameters.simType === 'multi') { document.querySelectorAll('.multi-stats, .multi-chart').forEach(el => { el.classList.add('visible'); }); document.getElementById('avgSurvivingFunds').textContent = `${summary.avgSurvivingFunds?.toFixed(1) || '--'} / ${parameters.numFunds}`; document.getElementById('survivorshipRate').textContent = formatPercentage(summary.survivorshipRate || 0); document.getElementById('diversificationBenefit').textContent = summary.returnVolatility < 0.3 ? 'Positive' : 'Limited'; } else { document.querySelectorAll('.multi-stats, .multi-chart').forEach(el => { el.classList.remove('visible'); }); } if (parameters.simType === 'threshold') { document.querySelectorAll('.threshold-stats').forEach(el => { el.classList.add('visible'); }); document.getElementById('targetReached').textContent = formatPercentage(summary.targetReachedRate || 0); document.getElementById('avgTimeToTarget').textContent = summary.avgTimeToTarget ? `${Math.round(summary.avgTimeToTarget)} days` : 'N/A'; document.getElementById('vsNeverStop').textContent = (summary.targetReachedRate || 0) > 0.5 ? 'Better' : 'Similar'; } else { document.querySelectorAll('.threshold-stats').forEach(el => { el.classList.remove('visible'); }); } if (parameters.simType === 'kelly') { document.querySelectorAll('.kelly-stats').forEach(el => { el.classList.add('visible'); }); document.getElementById('avgBetSize').textContent = formatPercentage(summary.avgBetSize || 0); document.getElementById('avgEdge').textContent = formatPercentage(summary.avgEdge || 0); document.getElementById('betsSkipped').textContent = summary.betsSkipped !== undefined ? `${summary.betsSkipped.toFixed(0)}` : '--'; } else { document.querySelectorAll('.kelly-stats').forEach(el => { el.classList.remove('visible'); }); } // Generate charts generateCharts(results); } function updateMetric(id, value, showSign = false, isRisk = false) { const el = document.getElementById(id); el.textContent = formatPercentage(value); // Add color classes el.classList.remove('positive', 'negative', 'high-risk'); if (isRisk) { if (value > 0.1) el.classList.add('high-risk'); } else if (showSign) { if (value > 0.02) el.classList.add('positive'); else if (value < -0.02) el.classList.add('negative'); } } // ===== CHARTS ===== function initializeEmptyCharts() { charts.return = createEmptyChart('returnChart', 'bar'); charts.capital = createEmptyChart('capitalChart', 'line'); charts.survivorship = createEmptyChart('survivorshipChart', 'bar'); } function createEmptyChart(canvasId, type) { const ctx = document.getElementById(canvasId).getContext('2d'); return new Chart(ctx, { type: type, data: { labels: [], datasets: [] }, options: getChartOptions(type) }); } function generateCharts(results) { const { runs, parameters } = results; // Destroy existing charts Object.values(charts).forEach(chart => chart?.destroy()); charts = {}; // Return distribution charts.return = createReturnDistributionChart(runs); // Capital evolution charts.capital = createCapitalEvolutionChart(runs); // Survivorship (multi-fund only) if (parameters.simType === 'multi') { charts.survivorship = createSurvivorshipChart(runs, parameters.numFunds); } else { charts.survivorship = createEmptyChart('survivorshipChart', 'bar'); } } function createReturnDistributionChart(runs) { const ctx = document.getElementById('returnChart').getContext('2d'); const returns = runs.map(r => r.totalReturn * 100); const minReturn = Math.min(...returns); const maxReturn = Math.max(...returns); const binStart = Math.floor(minReturn / 10) * 10; const binEnd = Math.min(Math.ceil(maxReturn / 10) * 10, 200); const bins = []; for (let i = binStart; i <= binEnd; i += 5) bins.push(i); const binCounts = new Array(bins.length).fill(0); returns.forEach(ret => { for (let i = 0; i < bins.length - 1; i++) { if (ret >= bins[i] && ret < bins[i + 1]) { binCounts[i]++; break; } } if (ret >= bins[bins.length - 1]) binCounts[bins.length - 1]++; }); return new Chart(ctx, { type: 'bar', data: { labels: bins.map(b => `${b}%`), datasets: [{ label: 'Frequency', data: binCounts, backgroundColor: 'rgba(59, 130, 246, 0.6)', borderColor: 'rgba(59, 130, 246, 1)', borderWidth: 1, borderRadius: 4 }] }, options: getChartOptions('bar') }); } function createCapitalEvolutionChart(runs) { const ctx = document.getElementById('capitalChart').getContext('2d'); const colors = [ { border: '#3b82f6', bg: 'rgba(59, 130, 246, 0.1)' }, { border: '#10b981', bg: 'rgba(16, 185, 129, 0.1)' }, { border: '#f59e0b', bg: 'rgba(245, 158, 11, 0.1)' }, { border: '#ef4444', bg: 'rgba(239, 68, 68, 0.1)' }, { border: '#8b5cf6', bg: 'rgba(139, 92, 246, 0.1)' } ]; const sampleRuns = runs.slice(0, 5); const datasets = sampleRuns.map((run, i) => { let data = []; if (run.capitalHistory?.length > 0) { data = run.capitalHistory.map((item, idx) => ({ x: typeof item === 'object' ? item.day : idx, y: typeof item === 'object' ? item.capital : item })); } else { data = [{ x: 0, y: 10000 }, { x: 1, y: run.finalCapital }]; } return { label: `Run ${i + 1}`, data: data, borderColor: colors[i].border, backgroundColor: colors[i].bg, borderWidth: 2, fill: false, tension: 0.3, pointRadius: 0 }; }); return new Chart(ctx, { type: 'line', data: { datasets }, options: getChartOptions('line') }); } function createSurvivorshipChart(runs, numFunds) { const ctx = document.getElementById('survivorshipChart').getContext('2d'); const survivingCounts = runs.map(r => r.survivingFunds); const labels = []; const data = []; for (let i = 0; i <= numFunds; i++) { labels.push(i.toString()); data.push(survivingCounts.filter(c => c === i).length); } return new Chart(ctx, { type: 'bar', data: { labels: labels, datasets: [{ label: 'Frequency', data: data, backgroundColor: 'rgba(139, 92, 246, 0.6)', borderColor: 'rgba(139, 92, 246, 1)', borderWidth: 1, borderRadius: 4 }] }, options: getChartOptions('bar') }); } function getChartOptions(type) { const baseOptions = { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: type === 'line', labels: { color: '#9ca3af', font: { size: 11 }, usePointStyle: true, padding: 16 } } }, scales: { x: { grid: { color: 'rgba(255, 255, 255, 0.05)' }, ticks: { color: '#6b7280', font: { size: 10 } } }, y: { grid: { color: 'rgba(255, 255, 255, 0.05)' }, ticks: { color: '#6b7280', font: { size: 10 } } } } }; if (type === 'line') { baseOptions.scales.x.type = 'linear'; } return baseOptions; } // ===== EXPORT ===== function exportResults() { if (!simulationResults) return; const data = { timestamp: new Date().toISOString(), parameters: simulationResults.parameters, summary: simulationResults.summary, runs: simulationResults.runs }; const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `safe_choices_${currentSimType}_${Date.now()}.json`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } // ===== UI HELPERS ===== function showProgress() { document.getElementById('progressPanel').classList.add('visible'); document.getElementById('progressFill').style.width = '0%'; } function hideProgress() { setTimeout(() => { document.getElementById('progressPanel').classList.remove('visible'); }, 500); } function disableControls() { const runBtn = document.getElementById('runBtn'); runBtn.disabled = true; runBtn.classList.add('running'); document.querySelector('.run-text').textContent = 'Running...'; } function enableControls() { const runBtn = document.getElementById('runBtn'); runBtn.disabled = false; runBtn.classList.remove('running'); document.querySelector('.run-text').textContent = 'Run Simulation'; } function formatPercentage(value) { if (value === null || value === undefined || isNaN(value)) return '--'; return `${(value * 100).toFixed(1)}%`; } // ===== UTILITY FUNCTIONS ===== function mean(arr) { return arr.reduce((a, b) => a + b, 0) / arr.length; } function median(arr) { const sorted = [...arr].sort((a, b) => a - b); const mid = Math.floor(sorted.length / 2); return sorted.length % 2 === 0 ? (sorted[mid - 1] + sorted[mid]) / 2 : sorted[mid]; } function standardDeviation(arr) { const avg = mean(arr); return Math.sqrt(arr.reduce((sum, val) => sum + Math.pow(val - avg, 2), 0) / arr.length); } function percentile(arr, p) { const sorted = [...arr].sort((a, b) => a - b); const index = (p / 100) * (sorted.length - 1); const lower = Math.floor(index); const upper = Math.ceil(index); const weight = index % 1; return sorted[lower] * (1 - weight) + sorted[upper] * weight; }