Touched up notebooks + webapp
This commit is contained in:
@@ -0,0 +1,475 @@
|
||||
/* Theme tokens and page structure intentionally mirror ClimbingBoardGPT. */
|
||||
:root {
|
||||
--base00: #1A1B26;
|
||||
--base01: #16161E;
|
||||
--base02: #2F3549;
|
||||
--base03: #444B6A;
|
||||
--base04: #787C99;
|
||||
--base05: #A9B1D6;
|
||||
--base07: #D5D6DB;
|
||||
--base08: #F7768E;
|
||||
--base0a: #0DB9D7;
|
||||
--base0b: #9ECE6A;
|
||||
--base0c: #B4F9F8;
|
||||
--base0d: #2AC3DE;
|
||||
--base0e: #BB9AF7;
|
||||
--base0f: #F7768E;
|
||||
--bg: var(--base00);
|
||||
--off-bg: var(--base01);
|
||||
--inner-bg: var(--base02);
|
||||
--fg: var(--base05);
|
||||
--off-fg: var(--base04);
|
||||
--muted: var(--base03);
|
||||
--link: var(--base0d);
|
||||
--hover: var(--base0c);
|
||||
--highlight: var(--base0a);
|
||||
--logo: var(--base0b);
|
||||
--danger: var(--base08);
|
||||
--border: rgba(120, 124, 153, 0.3);
|
||||
--sans: "Inter", sans-serif;
|
||||
--mono: "Fira Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: var(--sans);
|
||||
font-size: 16px;
|
||||
line-height: 1.6rem;
|
||||
background: var(--bg);
|
||||
color: var(--fg);
|
||||
}
|
||||
|
||||
.site-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
gap: 1.5rem;
|
||||
max-width: 78rem;
|
||||
margin: 1rem auto 0;
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
.eyebrow {
|
||||
margin: 0;
|
||||
color: var(--logo);
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.site-header h1 {
|
||||
margin: 0;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.site-header h1::before { content: none; }
|
||||
|
||||
.site-header p {
|
||||
margin: 0;
|
||||
color: var(--off-fg);
|
||||
}
|
||||
|
||||
.health {
|
||||
flex-shrink: 0;
|
||||
font-size: 0.78rem;
|
||||
color: var(--highlight);
|
||||
white-space: nowrap;
|
||||
border: 1px solid var(--border);
|
||||
padding: 0.25rem 0.5rem;
|
||||
background: var(--inner-bg);
|
||||
}
|
||||
|
||||
.layout {
|
||||
display: grid;
|
||||
grid-template-columns: 22rem minmax(0, 1fr);
|
||||
grid-template-rows: auto 1fr;
|
||||
grid-template-areas:
|
||||
"col-top col-viewer"
|
||||
"col-info col-viewer";
|
||||
gap: 2rem;
|
||||
padding: 2rem 1rem 1rem;
|
||||
max-width: 78rem;
|
||||
margin: 0 auto;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
#col-top { grid-area: col-top; }
|
||||
#col-viewer { grid-area: col-viewer; }
|
||||
#col-info { grid-area: col-info; }
|
||||
|
||||
.controls {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.card, .result-card {
|
||||
background: var(--off-bg);
|
||||
border: 1px solid var(--border);
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.card h2, .result-card h2 {
|
||||
margin: 0 0 1rem;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: var(--fg);
|
||||
}
|
||||
|
||||
.card h2::before, .result-card h2::before { content: none; }
|
||||
|
||||
label {
|
||||
display: block;
|
||||
margin: 0.7rem 0;
|
||||
font-size: 0.82rem;
|
||||
color: var(--off-fg);
|
||||
}
|
||||
|
||||
input, select, textarea {
|
||||
display: block;
|
||||
width: 100%;
|
||||
margin-top: 0.28rem;
|
||||
border: 1px solid var(--border);
|
||||
padding: 0.6rem 0.7rem;
|
||||
font: inherit;
|
||||
color: var(--fg);
|
||||
background: var(--inner-bg);
|
||||
}
|
||||
|
||||
input:focus, select:focus, textarea:focus {
|
||||
outline: 2px solid rgba(137, 221, 255, 0.28);
|
||||
border-color: var(--hover);
|
||||
}
|
||||
|
||||
button {
|
||||
width: 100%;
|
||||
border: 1px solid var(--link);
|
||||
padding: 0.68rem 0.9rem;
|
||||
margin-top: 0.4rem;
|
||||
font-weight: 700;
|
||||
color: var(--bg);
|
||||
background: var(--link);
|
||||
cursor: pointer;
|
||||
font-family: var(--sans);
|
||||
}
|
||||
|
||||
button:hover {
|
||||
border-color: var(--hover);
|
||||
background: var(--hover);
|
||||
}
|
||||
|
||||
button:disabled { opacity: 0.55; cursor: not-allowed; }
|
||||
|
||||
/* Keep the run control reachable while scrolling through the long parameter card. */
|
||||
#mc-run-btn {
|
||||
position: sticky;
|
||||
bottom: 0.6rem;
|
||||
z-index: 5;
|
||||
box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.card.collapsible > h2 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.card.collapsible > h2::after {
|
||||
content: "▾";
|
||||
font-size: 2rem;
|
||||
color: var(--muted);
|
||||
margin-left: auto;
|
||||
padding-left: 0.5rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.card.collapsible.collapsed > h2::after { content: "▸"; }
|
||||
.card.collapsible > h2:hover::after { color: var(--off-fg); }
|
||||
.card.collapsible.collapsed > *:not(h2) { display: none; }
|
||||
|
||||
.field-help {
|
||||
display: block;
|
||||
margin-top: 0.35rem;
|
||||
color: var(--muted);
|
||||
font-size: 0.72rem;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.checkbox-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.55rem;
|
||||
color: var(--fg);
|
||||
}
|
||||
|
||||
.checkbox-label input {
|
||||
width: auto;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
input[type="range"] {
|
||||
padding: 0;
|
||||
height: 6px;
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
background: var(--inner-bg);
|
||||
border: 1px solid var(--border);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
input[type="range"]::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
background: var(--link);
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.range-row {
|
||||
display: flex;
|
||||
gap: 0.55rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.range-row input[type="range"] { flex: 1; }
|
||||
|
||||
.range-row input[type="number"] {
|
||||
width: 7.5rem;
|
||||
flex-shrink: 0;
|
||||
-moz-appearance: textfield;
|
||||
}
|
||||
|
||||
.range-row input[type="number"]::-webkit-inner-spin-button,
|
||||
.range-row input[type="number"]::-webkit-outer-spin-button {
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.note p, .small {
|
||||
color: var(--off-fg);
|
||||
font-size: 0.82rem;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.note p:first-of-type { margin-top: 0; }
|
||||
.note p:last-child { margin-bottom: 0; }
|
||||
|
||||
.result-header {
|
||||
text-align: center;
|
||||
margin-bottom: 0.85rem;
|
||||
}
|
||||
|
||||
.result-header h2 { margin-bottom: 0.25rem; }
|
||||
|
||||
.result-header p {
|
||||
margin: 0;
|
||||
color: var(--off-fg);
|
||||
font-size: 0.84rem;
|
||||
}
|
||||
|
||||
.headline {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 0.6rem;
|
||||
max-width: 820px;
|
||||
margin: 0 auto 1rem;
|
||||
}
|
||||
|
||||
.tile {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 0.6rem 0.4rem;
|
||||
border: 1px solid var(--border);
|
||||
background: var(--inner-bg);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.tile-num {
|
||||
font-size: 1.3rem;
|
||||
font-weight: 700;
|
||||
color: var(--highlight);
|
||||
line-height: 1.1;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.tile-lab {
|
||||
font-size: 0.74rem;
|
||||
font-weight: 600;
|
||||
color: var(--fg);
|
||||
margin-top: 0.2rem;
|
||||
}
|
||||
|
||||
.tile-sub {
|
||||
font-size: 0.66rem;
|
||||
color: var(--muted);
|
||||
margin-top: 0.1rem;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
.result-note {
|
||||
max-width: 760px;
|
||||
margin: 0 auto 0.85rem;
|
||||
color: var(--off-fg);
|
||||
font-size: 0.78rem;
|
||||
line-height: 1.45;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.chart-stage {
|
||||
width: 100%;
|
||||
max-width: 960px;
|
||||
margin: 0 auto;
|
||||
min-height: 420px;
|
||||
border: 1px solid var(--border);
|
||||
background: var(--bg);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.chart-stage .js-plotly-plot { width: 100% !important; }
|
||||
|
||||
.warning-box {
|
||||
margin: 0.7rem auto 0.85rem;
|
||||
max-width: 760px;
|
||||
border: 1px solid rgba(255, 203, 107, 0.55);
|
||||
background: rgba(255, 203, 107, 0.12);
|
||||
color: var(--highlight);
|
||||
padding: 0.65rem 0.8rem;
|
||||
font-size: 0.8rem;
|
||||
text-align: left;
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
.advanced-opts {
|
||||
margin-top: 0.9rem;
|
||||
}
|
||||
|
||||
.advanced-opts summary {
|
||||
cursor: pointer;
|
||||
font-size: 0.78rem;
|
||||
color: var(--muted);
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.advanced-opts summary:hover { color: var(--highlight); }
|
||||
|
||||
.method-note {
|
||||
max-width: 900px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
color: var(--off-fg);
|
||||
font-size: 0.8rem;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.method-note ol {
|
||||
margin: 0.65rem 0 0.45rem;
|
||||
padding-left: 1.25rem;
|
||||
}
|
||||
|
||||
.method-note li + li { margin-top: 0.32rem; }
|
||||
.method-note p { margin: 0.45rem 0 0; }
|
||||
.method-note strong { color: var(--fg); }
|
||||
|
||||
.explain dl { margin: 0; }
|
||||
|
||||
.explain dt {
|
||||
color: var(--highlight);
|
||||
font-size: 0.78rem;
|
||||
margin-top: 0.75rem;
|
||||
}
|
||||
|
||||
.explain dt:first-child { margin-top: 0; }
|
||||
|
||||
.explain dd {
|
||||
margin: 0.22rem 0 0;
|
||||
color: var(--off-fg);
|
||||
font-size: 0.78rem;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.explain p {
|
||||
color: var(--off-fg);
|
||||
font-size: 0.82rem;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.explain p:first-of-type { margin-top: 0; }
|
||||
.explain p:last-child { margin-bottom: 0; }
|
||||
|
||||
.link-list {
|
||||
margin: 0;
|
||||
padding-left: 1.1rem;
|
||||
color: var(--off-fg);
|
||||
font-size: 0.82rem;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.link-list li::marker {
|
||||
content: '·\00A0\00A0';
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.json-block {
|
||||
margin-top: 1rem;
|
||||
color: var(--off-fg);
|
||||
}
|
||||
|
||||
.json-block summary {
|
||||
cursor: pointer;
|
||||
font-size: 0.78rem;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.json-block summary:hover { color: var(--highlight); }
|
||||
|
||||
.json-block pre {
|
||||
overflow: auto;
|
||||
max-height: 300px;
|
||||
padding: 1rem;
|
||||
background: var(--inner-bg);
|
||||
color: var(--off-fg);
|
||||
border: 1px solid var(--border);
|
||||
font-size: 0.76rem;
|
||||
}
|
||||
|
||||
.site-footer {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem 1.5rem;
|
||||
max-width: 78rem;
|
||||
margin: 0 auto 1.5rem;
|
||||
padding: 0 1rem;
|
||||
color: var(--off-fg);
|
||||
font-size: 0.82rem;
|
||||
}
|
||||
|
||||
.site-footer a, .link-list a {
|
||||
color: var(--link);
|
||||
transition: color 0.15s ease;
|
||||
}
|
||||
|
||||
.site-footer a:hover, .link-list a:hover { color: var(--hover); }
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.layout {
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-rows: auto auto auto;
|
||||
grid-template-areas:
|
||||
"col-top"
|
||||
"col-viewer"
|
||||
"col-info";
|
||||
}
|
||||
|
||||
.site-header { flex-direction: column; }
|
||||
.headline { grid-template-columns: repeat(2, 1fr); }
|
||||
}
|
||||
|
||||
@media (max-width: 520px) {
|
||||
.headline { grid-template-columns: 1fr; }
|
||||
.range-row { align-items: stretch; flex-direction: column; }
|
||||
.range-row input[type="number"] { width: 100%; }
|
||||
}
|
||||
@@ -0,0 +1,373 @@
|
||||
/*
|
||||
* 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 <strong>fixed-cadence</strong> 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 <strong>detection quality and observation cost</strong>.';
|
||||
$("allocation-method").innerHTML =
|
||||
'<strong>Allocate samples:</strong> 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 <strong>fixed-rate</strong> 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 <strong>same number of samples on every path</strong>. The experiment therefore compares sample placement—not computational cost.';
|
||||
$("allocation-method").innerHTML =
|
||||
'<strong>Allocate samples:</strong> 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 cards to match the ClimbingBoardGPT interaction. */
|
||||
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);
|
||||
});
|
||||
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
||||
<rect width="32" height="32" rx="4" fill="#1A1B26"/>
|
||||
<text x="16" y="23" text-anchor="middle" font-size="20" fill="#F7768E" font-family="sans-serif">▼</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 232 B |
@@ -0,0 +1,206 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Adaptive Barrier Monitor</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600&display=swap">
|
||||
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg">
|
||||
<script src="https://cdn.plot.ly/plotly-3.0.1.min.js"></script>
|
||||
<link rel="stylesheet" href="/static/app.css?v=1" />
|
||||
</head>
|
||||
<body>
|
||||
<header class="site-header">
|
||||
<div>
|
||||
<p class="eyebrow">Adaptive Barrier Monitor</p>
|
||||
<h1>State-dependent sampling near price barriers</h1>
|
||||
<p>Monte Carlo demo comparing adaptive sampling with equal-budget and fixed-cadence baselines.</p>
|
||||
</div>
|
||||
<div id="health" class="health">Loading…</div>
|
||||
</header>
|
||||
|
||||
<main class="layout">
|
||||
<!-- Left column, row 1: interactive controls. -->
|
||||
<section class="controls" id="col-top">
|
||||
<div class="card collapsible" id="card-simulation">
|
||||
<h2>Simulation parameters</h2>
|
||||
<label>Comparison mode
|
||||
<select id="mc-comparison-mode">
|
||||
<option value="equal_budget">Equal budget</option>
|
||||
<option value="fixed_cadence">Fixed cadence</option>
|
||||
</select>
|
||||
<span class="field-help">Equal budget isolates sample placement. Fixed cadence compares detection and observation cost against an independently chosen interval.</span>
|
||||
</label>
|
||||
<label id="mc-fixed-cadence-row" hidden>Fixed cadence (minutes)
|
||||
<input id="mc-fixed-cadence" type="number" min="0.1" max="39000" step="0.1" value="60" />
|
||||
<span class="field-help">The fixed monitor samples at this interval, plus the start and end of the window. Smaller values use more observations.</span>
|
||||
</label>
|
||||
<label>Annual volatility (σ)
|
||||
<div class="range-row">
|
||||
<input id="mc-sigma" type="range" min="0.05" max="1.0" step="0.01" value="0.30" />
|
||||
<input id="mc-sigma-val" type="number" min="0.05" max="1.0" step="0.01" value="0.30" />
|
||||
</div>
|
||||
<span class="field-help">Annualised volatility. Higher values produce wider price swings and more barrier events.</span>
|
||||
</label>
|
||||
<label>Annual drift (μ)
|
||||
<input id="mc-mu" type="number" min="-0.5" max="0.5" step="0.01" value="0.07" />
|
||||
<span class="field-help">Average annual return. Positive values tilt paths upward; negative values push them toward the lower barrier.</span>
|
||||
</label>
|
||||
<label>Window (minutes)
|
||||
<input id="mc-window" type="number" min="5" max="39000" step="5" value="1950" />
|
||||
<span class="field-help">1950 minutes is about five trading days. Longer windows tend to produce more events.</span>
|
||||
</label>
|
||||
<label>Number of paths
|
||||
<input id="mc-paths" type="number" min="1" max="100" step="1" value="20" />
|
||||
<span class="field-help">More paths give steadier aggregate results but take longer to render.</span>
|
||||
</label>
|
||||
<label>Steps per path
|
||||
<input id="mc-steps" type="number" min="50" max="2000" step="10" value="500" />
|
||||
<span class="field-help">The fine simulation grid used as the reference history. More steps resolve shorter-lived crossings.</span>
|
||||
</label>
|
||||
<label>Drop threshold (%)
|
||||
<input id="mc-threshold" type="number" min="0.1" max="50" step="0.1" value="5" />
|
||||
<span class="field-help">Percentage decline from the initial price that defines a lower-barrier event.</span>
|
||||
</label>
|
||||
<label>Rise threshold (%)
|
||||
<input id="mc-rise-threshold" type="number" min="0.1" max="50" step="0.1" value="10" />
|
||||
<span class="field-help">Percentage increase from the initial price that defines an upper-barrier event.</span>
|
||||
</label>
|
||||
<label>Miss budget (ε)
|
||||
<div class="range-row">
|
||||
<input id="mc-eps" type="range" min="1e-6" max="0.1" step="1e-6" value="0.001" />
|
||||
<input id="mc-eps-val" type="number" min="1e-6" max="0.1" step="0.0001" value="0.001" />
|
||||
</div>
|
||||
<span class="field-help">Local diffusion-model design parameter. Lower ε produces denser adaptive sampling; it is not a global guarantee and does not cover jumps.</span>
|
||||
</label>
|
||||
<label>Max interval cap (minutes)
|
||||
<input id="mc-dtcap" type="number" min="1" max="1440" step="1" placeholder="auto" />
|
||||
<span class="field-help">Leave empty for an automatic cap of window/15. Lower values force denser sampling everywhere.</span>
|
||||
</label>
|
||||
<label>Detection deadline (grid steps)
|
||||
<input id="mc-max-lag" type="number" min="0" max="100" step="1" value="3" />
|
||||
<span class="field-help">An event counts as detected only if a sample still lies beyond the barrier within this many reference-grid steps.</span>
|
||||
</label>
|
||||
<label>RNG seed
|
||||
<input id="mc-seed" type="number" min="0" max="100000" step="1" placeholder="random" />
|
||||
<span class="field-help">Leave empty for a new run, or enter a seed to reproduce and share the same paths.</span>
|
||||
</label>
|
||||
<label>
|
||||
<span class="checkbox-label">
|
||||
<input id="mc-jumps" type="checkbox" /> Include jumps (Merton jump-diffusion)
|
||||
</span>
|
||||
<span class="field-help">Adds sudden moves that the diffusion-derived schedule cannot anticipate. Treat this as a model-risk stress test.</span>
|
||||
</label>
|
||||
<button id="mc-run-btn">Run simulation</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Right column: results and simulated paths. -->
|
||||
<section class="viewer" id="col-viewer">
|
||||
<div class="result-card">
|
||||
<div class="result-header">
|
||||
<h2 id="result-title">Monte Carlo Sandbox</h2>
|
||||
<p id="result-subtitle">Running…</p>
|
||||
</div>
|
||||
|
||||
<div class="headline">
|
||||
<div class="tile">
|
||||
<span class="tile-num" id="hl-adaptive">–</span>
|
||||
<span class="tile-lab">events detected — adaptive</span>
|
||||
<span class="tile-sub" id="hl-adaptive-sub">↓ lower · ↑ upper</span>
|
||||
</div>
|
||||
<div class="tile">
|
||||
<span class="tile-num" id="hl-fixed">–</span>
|
||||
<span class="tile-lab">events detected — fixed</span>
|
||||
<span class="tile-sub" id="hl-fixed-sub">↓ lower · ↑ upper</span>
|
||||
</div>
|
||||
<div class="tile">
|
||||
<span class="tile-num" id="hl-samples">–</span>
|
||||
<span class="tile-lab">total samples</span>
|
||||
<span class="tile-sub" id="hl-samples-sub">matched path by path</span>
|
||||
</div>
|
||||
<div class="tile">
|
||||
<span class="tile-num" id="hl-lag">–</span>
|
||||
<span class="tile-lab">mean detection lag</span>
|
||||
<span class="tile-sub">adaptive vs fixed · detected events only</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="result-note" id="comparison-note">
|
||||
The adaptive and fixed monitors receive the <strong>same number of samples on every path</strong>.
|
||||
The experiment therefore compares sample placement—not computational cost.
|
||||
</p>
|
||||
<div id="warning-box" class="warning-box" hidden></div>
|
||||
|
||||
<div id="chart-stage" class="chart-stage">
|
||||
<div style="padding:3rem; text-align:center; color:var(--muted)">Running simulation…</div>
|
||||
</div>
|
||||
|
||||
<details class="advanced-opts method-note" open>
|
||||
<summary>How the Monte Carlo comparison works</summary>
|
||||
<ol>
|
||||
<li><strong>Simulate:</strong> generate independent GBM or Merton jump-diffusion price paths on a fine grid.</li>
|
||||
<li><strong>Mark events:</strong> record each path's first lower- and upper-barrier crossing.</li>
|
||||
<li id="allocation-method"><strong>Allocate samples:</strong> adaptive intervals shrink near the closest barrier; the fixed monitor receives the same sample count, spaced uniformly.</li>
|
||||
<li><strong>Score:</strong> an event is detected when a sampled price remains beyond the barrier within the selected deadline.</li>
|
||||
</ol>
|
||||
<p>
|
||||
<span id="method-conclusion">The headline compares detections and mean lag under an equal sample budget.</span>
|
||||
The miss budget <strong>ε</strong> is a local diffusion-model design parameter, not a global guarantee.
|
||||
</p>
|
||||
</details>
|
||||
|
||||
<details class="json-block">
|
||||
<summary>Raw result JSON</summary>
|
||||
<pre id="stats-json">{}</pre>
|
||||
</details>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Left column, row 2: supporting information, mirroring ClimbingBoardGPT. -->
|
||||
<section class="controls" id="col-info">
|
||||
<div class="card explain">
|
||||
<h2>How to read it</h2>
|
||||
<dl>
|
||||
<dt>Open cyan circles</dt>
|
||||
<dd>Adaptive observations. The interval shrinks quadratically as the log-price approaches the nearer barrier.</dd>
|
||||
<dt>Grey dots</dt>
|
||||
<dd id="fixed-explanation">An equal-budget fixed sampler with the same number of observations on each path, spaced uniformly.</dd>
|
||||
<dt>Red × and green ▲</dt>
|
||||
<dd>The first lower- and upper-barrier events on the fine reference grid.</dd>
|
||||
<dt>Headline</dt>
|
||||
<dd id="headline-explanation">Same paths and the same sample budget. Compare how many events each schedule confirms before the deadline.</dd>
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
<div class="card note">
|
||||
<h2>Research demo caveat</h2>
|
||||
<p>The adaptive law is derived from a continuous diffusion model. Discrete grids, estimated volatility, latency, and jumps weaken any literal miss-probability guarantee.</p>
|
||||
</div>
|
||||
|
||||
<div class="card explain">
|
||||
<h2>How this works</h2>
|
||||
<p>The app simulates price paths, marks first barrier crossings, and compares two observation schedules on exactly the same paths.</p>
|
||||
<p>Adaptive intervals use a Brownian-bridge proxy and shrink with squared distance to the nearest barrier. Fixed observations are either equal-budget or independently spaced at a chosen cadence.</p>
|
||||
</div>
|
||||
|
||||
<div class="card note">
|
||||
<h2>Links</h2>
|
||||
<ul class="link-list">
|
||||
<li><a href="https://pawelsarkowicz.xyz" target="_blank" rel="noreferrer">pawelsarkowicz.xyz</a></li>
|
||||
<li><a href="https://github.com/psark007/adaptive-barrier-monitor" target="_blank" rel="noreferrer">Adaptive Barrier Monitor repo</a></li>
|
||||
<li><a href="https://github.com/psark007/adaptive-barrier-monitor/blob/main/LICENSE" target="_blank" rel="noreferrer">License</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<footer class="site-footer">
|
||||
<span>© Pawel Sarkowicz</span>
|
||||
</footer>
|
||||
|
||||
<script src="/static/app.js?v=1"></script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user