Touched up notebooks + webapp

This commit is contained in:
2026-07-31 17:05:14 -04:00
commit 8e6c98945b
31 changed files with 10824 additions and 0 deletions
+23
View File
@@ -0,0 +1,23 @@
FROM python:3.12-slim
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
PIP_NO_CACHE_DIR=1
WORKDIR /app
COPY pyproject.toml README.md ./
COPY src ./src
RUN pip install ".[webapp]"
COPY webapp ./webapp
RUN useradd --create-home --uid 10001 appuser && chown -R appuser:appuser /app
USER appuser
EXPOSE 8055
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD python -c "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8055/api/health', timeout=2)"
CMD ["uvicorn", "webapp.app:app", "--host", "0.0.0.0", "--port", "8055"]
+1
View File
@@ -0,0 +1 @@
"""FastAPI application package for the Adaptive Barrier Monitor demo."""
+123
View File
@@ -0,0 +1,123 @@
"""FastAPI web demo for the Adaptive Barrier Monitor."""
from __future__ import annotations
import re
import sys
from pathlib import Path
from typing import Any, Literal
import numpy as np
from fastapi import FastAPI
from fastapi.responses import HTMLResponse
from fastapi.staticfiles import StaticFiles
from pydantic import BaseModel, Field
# Support both an installed package (`pip install -e .`) and direct launches
# from a source checkout (`uvicorn webapp.app:app`).
REPO_ROOT = Path(__file__).resolve().parents[1]
SRC_DIR = REPO_ROOT / "src"
if str(SRC_DIR) not in sys.path:
sys.path.insert(0, str(SRC_DIR))
from adaptive_barrier import __version__
from adaptive_barrier.engine import (
DROP_FRACTION,
LOG_BARRIER,
RISE_FRACTION,
TYPICAL_ANNUAL_VOL,
WINDOW_MINUTES,
run_monte_carlo_simulation,
)
STATIC_DIR = REPO_ROOT / "webapp" / "static"
class SimulateRequest(BaseModel):
"""Validated Monte Carlo and detector-comparison parameters."""
S0: float = Field(100.0, gt=0.0, le=10000.0)
sigma_annual: float = Field(TYPICAL_ANNUAL_VOL, gt=0.0, le=2.0)
mu_annual: float = Field(0.0, ge=-0.5, le=0.5)
window_minutes: float = Field(1950.0, gt=0.0, le=39000.0)
n_paths: int = Field(20, ge=1, le=100)
n_steps: int = Field(500, ge=50, le=2000)
use_jumps: bool = Field(
False,
description="Use Merton jump diffusion. The Brownian miss proxy does not control jumps.",
)
jump_intensity: float = Field(25.0, ge=0.0, le=500.0)
jump_mean: float = Field(-0.02, ge=-0.5, le=0.5)
jump_sigma: float = Field(0.05, ge=0.0, le=0.5)
eps: float = Field(1e-3, gt=0.0, lt=1.0)
dt_cap_minutes: float | None = Field(None, gt=0.0, le=1440.0)
rng_seed: int | None = Field(None, ge=0, le=2_147_483_647)
drop_fraction: float = Field(DROP_FRACTION, gt=0.0, lt=1.0)
rise_fraction: float = Field(RISE_FRACTION, gt=0.0, le=1.0)
max_detection_lag_steps: int | None = Field(3, ge=0, le=100)
comparison_mode: Literal["equal_budget", "fixed_cadence"] = "equal_budget"
fixed_cadence_minutes: float = Field(60.0, gt=0.0, le=39000.0)
def _file_version(path: Path) -> str:
try:
stat = path.stat()
return f"{int(stat.st_mtime)}-{stat.st_size}"
except FileNotFoundError:
return "missing"
def _json_safe(obj: Any) -> Any:
if isinstance(obj, dict):
return {str(key): _json_safe(value) for key, value in obj.items()}
if isinstance(obj, (list, tuple)):
return [_json_safe(value) for value in obj]
if isinstance(obj, np.integer):
return int(obj)
if isinstance(obj, np.floating):
value = float(obj)
return value if np.isfinite(value) else None
if isinstance(obj, np.ndarray):
return _json_safe(obj.tolist())
if isinstance(obj, np.bool_):
return bool(obj)
return obj
app = FastAPI(title="Adaptive Barrier Monitor", version=__version__)
app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static")
@app.get("/")
def index() -> HTMLResponse:
html = (STATIC_DIR / "index.html").read_text(encoding="utf-8")
html = re.sub(
r'app\.css\?v=[^\s"\']+',
f"app.css?v={_file_version(STATIC_DIR / 'app.css')}",
html,
)
html = re.sub(
r'app\.js\?v=[^\s"\']+',
f"app.js?v={_file_version(STATIC_DIR / 'app.js')}",
html,
)
return HTMLResponse(content=html)
@app.get("/api/health")
def health() -> dict:
return {
"ok": True,
"version": __version__,
"default_sigma_annual": TYPICAL_ANNUAL_VOL,
"default_window_minutes": WINDOW_MINUTES,
"drop_fraction": DROP_FRACTION,
"rise_fraction": RISE_FRACTION,
"log_barrier": LOG_BARRIER,
}
@app.post("/api/simulate")
def simulate(req: SimulateRequest) -> dict:
result = run_monte_carlo_simulation(**req.model_dump())
return _json_safe(result)
+475
View File
@@ -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%; }
}
+373
View File
@@ -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 qualitycost 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);
});
+4
View File
@@ -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

+206
View File
@@ -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>