dhruv575
Minimum Volume Filter
09368f6
// 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;
}