/* * Browser-side controller for the Adaptive Barrier Monitor demo (Monte Carlo showcase). * * Runs a simulation and renders each path with adaptive sampling (cyan) and * either an equal-budget or independently fixed-cadence baseline (grey). */ const state = { lastResult: null }; // ---- helpers ---- function $(id) { return document.getElementById(id); } async function fetchJson(url, options = {}) { const resp = await fetch(url, options); const text = await resp.text(); let payload; try { payload = text ? JSON.parse(text) : {}; } catch { payload = { detail: text }; } if (!resp.ok) { const detail = payload.detail ?? payload; throw new Error(typeof detail === "string" ? detail : JSON.stringify(detail, null, 2)); } return payload; } function setBusy(button, busy) { button.disabled = busy; button.textContent = busy ? "Working…" : (button.dataset.label || button.textContent); } function showWarnings(warnings) { const box = $("warning-box"); const norm = (warnings || []).filter(Boolean).map(String); if (norm.length === 0) { box.hidden = true; box.textContent = ""; return; } box.hidden = false; box.textContent = norm.join("\n"); } // ---- slider ↔ number sync ---- function bindRange(id) { const range = $(id); const num = $(id + "-val"); if (!range || !num) return; range.addEventListener("input", () => { num.value = parseFloat(range.value); }); num.addEventListener("change", () => { range.value = parseFloat(num.value); }); } function bindLogRange(id) { const range = $(id); const num = $(id + "-val"); if (!range || !num) return; range.addEventListener("input", () => { num.value = Number(range.value).toExponential(4); }); num.addEventListener("change", () => { range.value = parseFloat(num.value); }); } function updateComparisonModeCopy() { const mode = $("mc-comparison-mode").value; const fixedCadence = mode === "fixed_cadence"; $("mc-fixed-cadence-row").hidden = !fixedCadence; if (fixedCadence) { $("fixed-explanation").innerHTML = 'A fixed-cadence sampler that observes every chosen number of minutes, independently of the adaptive sample count.'; $("headline-explanation").textContent = 'The schedules use independent sample counts. Compare detections, lag, and total observations to see the quality–cost trade-off.'; $("comparison-note").innerHTML = 'The fixed monitor samples at the selected cadence. The experiment compares detection quality and observation cost.'; $("allocation-method").innerHTML = 'Allocate samples: adaptive intervals shrink near the closest barrier; the fixed monitor samples at the selected cadence.'; $("method-conclusion").textContent = 'The headline compares detections, mean lag, and total observations under independent schedules.'; } else { $("fixed-explanation").innerHTML = 'An exactly equal-budget fixed-rate sampler: each path receives the same number of observations as its adaptive counterpart, spread uniformly.'; $("headline-explanation").textContent = 'Same paths and exactly the same sample budget. The comparison reports which schedule confirms more lower and upper barrier events before the detection deadline.'; $("comparison-note").innerHTML = 'The adaptive and fixed monitors receive the same number of samples on every path. The experiment therefore compares sample placement—not computational cost.'; $("allocation-method").innerHTML = 'Allocate samples: adaptive intervals shrink near the closest barrier; the fixed monitor receives the exact same sample count, spaced uniformly.'; $("method-conclusion").textContent = 'The headline compares detections and mean lag under an equal sample budget.'; } } // ---- Plotly helpers ---- const PLOTLY_CONFIG = { displayModeBar: true, modeBarButtonsToRemove: ["lasso2d", "select2d"], displaylogo: false, responsive: true, }; const PLOTLY_LAYOUT = { font: { color: "#A9B1D6", family: "Inter, sans-serif" }, paper_bgcolor: "#1A1B26", plot_bgcolor: "#1A1B26", xaxis: { gridcolor: "rgba(120,124,153,0.15)", zerolinecolor: "rgba(120,124,153,0.3)" }, yaxis: { gridcolor: "rgba(120,124,153,0.15)", zerolinecolor: "rgba(120,124,153,0.3)" }, margin: { l: 60, r: 30, t: 20, b: 50 }, legend: { font: { color: "#A9B1D6" }, x: 0.01, y: 0.99, bgcolor: "rgba(26,27,38,0.6)" }, }; function renderPlot(data, layoutOverrides = {}) { Plotly.newPlot("chart-stage", data, { ...PLOTLY_LAYOUT, ...layoutOverrides }, PLOTLY_CONFIG); } // ---- Monte Carlo ---- async function runMonteCarlo() { const button = $("mc-run-btn"); setBusy(button, true); showWarnings([]); $("result-subtitle").textContent = "Running simulation…"; try { const thresholdPct = parseFloat($("mc-threshold").value); const risePct = parseFloat($("mc-rise-threshold").value); const dtcapRaw = $("mc-dtcap").value.trim(); const seedRaw = $("mc-seed").value.trim(); const comparisonMode = $("mc-comparison-mode").value; const payload = { S0: 100.0, sigma_annual: parseFloat($("mc-sigma").value), mu_annual: parseFloat($("mc-mu").value), window_minutes: parseFloat($("mc-window").value), n_paths: parseInt($("mc-paths").value), n_steps: parseInt($("mc-steps").value), use_jumps: $("mc-jumps").checked, jump_intensity: 25.0, jump_mean: -0.02, jump_sigma: 0.05, eps: parseFloat($("mc-eps").value), drop_fraction: thresholdPct / 100.0, rise_fraction: risePct / 100.0, dt_cap_minutes: dtcapRaw ? parseFloat(dtcapRaw) : null, rng_seed: seedRaw ? parseInt(seedRaw) : null, max_detection_lag_steps: parseInt($("mc-max-lag").value), comparison_mode: comparisonMode, fixed_cadence_minutes: parseFloat($("mc-fixed-cadence").value), }; const r = await fetchJson("/api/simulate", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }); state.lastResult = r; const colors = ["#2AC3DE", "#BB9AF7", "#9ECE6A", "#0DB9D7", "#B4F9F8", "#FF9E64", "#7DCFFF", "#C0CAF5", "#73DACA", "#F7768E"]; const t = r.times_minutes; const t0 = t[0], t1 = t[t.length - 1]; const traces = []; for (let i = 0; i < r.paths.length; i++) { const p = r.paths[i]; const c = colors[i % colors.length]; traces.push({ x: t, y: p.prices, type: "scatter", mode: "lines", line: { color: c, width: 0.8, opacity: 0.5 }, name: `Path ${i + 1}`, showlegend: i < 5, }); if (p.sample_times.length) { traces.push({ x: p.sample_times, y: p.sample_prices, type: "scatter", mode: "markers", marker: { color: "#2AC3DE", size: 6, symbol: "circle-open", opacity: 0.9 }, name: "adaptive", showlegend: i === 0, }); } if (p.fixed_sample_times.length) { traces.push({ x: p.fixed_sample_times, y: p.fixed_sample_prices, type: "scatter", mode: "markers", marker: { color: "#787C99", size: 3, opacity: 0.5 }, name: r.comparison_mode === "fixed_cadence" ? "fixed cadence" : "fixed equal-budget", showlegend: i === 0, }); } } // Lower barrier (drop) traces.push({ x: [t0, t1], y: [r.lower_barrier_price, r.lower_barrier_price], type: "scatter", mode: "lines", line: { color: "#F7768E", width: 2, dash: "dash" }, name: `−${thresholdPct}% lower`, }); // Upper barrier (rise) traces.push({ x: [t0, t1], y: [r.upper_barrier_price, r.upper_barrier_price], type: "scatter", mode: "lines", line: { color: "#9ECE6A", width: 2, dash: "dash" }, name: `+${risePct}% upper`, }); // Lower breach markers (red X) const bxDown = [], byDown = []; for (const p of r.paths) { if (p.lower_breach_idx !== null) { bxDown.push(t[p.lower_breach_idx]); byDown.push(p.prices[p.lower_breach_idx]); } } if (bxDown.length) { traces.push({ x: bxDown, y: byDown, type: "scatter", mode: "markers", marker: { color: "#F7768E", size: 13, symbol: "x", line: { width: 3 } }, name: `↓ breaches (${bxDown.length})`, }); } // Upper breach markers (green triangles) const bxUp = [], byUp = []; for (const p of r.paths) { if (p.upper_breach_idx !== null) { bxUp.push(t[p.upper_breach_idx]); byUp.push(p.prices[p.upper_breach_idx]); } } if (bxUp.length) { traces.push({ x: bxUp, y: byUp, type: "scatter", mode: "markers", marker: { color: "#9ECE6A", size: 13, symbol: "triangle-up", line: { width: 3 } }, name: `↑ breaches (${bxUp.length})`, }); } renderPlot(traces, { xaxis: { title: "Time (minutes)" }, yaxis: { title: "Price ($)" }, showlegend: r.paths.length <= 10, }); // Headline tiles — use explicit event counts and method labels. // A barrier event is the first lower-barrier or upper-barrier crossing on a path. // One path can therefore contribute up to two events. const nEvents = r.n_barrier_events ?? r.n_breaches; const nLowerEvents = r.n_lower_events ?? r.n_lower_breaches; const nUpperEvents = r.n_upper_events ?? r.n_upper_breaches; const number = new Intl.NumberFormat(); const directionSummary = (lowerCaught, upperCaught) => { const lower = nLowerEvents > 0 ? `↓ ${lowerCaught} of ${nLowerEvents} lower` : "↓ no lower events"; const upper = nUpperEvents > 0 ? `↑ ${upperCaught} of ${nUpperEvents} upper` : "↑ no upper events"; return `${lower} · ${upper}`; }; $("hl-adaptive").textContent = nEvents > 0 ? `${r.adaptive_detections} of ${nEvents}` : "No events"; $("hl-fixed").textContent = nEvents > 0 ? `${r.fixed_detections} of ${nEvents}` : "No events"; $("hl-adaptive-sub").textContent = directionSummary( r.adaptive_lower_detections, r.adaptive_upper_detections, ); $("hl-fixed-sub").textContent = directionSummary( r.fixed_lower_detections, r.fixed_upper_detections, ); const equalBudgetMode = r.comparison_mode === "equal_budget"; $("hl-samples").textContent = equalBudgetMode ? `${number.format(r.adaptive_total_samples)} each` : `A ${number.format(r.adaptive_total_samples)} · F ${number.format(r.fixed_total_samples)}`; $("hl-samples-sub").textContent = equalBudgetMode ? "same count on every path" : `adaptive · fixed every ${number.format(r.fixed_cadence_minutes)} min`; const adaptiveLag = (r.mean_detection_lag !== null && r.mean_detection_lag !== undefined) ? r.mean_detection_lag.toFixed(1) : "–"; const fixedLag = (r.mean_fixed_detection_lag !== null && r.mean_fixed_detection_lag !== undefined) ? r.mean_fixed_detection_lag.toFixed(1) : "–"; $("hl-lag").textContent = `${adaptiveLag} vs ${fixedLag}`; const breachedPaths = r.n_paths_with_any_breach ?? r.paths.filter( (path) => path.lower_breach_idx !== null || path.upper_breach_idx !== null, ).length; $("result-subtitle").textContent = `${nEvents} barrier events across ${breachedPaths} of ${r.n_paths} paths` + ` · ${nLowerEvents} lower, ${nUpperEvents} upper` + ` · deadline ≤${r.max_detection_lag_steps} grid steps` + (r.comparison_mode === "fixed_cadence" ? ` · fixed every ${r.fixed_cadence_minutes} min` : " · equal budget") + (r.use_jumps ? " · Merton jumps" : " · pure GBM"); $("stats-json").textContent = JSON.stringify({ threshold_pct: thresholdPct, rise_pct: risePct, seed: payload.rng_seed, n_paths: r.n_paths, n_paths_with_any_breach: r.n_paths_with_any_breach, n_barrier_events: r.n_barrier_events ?? r.n_breaches, n_lower_events: r.n_lower_events ?? r.n_lower_breaches, n_upper_events: r.n_upper_events ?? r.n_upper_breaches, adaptive_detections: r.adaptive_detections, adaptive_lower_detections: r.adaptive_lower_detections, adaptive_upper_detections: r.adaptive_upper_detections, fixed_detections: r.fixed_detections, fixed_lower_detections: r.fixed_lower_detections, fixed_upper_detections: r.fixed_upper_detections, adaptive_total_samples: r.adaptive_total_samples, fixed_total_samples: r.fixed_total_samples, mean_detection_lag: r.mean_detection_lag, mean_lower_detection_lag: r.mean_lower_detection_lag, mean_upper_detection_lag: r.mean_upper_detection_lag, sigma_annual: r.sigma_annual, mu_annual: r.mu_annual, eps: r.eps, drop_fraction: r.drop_fraction, rise_fraction: r.rise_fraction, dt_cap_minutes: r.dt_cap_minutes, max_detection_lag_steps: r.max_detection_lag_steps, comparison_mode: r.comparison_mode, fixed_cadence_minutes: r.fixed_cadence_minutes, grid_step_minutes: r.grid_step_minutes, model_scope: r.model_scope, }, null, 2); const warnings = []; if (r.use_jumps) { warnings.push("Jump stress test: ε is derived from a continuous diffusion and does not bound missed jump events."); } if (r.comparison_mode === "fixed_cadence" && r.fixed_cadence_minutes < r.grid_step_minutes) { warnings.push(`The requested fixed cadence (${r.fixed_cadence_minutes} min) is finer than the simulation grid (${r.grid_step_minutes.toFixed(2)} min), so it is limited to one sample per grid point.`); } if (r.n_breaches === 0) { warnings.push("No barrier events occurred in this run. Increase the horizon/volatility, lower the thresholds, or choose another seed."); } showWarnings(warnings); } catch (err) { showWarnings([err.message]); } finally { setBusy(button, false); } } /** Initialize collapsible parameter cards. */ function initCollapsibleCards() { document.querySelectorAll(".card.collapsible > h2").forEach((heading) => { const card = heading.parentElement; heading.addEventListener("click", () => { card.classList.toggle("collapsed"); }); }); } // ---- init ---- async function init() { $("mc-run-btn").dataset.label = "Run simulation"; try { await fetchJson("/api/health"); $("health").textContent = "ready"; } catch { $("health").textContent = "offline"; } bindRange("mc-sigma"); bindLogRange("mc-eps"); updateComparisonModeCopy(); initCollapsibleCards(); $("mc-comparison-mode").addEventListener("change", async () => { updateComparisonModeCopy(); await runMonteCarlo(); }); $("mc-run-btn").addEventListener("click", runMonteCarlo); // Enter opens on a compelling scenario immediately. await runMonteCarlo(); } init().catch(err => { $("health").textContent = `Error: ${err.message}`; showWarnings(["Initialization error: " + err.message]); console.error(err); });