Touched up notebooks + webapp
This commit is contained in:
+27
@@ -0,0 +1,27 @@
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*.egg-info/
|
||||
*.egg
|
||||
build/
|
||||
dist/
|
||||
|
||||
# Jupyter
|
||||
.ipynb_checkpoints/
|
||||
|
||||
# Generated notebook figures (regenerated on execution)
|
||||
images/
|
||||
|
||||
# Testing / tooling
|
||||
.pytest_cache/
|
||||
.mypy_cache/
|
||||
.ruff_cache/
|
||||
|
||||
# Virtual environments
|
||||
.venv/
|
||||
venv/
|
||||
|
||||
# OS / editor
|
||||
.DS_Store
|
||||
.idea/
|
||||
.vscode/
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 Pawel Sarkowicz
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,195 @@
|
||||
# Adaptive Barrier Monitor
|
||||
|
||||
A five-notebook quantitative-finance project connecting random walks, Brownian
|
||||
motion, geometric Brownian motion, first-passage times, Brownian bridges, and
|
||||
state-dependent monitoring.
|
||||
|
||||
The motivating question is:
|
||||
|
||||
> A stock is monitored for a large move over a short window. Continuous polling
|
||||
> is expensive. What probability model describes a hidden barrier crossing, and
|
||||
> how can that model inform a sampling schedule?
|
||||
|
||||
The project is written for a mathematically mature reader who wants to see how
|
||||
Gaussian processes, conditioning, stochastic calculus, and Monte Carlo methods
|
||||
appear in a practical monitoring problem.
|
||||
|
||||
## Core results and scope
|
||||
|
||||
Under geometric Brownian motion,
|
||||
|
||||
$$
|
||||
\frac{dS_t}{S_t}=\mu\,dt+\sigma\,dW_t,
|
||||
$$
|
||||
|
||||
the relative log-price $X_t=\log(S_t/S_0)$ is arithmetic Brownian motion. A
|
||||
10% drop corresponds to the lower log barrier $B=\log(0.9)$.
|
||||
|
||||
For zero drift, the probability of touching the barrier by time $T$ is
|
||||
|
||||
$$
|
||||
P(\tau_B\leq T)=2\Phi\!\left(\frac{B}{\sigma\sqrt{T}}\right).
|
||||
$$
|
||||
|
||||
At 30% annualised volatility, a 10% move in five trading minutes is roughly a
|
||||
49-standard-deviation diffusion event. Pure GBM therefore assigns it probability
|
||||
below ordinary floating-point resolution; jumps and market microstructure are
|
||||
essential for realistic extreme-move modelling.
|
||||
|
||||
Conditional on two observations $x_0,x_T>B$, the Brownian-bridge probability
|
||||
that the hidden path crossed the barrier is
|
||||
|
||||
$$
|
||||
P_{\mathrm{cross}}
|
||||
=\exp\!\left(
|
||||
-\frac{2(x_0-B)(x_T-B)}{\sigma^2\Delta t}
|
||||
\right).
|
||||
$$
|
||||
|
||||
If both endpoint distances are set equal to $D$, inversion gives
|
||||
|
||||
$$
|
||||
\Delta t_{\mathrm{sym}}
|
||||
=\frac{2D^2}{\sigma^2\log(1/\varepsilon)}.
|
||||
$$
|
||||
|
||||
This inversion is exact **conditional on both endpoints being known**. In a
|
||||
live scheduler, the future endpoint is unknown; the implementation substitutes
|
||||
the current distance for both endpoints. Thus $\varepsilon$ is a local
|
||||
diffusion-design parameter, not an unconditional miss guarantee, and it does
|
||||
not control jumps. A hard maximum polling interval remains necessary.
|
||||
|
||||
## Notebooks
|
||||
|
||||
| # | Notebook | Main topics |
|
||||
|---|---|---|
|
||||
| 01 | [Random walks to Brownian motion](notebooks/01_random_walks_to_brownian_motion.ipynb) | Log returns, the $\min(s,t)$ covariance kernel, Cholesky sampling, Brownian scaling |
|
||||
| 02 | [GBM and Itô's lemma](notebooks/02_geometric_brownian_motion_and_ito.ipynb) | Multiplicative prices, exact GBM simulation, Itô correction |
|
||||
| 03 | [First passage and reflection](notebooks/03_first_passage_and_reflection_principle.ipynb) | Reflection principle, Bachelier–Lévy formula, hitting-time diagnostics |
|
||||
| 04 | [Brownian bridges and hidden crossings](notebooks/04_brownian_bridges_and_miss_probability.ipynb) | Gaussian conditioning, Schur complements, bridge crossing probabilities |
|
||||
| 05 | [Adaptive barrier monitoring](notebooks/05_adaptive_barrier_monitor.ipynb) | Unit-consistent scheduler, practical polling cap, controlled jump stress test, model-risk discussion |
|
||||
|
||||
The analytical formulae are checked against Monte Carlo simulation in the
|
||||
notebooks.
|
||||
|
||||
## Interactive web application
|
||||
|
||||
The FastAPI/Plotly demo compares two sampling schedules on the **same simulated
|
||||
paths**. The adaptive schedule uses the local symmetric-endpoint bridge proxy,
|
||||
while the fixed baseline can run in either of two modes:
|
||||
|
||||
- **Equal budget:** the fixed schedule receives exactly the adaptive schedule's
|
||||
sample count on each path, isolating where observations are placed.
|
||||
- **Fixed cadence:** the fixed schedule samples at a user-selected interval, so
|
||||
detection quality, lag, and total observation cost can be compared directly.
|
||||
|
||||
A barrier event counts as detected only if a sampled point remains beyond the
|
||||
barrier within a configurable number of simulation steps. The comparison is
|
||||
therefore explicit and reproducible rather than based on an unrestricted
|
||||
"eventually detected" definition.
|
||||
|
||||
The demo supports GBM and an optional Merton jump-diffusion stress mode. When
|
||||
jumps are enabled, the interface explicitly warns that the Brownian diffusion
|
||||
parameter $\varepsilon$ does not bound jump-event misses.
|
||||
|
||||
### Run locally
|
||||
|
||||
```bash
|
||||
python -m venv .venv
|
||||
source .venv/bin/activate
|
||||
python -m pip install -e ".[webapp]"
|
||||
python -m uvicorn webapp.app:app --host 127.0.0.1 --port 8055
|
||||
```
|
||||
|
||||
Open `http://127.0.0.1:8055`.
|
||||
|
||||
### Docker
|
||||
|
||||
```bash
|
||||
docker compose -f docker-compose.webapp.yml up --build
|
||||
```
|
||||
|
||||
For an existing Caddy Docker network:
|
||||
|
||||
```bash
|
||||
docker compose -f docker-compose.webapp.proxy.yml up --build -d
|
||||
```
|
||||
|
||||
The container runs as a non-root user and includes an HTTP health check.
|
||||
|
||||
## Run the notebooks
|
||||
|
||||
```bash
|
||||
python -m venv .venv
|
||||
source .venv/bin/activate
|
||||
pip install -e ".[notebooks]"
|
||||
jupyter lab notebooks/
|
||||
```
|
||||
|
||||
Notebooks that request market data cache successful downloads under
|
||||
`data/cache/`. Their analytical and simulation sections remain usable when the
|
||||
network fetch is unavailable.
|
||||
|
||||
## Tests
|
||||
|
||||
```bash
|
||||
pip install -e ".[dev,webapp]"
|
||||
pytest
|
||||
```
|
||||
|
||||
The test suite covers:
|
||||
|
||||
- inversion of the Brownian-bridge formula;
|
||||
- vectorised interval calculations and input validation;
|
||||
- consistent time/volatility units in the adaptive schedule;
|
||||
- enforcement of the detection deadline;
|
||||
- exact per-path sample-budget equality;
|
||||
- equivalence of zero-intensity jump diffusion and GBM;
|
||||
- aggregate simulation invariants.
|
||||
|
||||
## Project structure
|
||||
|
||||
```text
|
||||
adaptive-barrier-monitor/
|
||||
├── notebooks/ # five executed research notebooks
|
||||
├── src/adaptive_barrier/
|
||||
│ ├── __init__.py
|
||||
│ └── engine.py # samplers, closed forms, scheduler, evaluation
|
||||
├── tests/
|
||||
│ └── test_engine.py
|
||||
├── webapp/
|
||||
│ ├── app.py # FastAPI API
|
||||
│ ├── Dockerfile
|
||||
│ └── static/ # vanilla JS, Plotly, CSS
|
||||
├── .github/workflows/tests.yml
|
||||
├── pyproject.toml
|
||||
├── requirements.txt
|
||||
├── requirements-webapp.txt
|
||||
├── requirements-dev.txt
|
||||
├── bibliography.md
|
||||
├── LICENSE
|
||||
└── webapp.md
|
||||
```
|
||||
|
||||
## Model limitations
|
||||
|
||||
- **Online endpoint uncertainty:** the bridge crossing formula is conditional on
|
||||
both endpoints; the scheduler uses a local approximation before the next
|
||||
endpoint exists.
|
||||
- **Jump risk:** diffusion-derived polling cannot guarantee detection of sudden
|
||||
jump-and-recovery events.
|
||||
- **No market microstructure model:** bid–ask bounce, asynchronous feeds,
|
||||
exchange halts, queueing, and packet latency are not represented.
|
||||
- **Simulation-grid dependence:** the web demo's detection deadline is measured
|
||||
in simulated grid steps; changing `n_steps` changes its physical duration.
|
||||
- **Educational calibration:** jump parameters in the sandbox are user-controlled
|
||||
stress parameters, not production estimates.
|
||||
|
||||
## Tech stack
|
||||
|
||||
Python, NumPy, SciPy, pandas, SymPy, Matplotlib, FastAPI, Pydantic, Uvicorn,
|
||||
Plotly.js, Docker, pytest, and GitHub Actions.
|
||||
|
||||
## License
|
||||
|
||||
MIT — see [`LICENSE`](LICENSE).
|
||||
@@ -0,0 +1,54 @@
|
||||
# Bibliography & References
|
||||
|
||||
Selected references supporting the mathematics, quantitative-finance models, numerical methods, and software used in the notebook series. Exercise-only and redundant lookup references have been removed so this file reflects the material that remains in the public project.
|
||||
|
||||
## Brownian motion and stochastic calculus
|
||||
|
||||
- **Mörters, P. & Peres, Y.** *Brownian Motion.* Cambridge University Press, 2010.
|
||||
- **Karatzas, I. & Shreve, S. E.** *Brownian Motion and Stochastic Calculus.* Springer, 2nd ed., 1991.
|
||||
- **Klebaner, F. C.** *Introduction to Stochastic Calculus with Applications.* Imperial College Press, 3rd ed., 2012.
|
||||
- **Shreve, S. E.** *Stochastic Calculus for Finance II: Continuous-Time Models.* Springer, 2004.
|
||||
- **Øksendal, B.** *Stochastic Differential Equations.* Springer, 6th ed., 2003.
|
||||
- **Protter, P. E.** *Stochastic Integration and Differential Equations.* Springer, 2nd ed., 2005.
|
||||
- **Bachelier, L.** “Théorie de la spéculation.” *Annales scientifiques de l’École Normale Supérieure* 17 (1900). Historical origin of Brownian price modelling.
|
||||
|
||||
## Quantitative-finance models and barrier problems
|
||||
|
||||
- **Hull, J. C.** *Options, Futures, and Other Derivatives.* Pearson, 11th ed., 2021.
|
||||
- **Joshi, M. S.** *The Concepts and Practice of Mathematical Finance.* Cambridge University Press, 2nd ed., 2008.
|
||||
- **Glasserman, P.** *Monte Carlo Methods in Financial Engineering.* Springer, 2003.
|
||||
- **Cont, R. & Tankov, P.** *Financial Modelling with Jump Processes.* Chapman & Hall/CRC, 2004.
|
||||
|
||||
## Gaussian conditioning and numerical linear algebra
|
||||
|
||||
- **Rasmussen, C. E. & Williams, C. K. I.** *Gaussian Processes for Machine Learning.* MIT Press, 2006. Free full text: <https://gaussianprocess.org/gpml/>.
|
||||
- **Anderson, T. W.** *An Introduction to Multivariate Statistical Analysis.* Wiley, 3rd ed., 2003.
|
||||
- **Revuz, D. & Yor, M.** *Continuous Martingales and Brownian Motion.* Springer, 3rd ed., 1999.
|
||||
- **Golub, G. H. & Van Loan, C. F.** *Matrix Computations.* Johns Hopkins University Press, 4th ed., 2013.
|
||||
- **Strang, G.** *Introduction to Linear Algebra.* Wellesley-Cambridge Press, 6th ed., 2023. Positive-definite matrices and factorisations. Lectures: <https://ocw.mit.edu/courses/18-06-linear-algebra-spring-2010/>.
|
||||
- **Trefethen, L. N. & Bau, D.** *Numerical Linear Algebra.* SIAM, 1997.
|
||||
|
||||
## Concise online references
|
||||
|
||||
- **Wiener process / Brownian motion:** <https://en.wikipedia.org/wiki/Wiener_process>
|
||||
- **Itô’s lemma:** <https://en.wikipedia.org/wiki/It%C3%B4%27s_lemma>
|
||||
- **Geometric Brownian motion:** <https://en.wikipedia.org/wiki/Geometric_Brownian_motion>
|
||||
- **Reflection principle:** <https://en.wikipedia.org/wiki/Reflection_principle_(Wiener_process)>
|
||||
- **Brownian bridge:** <https://en.wikipedia.org/wiki/Brownian_bridge>
|
||||
- **Jump diffusion:** <https://en.wikipedia.org/wiki/Jump_diffusion>
|
||||
- **Maximum drawdown:** <https://en.wikipedia.org/wiki/Maximum_drawdown>
|
||||
- **Cholesky decomposition:** <https://en.wikipedia.org/wiki/Cholesky_decomposition>
|
||||
- **Monte Carlo method:** <https://en.wikipedia.org/wiki/Monte_Carlo_method>
|
||||
|
||||
## Software and data
|
||||
|
||||
- **NumPy:** <https://numpy.org/doc/stable/> — vectorised simulation, array operations, and Cholesky factorisation.
|
||||
- **SciPy:** <https://docs.scipy.org/doc/scipy/> — Gaussian distribution functions and numerical utilities.
|
||||
- **Matplotlib:** <https://matplotlib.org/stable/> — notebook figures.
|
||||
- **pandas:** <https://pandas.pydata.org/docs/> — market-data frames, time indexes, and resampling.
|
||||
- **SymPy:** <https://docs.sympy.org/> — symbolic checks in the Itô-calculus notebook.
|
||||
- **Requests:** <https://requests.readthedocs.io/> — HTTP access to the market-data endpoint.
|
||||
|
||||
## Conventions
|
||||
|
||||
Trading time is measured using 252 trading days × 6.5 hours × 60 minutes = 98,280 trading minutes per year. Annualised volatility is scaled by the square root of elapsed trading time.
|
||||
Vendored
+1952
File diff suppressed because it is too large
Load Diff
Vendored
+1640
File diff suppressed because it is too large
Load Diff
Vendored
+392
@@ -0,0 +1,392 @@
|
||||
timestamp,open,high,low,close,volume
|
||||
2026-07-20 13:30:00+00:00,747.0599975585938,748.0499877929688,746.7999877929688,748.0499877929688,1851776
|
||||
2026-07-20 13:35:00+00:00,748.030029296875,748.7100219726562,747.72998046875,748.1699829101562,833394
|
||||
2026-07-20 13:40:00+00:00,748.1699829101562,748.5900268554688,748.0599975585938,748.5599975585938,511709
|
||||
2026-07-20 13:45:00+00:00,748.5700073242188,748.72998046875,747.530029296875,747.5800170898438,805592
|
||||
2026-07-20 13:50:00+00:00,747.5800170898438,747.8400268554688,746.25,746.4550170898438,874876
|
||||
2026-07-20 13:55:00+00:00,746.4500122070312,746.489990234375,745.52001953125,745.6900024414062,671795
|
||||
2026-07-20 14:00:00+00:00,745.6799926757812,746.3599853515625,745.3900146484375,745.885009765625,524174
|
||||
2026-07-20 14:05:00+00:00,745.9000244140625,746.1199951171875,745.27001953125,745.4000244140625,475704
|
||||
2026-07-20 14:10:00+00:00,745.3400268554688,745.3800048828125,744.7650146484375,744.989990234375,414066
|
||||
2026-07-20 14:15:00+00:00,745.02001953125,745.47998046875,744.6300048828125,744.9000244140625,420432
|
||||
2026-07-20 14:20:00+00:00,744.8499755859375,745.2100219726562,744.260009765625,744.969970703125,561519
|
||||
2026-07-20 14:25:00+00:00,744.969970703125,745.739990234375,744.9099731445312,745.6799926757812,355120
|
||||
2026-07-20 14:30:00+00:00,745.6900024414062,745.9600219726562,745.0399780273438,745.260009765625,475390
|
||||
2026-07-20 14:35:00+00:00,745.260009765625,745.530029296875,744.9598999023438,745.3300170898438,453223
|
||||
2026-07-20 14:40:00+00:00,745.3499755859375,745.7899780273438,744.7520141601562,744.8900146484375,258874
|
||||
2026-07-20 14:45:00+00:00,744.8800048828125,745.739990234375,744.4901123046875,744.6400146484375,323848
|
||||
2026-07-20 14:50:00+00:00,744.6199951171875,744.9400024414062,744.1500244140625,744.3250122070312,539597
|
||||
2026-07-20 14:55:00+00:00,744.3200073242188,744.489990234375,743.6900024414062,744.1099853515625,429824
|
||||
2026-07-20 15:00:00+00:00,744.1400146484375,744.8800048828125,743.8049926757812,744.5800170898438,297794
|
||||
2026-07-20 15:05:00+00:00,744.5599975585938,745.4849853515625,743.8499755859375,745.4099731445312,502571
|
||||
2026-07-20 15:10:00+00:00,745.4000244140625,745.4600219726562,744.6699829101562,744.7999877929688,226403
|
||||
2026-07-20 15:15:00+00:00,744.7999877929688,745.530029296875,744.6099853515625,745.27001953125,295430
|
||||
2026-07-20 15:20:00+00:00,745.260009765625,745.969970703125,745.0700073242188,745.75,316075
|
||||
2026-07-20 15:25:00+00:00,745.719970703125,745.969970703125,745.219970703125,745.8699951171875,503848
|
||||
2026-07-20 15:30:00+00:00,745.8900146484375,746.02001953125,745.77001953125,745.8800048828125,192342
|
||||
2026-07-20 15:35:00+00:00,745.9000244140625,745.9299926757812,745.3900146484375,745.4600219726562,214063
|
||||
2026-07-20 15:40:00+00:00,745.469970703125,745.969970703125,745.280029296875,745.969970703125,214146
|
||||
2026-07-20 15:45:00+00:00,745.97998046875,745.989990234375,745.0999755859375,745.2999877929688,254897
|
||||
2026-07-20 15:50:00+00:00,745.2899780273438,746.3599853515625,745.1300048828125,746.219970703125,400298
|
||||
2026-07-20 15:55:00+00:00,746.22998046875,746.4400024414062,745.9400024414062,745.989990234375,310885
|
||||
2026-07-20 16:00:00+00:00,746.0,746.25,745.5800170898438,746.1599731445312,407969
|
||||
2026-07-20 16:05:00+00:00,746.1599731445312,746.8350219726562,746.1599731445312,746.7999877929688,588741
|
||||
2026-07-20 16:10:00+00:00,746.780029296875,747.0050048828125,746.6400146484375,746.7899780273438,297597
|
||||
2026-07-20 16:15:00+00:00,746.8300170898438,746.844970703125,746.3099975585938,746.6799926757812,314922
|
||||
2026-07-20 16:20:00+00:00,746.7000122070312,746.8800048828125,746.6199951171875,746.780029296875,227319
|
||||
2026-07-20 16:25:00+00:00,746.780029296875,746.8300170898438,746.1699829101562,746.5,323898
|
||||
2026-07-20 16:30:00+00:00,746.489990234375,746.780029296875,745.7999877929688,745.8300170898438,328389
|
||||
2026-07-20 16:35:00+00:00,745.8200073242188,746.3699951171875,745.8099975585938,746.3499755859375,240742
|
||||
2026-07-20 16:40:00+00:00,746.3300170898438,746.52001953125,744.5,745.4299926757812,1044388
|
||||
2026-07-20 16:45:00+00:00,745.4099731445312,746.1799926757812,745.2100219726562,746.1300048828125,420926
|
||||
2026-07-20 16:50:00+00:00,746.1199951171875,746.155029296875,744.9099731445312,744.969970703125,316124
|
||||
2026-07-20 16:55:00+00:00,744.969970703125,745.1900024414062,744.72998046875,745.0,481423
|
||||
2026-07-20 17:00:00+00:00,745.010009765625,745.2100219726562,744.5499877929688,744.552490234375,307945
|
||||
2026-07-20 17:05:00+00:00,744.5399780273438,744.6500244140625,744.1300048828125,744.5900268554688,288703
|
||||
2026-07-20 17:10:00+00:00,744.5900268554688,744.6400146484375,743.8400268554688,743.9000244140625,294244
|
||||
2026-07-20 17:15:00+00:00,743.8900146484375,744.260009765625,743.5599975585938,744.1199951171875,383022
|
||||
2026-07-20 17:20:00+00:00,744.114990234375,744.489990234375,743.969970703125,744.1099853515625,228512
|
||||
2026-07-20 17:25:00+00:00,744.0999755859375,744.3699951171875,743.8900146484375,744.1199951171875,258856
|
||||
2026-07-20 17:30:00+00:00,744.1300048828125,744.4201049804688,743.9500122070312,744.3250122070312,243517
|
||||
2026-07-20 17:35:00+00:00,744.2999877929688,744.5399780273438,743.989990234375,744.010009765625,267253
|
||||
2026-07-20 17:40:00+00:00,744.030029296875,744.0499877929688,743.5900268554688,743.760009765625,344669
|
||||
2026-07-20 17:45:00+00:00,743.77001953125,744.3499755859375,743.6300048828125,744.25,281957
|
||||
2026-07-20 17:50:00+00:00,744.219970703125,744.9199829101562,744.2100219726562,744.6500244140625,290349
|
||||
2026-07-20 17:55:00+00:00,744.6500244140625,744.6900024414062,744.2100830078125,744.5499877929688,261761
|
||||
2026-07-20 18:00:00+00:00,744.5499877929688,745.1699829101562,744.5499877929688,744.8499755859375,363635
|
||||
2026-07-20 18:05:00+00:00,744.8400268554688,744.9400024414062,744.530029296875,744.875,239896
|
||||
2026-07-20 18:10:00+00:00,744.9000244140625,745.219970703125,744.7100219726562,745.030029296875,219433
|
||||
2026-07-20 18:15:00+00:00,745.02001953125,745.3800048828125,744.8800048828125,745.239990234375,387343
|
||||
2026-07-20 18:20:00+00:00,745.219970703125,745.2899780273438,744.25,744.280029296875,618991
|
||||
2026-07-20 18:25:00+00:00,744.2899780273438,744.7999877929688,744.1300048828125,744.469970703125,343916
|
||||
2026-07-20 18:30:00+00:00,744.469970703125,744.489990234375,743.47998046875,743.6649780273438,473873
|
||||
2026-07-20 18:35:00+00:00,743.6599731445312,744.2949829101562,743.4400024414062,743.5750122070312,340647
|
||||
2026-07-20 18:40:00+00:00,743.5750122070312,743.6199951171875,742.530029296875,742.5449829101562,546380
|
||||
2026-07-20 18:45:00+00:00,742.5499877929688,743.0499877929688,742.4299926757812,742.7000122070312,685176
|
||||
2026-07-20 18:50:00+00:00,742.7100219726562,743.22998046875,742.5599975585938,743.0999755859375,302428
|
||||
2026-07-20 18:55:00+00:00,743.094970703125,743.1400146484375,742.47998046875,742.719970703125,308207
|
||||
2026-07-20 19:00:00+00:00,742.760009765625,743.1900024414062,742.4199829101562,742.5900268554688,616940
|
||||
2026-07-20 19:05:00+00:00,742.5900268554688,742.8209838867188,742.25,742.6950073242188,525097
|
||||
2026-07-20 19:10:00+00:00,742.6994018554688,743.0999755859375,741.7000122070312,741.7000122070312,591743
|
||||
2026-07-20 19:15:00+00:00,741.7100219726562,742.2100219726562,741.5103149414062,741.9199829101562,970049
|
||||
2026-07-20 19:20:00+00:00,741.9299926757812,742.4299926757812,741.7899780273438,742.2999877929688,614806
|
||||
2026-07-20 19:25:00+00:00,742.3200073242188,742.9199829101562,741.8599853515625,742.72998046875,683468
|
||||
2026-07-20 19:30:00+00:00,742.739990234375,743.2000122070312,742.3400268554688,742.97998046875,709918
|
||||
2026-07-20 19:35:00+00:00,742.9600219726562,743.1300048828125,742.510009765625,742.97998046875,647393
|
||||
2026-07-20 19:40:00+00:00,742.969970703125,743.3400268554688,742.7999877929688,742.989990234375,568417
|
||||
2026-07-20 19:45:00+00:00,743.0,743.25,742.7000122070312,743.0800170898438,993664
|
||||
2026-07-20 19:50:00+00:00,743.0700073242188,743.0700073242188,741.760009765625,742.2000122070312,1894670
|
||||
2026-07-20 19:55:00+00:00,742.1799926757812,742.239990234375,741.6099853515625,742.1400146484375,4361321
|
||||
2026-07-21 13:30:00+00:00,746.2899780273438,746.6799926757812,745.1199951171875,745.5999755859375,1901011
|
||||
2026-07-21 13:35:00+00:00,745.6099853515625,746.231689453125,745.27001953125,745.280029296875,824912
|
||||
2026-07-21 13:40:00+00:00,745.260009765625,745.3099975585938,744.2708740234375,744.510009765625,600352
|
||||
2026-07-21 13:45:00+00:00,744.52001953125,745.0,744.1799926757812,744.9500122070312,420126
|
||||
2026-07-21 13:50:00+00:00,744.969970703125,745.3499755859375,744.260009765625,745.0700073242188,386215
|
||||
2026-07-21 13:55:00+00:00,745.0499877929688,745.364990234375,744.6099853515625,745.3599853515625,344126
|
||||
2026-07-21 14:00:00+00:00,745.3699951171875,745.869873046875,744.9600219726562,745.4199829101562,348723
|
||||
2026-07-21 14:05:00+00:00,745.4199829101562,745.77001953125,745.02001953125,745.1950073242188,212358
|
||||
2026-07-21 14:10:00+00:00,745.1900024414062,745.8599853515625,744.9500122070312,745.7449951171875,284660
|
||||
2026-07-21 14:15:00+00:00,745.719970703125,746.1699829101562,744.7899780273438,746.02001953125,302137
|
||||
2026-07-21 14:20:00+00:00,746.0150146484375,746.1900024414062,745.4550170898438,745.97998046875,224452
|
||||
2026-07-21 14:25:00+00:00,745.9600219726562,746.989990234375,745.9299926757812,746.8699951171875,450689
|
||||
2026-07-21 14:30:00+00:00,746.8499755859375,747.25,746.5800170898438,746.7999877929688,340012
|
||||
2026-07-21 14:35:00+00:00,746.7949829101562,747.02001953125,746.0900268554688,746.2999877929688,301549
|
||||
2026-07-21 14:40:00+00:00,746.2999877929688,746.635009765625,745.77001953125,746.5999755859375,298164
|
||||
2026-07-21 14:45:00+00:00,746.6099853515625,747.260009765625,746.4500122070312,746.9099731445312,401228
|
||||
2026-07-21 14:50:00+00:00,746.9099731445312,746.9600219726562,746.5,746.6900024414062,166050
|
||||
2026-07-21 14:55:00+00:00,746.6799926757812,746.7899780273438,746.260009765625,746.739990234375,202701
|
||||
2026-07-21 15:00:00+00:00,746.77001953125,746.8900146484375,745.8300170898438,746.0009765625,310493
|
||||
2026-07-21 15:05:00+00:00,746.0,747.0700073242188,745.8499755859375,746.97998046875,292853
|
||||
2026-07-21 15:10:00+00:00,746.97998046875,747.52001953125,746.9400024414062,747.2097778320312,337144
|
||||
2026-07-21 15:15:00+00:00,747.2100219726562,747.5499877929688,747.1588134765625,747.4099731445312,202955
|
||||
2026-07-21 15:20:00+00:00,747.4099731445312,747.97998046875,747.3200073242188,747.656005859375,426494
|
||||
2026-07-21 15:25:00+00:00,747.6544189453125,748.010009765625,747.6099853515625,748.0,191435
|
||||
2026-07-21 15:30:00+00:00,747.97998046875,748.0,747.4099731445312,747.7750244140625,364371
|
||||
2026-07-21 15:35:00+00:00,747.7899780273438,747.9299926757812,747.4299926757812,747.5800170898438,267226
|
||||
2026-07-21 15:40:00+00:00,747.5599975585938,747.8588256835938,747.5198974609375,747.594970703125,235112
|
||||
2026-07-21 15:45:00+00:00,747.580322265625,747.9199829101562,747.530029296875,747.8115234375,165894
|
||||
2026-07-21 15:50:00+00:00,747.8200073242188,747.97998046875,747.7000122070312,747.719970703125,270841
|
||||
2026-07-21 15:55:00+00:00,747.77001953125,747.9000244140625,747.469970703125,747.52001953125,178981
|
||||
2026-07-21 16:00:00+00:00,747.52001953125,748.22998046875,747.489990234375,748.219970703125,235413
|
||||
2026-07-21 16:05:00+00:00,748.2255249023438,748.3099975585938,748.0700073242188,748.1500244140625,157808
|
||||
2026-07-21 16:10:00+00:00,748.1497802734375,748.27001953125,748.0399780273438,748.22998046875,163585
|
||||
2026-07-21 16:15:00+00:00,748.239990234375,748.530029296875,748.219970703125,748.469970703125,456214
|
||||
2026-07-21 16:20:00+00:00,748.47998046875,748.75,748.4099731445312,748.5800170898438,202970
|
||||
2026-07-21 16:25:00+00:00,748.5800170898438,748.7000122070312,748.4400024414062,748.6599731445312,119457
|
||||
2026-07-21 16:30:00+00:00,748.6400146484375,748.780029296875,748.4801025390625,748.6849975585938,186951
|
||||
2026-07-21 16:35:00+00:00,748.680908203125,748.719970703125,748.489990234375,748.6699829101562,101468
|
||||
2026-07-21 16:40:00+00:00,748.6699829101562,748.739990234375,748.5,748.6099853515625,120496
|
||||
2026-07-21 16:45:00+00:00,748.6300048828125,748.6500244140625,748.4000244140625,748.489990234375,125200
|
||||
2026-07-21 16:50:00+00:00,748.489990234375,748.9949951171875,748.4500122070312,748.9598999023438,215362
|
||||
2026-07-21 16:55:00+00:00,748.9600219726562,749.0399780273438,748.6799926757812,748.6900024414062,620830
|
||||
2026-07-21 17:00:00+00:00,748.6884155273438,748.8300170898438,748.5805053710938,748.6199951171875,170343
|
||||
2026-07-21 17:05:00+00:00,748.6199951171875,748.85498046875,748.5800170898438,748.7100219726562,356355
|
||||
2026-07-21 17:10:00+00:00,748.719970703125,748.739990234375,748.530029296875,748.5999755859375,478893
|
||||
2026-07-21 17:15:00+00:00,748.6099853515625,748.6599731445312,748.3300170898438,748.35498046875,335585
|
||||
2026-07-21 17:20:00+00:00,748.3499755859375,748.47998046875,748.25,748.4000244140625,192939
|
||||
2026-07-21 17:25:00+00:00,748.4199829101562,748.47998046875,748.239990234375,748.3599853515625,175614
|
||||
2026-07-21 17:30:00+00:00,748.3599853515625,748.5700073242188,748.2150268554688,748.3599853515625,225080
|
||||
2026-07-21 17:35:00+00:00,748.3499755859375,748.7999877929688,748.3400268554688,748.7000122070312,247679
|
||||
2026-07-21 17:40:00+00:00,748.7000122070312,748.72998046875,748.510009765625,748.669921875,142384
|
||||
2026-07-21 17:45:00+00:00,748.6599731445312,748.8599853515625,748.5499877929688,748.8200073242188,161776
|
||||
2026-07-21 17:50:00+00:00,748.8400268554688,748.9299926757812,748.6599731445312,748.6799926757812,306097
|
||||
2026-07-21 17:55:00+00:00,748.6749877929688,748.719970703125,748.4400024414062,748.5900268554688,232952
|
||||
2026-07-21 18:00:00+00:00,748.5900268554688,748.77001953125,748.260009765625,748.3200073242188,295577
|
||||
2026-07-21 18:05:00+00:00,748.2999877929688,748.5499877929688,748.27001953125,748.4550170898438,163057
|
||||
2026-07-21 18:10:00+00:00,748.4400024414062,748.5490112304688,748.1199951171875,748.1599731445312,245581
|
||||
2026-07-21 18:15:00+00:00,748.1699829101562,748.27001953125,748.0900268554688,748.2100219726562,219304
|
||||
2026-07-21 18:20:00+00:00,748.1900024414062,748.4000244140625,748.1099853515625,748.2999877929688,200541
|
||||
2026-07-21 18:25:00+00:00,748.2899780273438,748.2999877929688,748.0650024414062,748.2100219726562,316580
|
||||
2026-07-21 18:30:00+00:00,748.219970703125,748.260009765625,747.9400024414062,747.969970703125,218286
|
||||
2026-07-21 18:35:00+00:00,747.969970703125,748.27001953125,747.9600219726562,748.1500244140625,227217
|
||||
2026-07-21 18:40:00+00:00,748.1500244140625,748.6099853515625,748.02001953125,748.5399780273438,215354
|
||||
2026-07-21 18:45:00+00:00,748.530029296875,748.6400146484375,748.3800048828125,748.4500122070312,161869
|
||||
2026-07-21 18:50:00+00:00,748.469970703125,748.510009765625,748.25,748.25927734375,185937
|
||||
2026-07-21 18:55:00+00:00,748.25,748.5700073242188,748.22998046875,748.3699951171875,314496
|
||||
2026-07-21 19:00:00+00:00,748.3800048828125,748.469970703125,748.1500244140625,748.27001953125,225750
|
||||
2026-07-21 19:05:00+00:00,748.25,748.4199829101562,748.1199951171875,748.135009765625,277072
|
||||
2026-07-21 19:10:00+00:00,748.1400146484375,748.4299926757812,748.0900268554688,748.239990234375,308319
|
||||
2026-07-21 19:15:00+00:00,748.25,748.4600219726562,748.1900024414062,748.3099975585938,190314
|
||||
2026-07-21 19:20:00+00:00,748.3099975585938,748.3900146484375,748.1900024414062,748.3099975585938,183500
|
||||
2026-07-21 19:25:00+00:00,748.2899780273438,748.4600219726562,748.219970703125,748.260009765625,375104
|
||||
2026-07-21 19:30:00+00:00,748.25,748.3900146484375,748.1500244140625,748.1799926757812,361485
|
||||
2026-07-21 19:35:00+00:00,748.1699829101562,748.5700073242188,748.1199951171875,748.4099731445312,332389
|
||||
2026-07-21 19:40:00+00:00,748.4005126953125,748.5880126953125,748.239990234375,748.530029296875,742764
|
||||
2026-07-21 19:45:00+00:00,748.530029296875,748.6400146484375,748.2449951171875,748.60498046875,605051
|
||||
2026-07-21 19:50:00+00:00,748.5900268554688,748.7000122070312,748.2899780273438,748.3800048828125,1277177
|
||||
2026-07-21 19:55:00+00:00,748.3800048828125,748.5800170898438,748.0900268554688,748.3300170898438,1743645
|
||||
2026-07-22 13:30:00+00:00,746.6199951171875,747.5700073242188,746.3699951171875,747.3150024414062,1723196
|
||||
2026-07-22 13:35:00+00:00,747.3099975585938,747.489990234375,747.02001953125,747.4000244140625,398272
|
||||
2026-07-22 13:40:00+00:00,747.4099731445312,747.7000122070312,747.0900268554688,747.5700073242188,386033
|
||||
2026-07-22 13:45:00+00:00,747.5999755859375,748.0599975585938,747.5700073242188,747.760009765625,387195
|
||||
2026-07-22 13:50:00+00:00,747.739990234375,748.1799926757812,747.39501953125,748.1500244140625,499544
|
||||
2026-07-22 13:55:00+00:00,748.1300048828125,748.7849731445312,748.02001953125,748.780029296875,496342
|
||||
2026-07-22 14:00:00+00:00,748.77001953125,748.969970703125,748.52001953125,748.969970703125,393130
|
||||
2026-07-22 14:05:00+00:00,748.969970703125,749.3800048828125,748.510009765625,748.5401000976562,386190
|
||||
2026-07-22 14:10:00+00:00,748.5399780273438,748.7750244140625,748.110107421875,748.239990234375,289947
|
||||
2026-07-22 14:15:00+00:00,748.260009765625,748.3200073242188,747.77001953125,748.1300048828125,251758
|
||||
2026-07-22 14:20:00+00:00,748.1400146484375,748.4299926757812,747.3800048828125,747.5999755859375,390741
|
||||
2026-07-22 14:25:00+00:00,747.6300048828125,747.8099975585938,747.0900268554688,747.2999877929688,311511
|
||||
2026-07-22 14:30:00+00:00,747.2899780273438,747.7899780273438,747.1699829101562,747.239990234375,405856
|
||||
2026-07-22 14:35:00+00:00,747.260009765625,747.4500122070312,747.05078125,747.3099975585938,396475
|
||||
2026-07-22 14:40:00+00:00,747.2999877929688,747.4500122070312,746.760009765625,746.780029296875,250611
|
||||
2026-07-22 14:45:00+00:00,746.7899780273438,747.469970703125,746.6799926757812,747.3699951171875,297686
|
||||
2026-07-22 14:50:00+00:00,747.3800048828125,747.6500244140625,747.0900268554688,747.4199829101562,348917
|
||||
2026-07-22 14:55:00+00:00,747.3900146484375,747.9769897460938,747.1500244140625,747.9349975585938,280430
|
||||
2026-07-22 15:00:00+00:00,747.9299926757812,748.4400024414062,747.8900146484375,748.3049926757812,277938
|
||||
2026-07-22 15:05:00+00:00,748.2999877929688,748.75,748.2000122070312,748.6099853515625,333941
|
||||
2026-07-22 15:10:00+00:00,748.6099853515625,749.039306640625,748.5900268554688,748.875,275844
|
||||
2026-07-22 15:15:00+00:00,748.8800048828125,748.9849853515625,748.52001953125,748.8599853515625,382048
|
||||
2026-07-22 15:20:00+00:00,748.8699951171875,749.4000244140625,748.8300170898438,749.3099975585938,177372
|
||||
2026-07-22 15:25:00+00:00,749.3200073242188,749.6799926757812,749.2606811523438,749.4099731445312,274008
|
||||
2026-07-22 15:30:00+00:00,749.4199829101562,749.510009765625,748.9199829101562,748.9600219726562,258364
|
||||
2026-07-22 15:35:00+00:00,748.9849853515625,749.1400146484375,748.7506713867188,748.9149780273438,184888
|
||||
2026-07-22 15:40:00+00:00,748.9149780273438,749.2100219726562,748.8499755859375,749.0,123396
|
||||
2026-07-22 15:45:00+00:00,749.02001953125,749.3800048828125,749.02001953125,749.3800048828125,251782
|
||||
2026-07-22 15:50:00+00:00,749.3800048828125,749.5900268554688,749.22998046875,749.280029296875,154286
|
||||
2026-07-22 15:55:00+00:00,749.260009765625,749.7899780273438,749.1799926757812,749.760009765625,157530
|
||||
2026-07-22 16:00:00+00:00,749.760009765625,749.8200073242188,749.489990234375,749.6849975585938,191780
|
||||
2026-07-22 16:05:00+00:00,749.6900024414062,749.9099731445312,749.5399780273438,749.7449951171875,230076
|
||||
2026-07-22 16:10:00+00:00,749.739990234375,749.7899780273438,749.5999755859375,749.7100219726562,200353
|
||||
2026-07-22 16:15:00+00:00,749.7100219726562,750.02001953125,749.6799926757812,749.9749755859375,195351
|
||||
2026-07-22 16:20:00+00:00,749.969970703125,749.969970703125,749.780029296875,749.8699951171875,234835
|
||||
2026-07-22 16:25:00+00:00,749.8699951171875,749.9400024414062,749.6799926757812,749.8499755859375,242635
|
||||
2026-07-22 16:30:00+00:00,749.8599853515625,750.0048217773438,749.75,749.9299926757812,178756
|
||||
2026-07-22 16:35:00+00:00,749.9199829101562,749.9600219726562,749.7899780273438,749.8699951171875,313848
|
||||
2026-07-22 16:40:00+00:00,749.8900146484375,749.8900146484375,749.6400146484375,749.7550048828125,179533
|
||||
2026-07-22 16:45:00+00:00,749.760009765625,749.9000244140625,749.52001953125,749.60498046875,179344
|
||||
2026-07-22 16:50:00+00:00,749.6099853515625,749.6799926757812,749.47998046875,749.52001953125,132286
|
||||
2026-07-22 16:55:00+00:00,749.510009765625,749.9500122070312,749.5,749.8300170898438,146439
|
||||
2026-07-22 17:00:00+00:00,749.8350219726562,749.9600219726562,749.3699951171875,749.5,233695
|
||||
2026-07-22 17:05:00+00:00,749.5,749.75,749.47998046875,749.6099853515625,580307
|
||||
2026-07-22 17:10:00+00:00,749.5999755859375,749.6199951171875,749.25,749.3200073242188,361788
|
||||
2026-07-22 17:15:00+00:00,749.3099975585938,749.4299926757812,749.1900024414062,749.25,502612
|
||||
2026-07-22 17:20:00+00:00,749.239990234375,749.260009765625,748.9500122070312,749.1749877929688,195943
|
||||
2026-07-22 17:25:00+00:00,749.1699829101562,749.3900146484375,749.0,749.0444946289062,196990
|
||||
2026-07-22 17:30:00+00:00,749.0499877929688,749.2899780273438,748.8599853515625,748.8800048828125,244761
|
||||
2026-07-22 17:35:00+00:00,748.8800048828125,749.4099731445312,748.8499755859375,749.3599853515625,210400
|
||||
2026-07-22 17:40:00+00:00,749.3599853515625,749.3699951171875,749.0499877929688,749.1199951171875,175551
|
||||
2026-07-22 17:45:00+00:00,749.1300048828125,749.22998046875,749.0150146484375,749.0499877929688,126164
|
||||
2026-07-22 17:50:00+00:00,749.0399780273438,749.1500244140625,748.6920166015625,748.7100219726562,166578
|
||||
2026-07-22 17:55:00+00:00,748.7000122070312,748.7000122070312,748.3900146484375,748.52001953125,645620
|
||||
2026-07-22 18:00:00+00:00,748.5150146484375,748.7100219726562,748.2000122070312,748.3800048828125,297919
|
||||
2026-07-22 18:05:00+00:00,748.3800048828125,748.6900024414062,748.27001953125,748.3289794921875,295069
|
||||
2026-07-22 18:10:00+00:00,748.2999877929688,748.4000244140625,748.010009765625,748.0399780273438,232207
|
||||
2026-07-22 18:15:00+00:00,748.030029296875,748.3900146484375,748.0250244140625,748.22998046875,203582
|
||||
2026-07-22 18:20:00+00:00,748.25,748.5900268554688,748.1599731445312,748.2225952148438,238646
|
||||
2026-07-22 18:25:00+00:00,748.22998046875,748.5499877929688,748.2100219726562,748.489990234375,188785
|
||||
2026-07-22 18:30:00+00:00,748.489990234375,748.489990234375,748.0499877929688,748.2000122070312,276360
|
||||
2026-07-22 18:35:00+00:00,748.1900024414062,748.469970703125,748.1599731445312,748.4099731445312,188524
|
||||
2026-07-22 18:40:00+00:00,748.4299926757812,748.469970703125,748.22998046875,748.2901000976562,288221
|
||||
2026-07-22 18:45:00+00:00,748.2899780273438,748.5999755859375,748.2048950195312,748.2224731445312,216078
|
||||
2026-07-22 18:50:00+00:00,748.219970703125,748.5798950195312,748.1099853515625,748.52001953125,245576
|
||||
2026-07-22 18:55:00+00:00,748.5,748.5900268554688,748.37890625,748.4099731445312,183536
|
||||
2026-07-22 19:00:00+00:00,748.4400024414062,748.4600219726562,748.1500244140625,748.239990234375,208281
|
||||
2026-07-22 19:05:00+00:00,748.2449951171875,748.3499755859375,748.1500244140625,748.1900024414062,162600
|
||||
2026-07-22 19:10:00+00:00,748.2000122070312,748.239990234375,747.8300170898438,747.8900146484375,248523
|
||||
2026-07-22 19:15:00+00:00,747.9000244140625,748.1900024414062,747.8300170898438,748.1099853515625,189199
|
||||
2026-07-22 19:20:00+00:00,748.1099853515625,748.1199951171875,747.7999877929688,747.8499755859375,205636
|
||||
2026-07-22 19:25:00+00:00,747.8499755859375,748.0999755859375,747.6500244140625,748.0,415094
|
||||
2026-07-22 19:30:00+00:00,748.0,748.0,747.6699829101562,747.7100219726562,616066
|
||||
2026-07-22 19:35:00+00:00,747.7100219726562,747.7999877929688,747.3599853515625,747.4099731445312,522654
|
||||
2026-07-22 19:40:00+00:00,747.4215087890625,747.9199829101562,747.4199829101562,747.7899780273438,556526
|
||||
2026-07-22 19:45:00+00:00,747.7949829101562,748.0700073242188,747.7550048828125,747.97998046875,742707
|
||||
2026-07-22 19:50:00+00:00,747.989990234375,748.2999877929688,747.6900024414062,747.8099975585938,870972
|
||||
2026-07-22 19:55:00+00:00,747.8200073242188,748.239990234375,747.2949829101562,747.3300170898438,3100017
|
||||
2026-07-23 13:30:00+00:00,739.3699951171875,741.3699951171875,738.469970703125,741.3200073242188,4201871
|
||||
2026-07-23 13:35:00+00:00,741.2999877929688,741.8699951171875,740.8499755859375,741.4450073242188,793095
|
||||
2026-07-23 13:40:00+00:00,741.4199829101562,742.5599975585938,741.4199829101562,741.8200073242188,612613
|
||||
2026-07-23 13:45:00+00:00,741.8499755859375,741.9949951171875,740.1699829101562,740.4000244140625,807521
|
||||
2026-07-23 13:50:00+00:00,740.3699951171875,740.9450073242188,739.719970703125,740.9299926757812,1031948
|
||||
2026-07-23 13:55:00+00:00,740.9500122070312,741.280029296875,739.2999877929688,739.5750122070312,676870
|
||||
2026-07-23 14:00:00+00:00,739.5800170898438,740.6300048828125,739.5800170898438,740.344970703125,531714
|
||||
2026-07-23 14:05:00+00:00,740.3599853515625,740.9299926757812,738.8699951171875,739.3599853515625,443580
|
||||
2026-07-23 14:10:00+00:00,739.344970703125,740.5399780273438,739.2100219726562,739.6900024414062,445606
|
||||
2026-07-23 14:15:00+00:00,739.6599731445312,740.3599853515625,739.4099731445312,739.510009765625,449562
|
||||
2026-07-23 14:20:00+00:00,739.52001953125,740.4349975585938,739.1699829101562,739.4400024414062,577693
|
||||
2026-07-23 14:25:00+00:00,739.4000244140625,739.9099731445312,738.989990234375,739.7999877929688,545442
|
||||
2026-07-23 14:30:00+00:00,739.739990234375,740.0399780273438,738.6599731445312,738.969970703125,449770
|
||||
2026-07-23 14:35:00+00:00,738.97998046875,740.510009765625,738.5900268554688,740.3800048828125,780782
|
||||
2026-07-23 14:40:00+00:00,740.3800048828125,740.6400146484375,738.6699829101562,738.8699951171875,432500
|
||||
2026-07-23 14:45:00+00:00,738.8599853515625,739.469970703125,738.6699829101562,738.8300170898438,422993
|
||||
2026-07-23 14:50:00+00:00,738.8400268554688,739.0700073242188,738.010009765625,738.1400146484375,527804
|
||||
2026-07-23 14:55:00+00:00,738.155029296875,738.5800170898438,738.0700073242188,738.3099975585938,354913
|
||||
2026-07-23 15:00:00+00:00,738.3200073242188,738.47998046875,737.5,737.6599731445312,456987
|
||||
2026-07-23 15:05:00+00:00,737.6699829101562,737.7000122070312,736.9400024414062,737.155029296875,831070
|
||||
2026-07-23 15:10:00+00:00,737.1599731445312,737.4099731445312,736.4500122070312,736.6500244140625,985434
|
||||
2026-07-23 15:15:00+00:00,736.6199951171875,737.2750244140625,736.5800170898438,737.1799926757812,637201
|
||||
2026-07-23 15:20:00+00:00,737.2000122070312,737.6900024414062,735.97998046875,736.669921875,1020234
|
||||
2026-07-23 15:25:00+00:00,736.6699829101562,736.7203979492188,735.8099975585938,735.9298706054688,857021
|
||||
2026-07-23 15:30:00+00:00,735.9099731445312,736.4199829101562,735.2100219726562,736.4099731445312,810901
|
||||
2026-07-23 15:35:00+00:00,736.4099731445312,737.4400024414062,736.3800048828125,737.3480224609375,696790
|
||||
2026-07-23 15:40:00+00:00,737.3400268554688,737.6599731445312,736.6400146484375,736.6400146484375,544099
|
||||
2026-07-23 15:45:00+00:00,736.6400146484375,736.969970703125,736.3599853515625,736.8961791992188,352859
|
||||
2026-07-23 15:50:00+00:00,736.8900146484375,737.3300170898438,736.5900268554688,737.3200073242188,320277
|
||||
2026-07-23 15:55:00+00:00,737.3300170898438,737.9000244140625,737.1199951171875,737.844970703125,333004
|
||||
2026-07-23 16:00:00+00:00,737.844970703125,738.3099975585938,737.530029296875,738.0349731445312,421746
|
||||
2026-07-23 16:05:00+00:00,738.0399780273438,738.469970703125,737.5499877929688,738.469970703125,343277
|
||||
2026-07-23 16:10:00+00:00,738.469970703125,738.8350219726562,738.4199829101562,738.6199951171875,409071
|
||||
2026-07-23 16:15:00+00:00,738.6199951171875,738.8300170898438,738.219970703125,738.5700073242188,1292232
|
||||
2026-07-23 16:20:00+00:00,738.5828247070312,739.6599731445312,738.5700073242188,739.4600219726562,484093
|
||||
2026-07-23 16:25:00+00:00,739.4500122070312,739.6199951171875,739.1500244140625,739.4500122070312,378385
|
||||
2026-07-23 16:30:00+00:00,739.4500122070312,739.489990234375,739.0599975585938,739.1199951171875,328174
|
||||
2026-07-23 16:35:00+00:00,739.0999755859375,739.719970703125,738.9400024414062,739.6998901367188,333622
|
||||
2026-07-23 16:40:00+00:00,739.7000122070312,739.7100219726562,738.969970703125,738.969970703125,222903
|
||||
2026-07-23 16:45:00+00:00,738.9600219726562,738.9600219726562,738.010009765625,738.0250244140625,351531
|
||||
2026-07-23 16:50:00+00:00,738.02001953125,738.3400268554688,737.75,737.97998046875,299219
|
||||
2026-07-23 16:55:00+00:00,737.97998046875,738.7100219726562,737.97998046875,738.6599731445312,271435
|
||||
2026-07-23 17:00:00+00:00,738.6699829101562,738.9400024414062,738.2000122070312,738.2100219726562,175458
|
||||
2026-07-23 17:05:00+00:00,738.219970703125,738.489990234375,737.7899780273438,738.489990234375,423821
|
||||
2026-07-23 17:10:00+00:00,738.4600219726562,738.8400268554688,738.4299926757812,738.6099853515625,405299
|
||||
2026-07-23 17:15:00+00:00,738.5599975585938,738.9000244140625,738.1599731445312,738.5250244140625,519411
|
||||
2026-07-23 17:20:00+00:00,738.5650024414062,739.030029296875,738.4000244140625,738.989990234375,1506693
|
||||
2026-07-23 17:25:00+00:00,738.97998046875,739.2000122070312,738.739990234375,738.75,374258
|
||||
2026-07-23 17:30:00+00:00,738.739990234375,738.7999877929688,738.0499877929688,738.0900268554688,647896
|
||||
2026-07-23 17:35:00+00:00,738.0549926757812,738.1199951171875,737.4650268554688,737.6199951171875,154118
|
||||
2026-07-23 17:40:00+00:00,737.6199951171875,737.8599853515625,737.27001953125,737.7100219726562,189087
|
||||
2026-07-23 17:45:00+00:00,737.7000122070312,738.2899780273438,737.5,738.1599731445312,179442
|
||||
2026-07-23 17:50:00+00:00,738.1699829101562,738.2399291992188,737.7000122070312,737.7100219726562,140875
|
||||
2026-07-23 17:55:00+00:00,737.6500244140625,737.7098999023438,737.1300048828125,737.1900024414062,149773
|
||||
2026-07-23 18:00:00+00:00,737.1599731445312,737.3099975585938,736.9000244140625,736.97998046875,212506
|
||||
2026-07-23 18:05:00+00:00,736.969970703125,737.1599731445312,736.8001098632812,736.8619995117188,209211
|
||||
2026-07-23 18:10:00+00:00,736.8900146484375,737.3699951171875,736.8699951171875,737.02978515625,593695
|
||||
2026-07-23 18:15:00+00:00,736.9719848632812,737.3599853515625,736.8200073242188,737.0,281077
|
||||
2026-07-23 18:20:00+00:00,737.02001953125,737.4400024414062,736.9400024414062,737.4099731445312,183660
|
||||
2026-07-23 18:25:00+00:00,737.4199829101562,737.489990234375,737.0349731445312,737.4199829101562,218263
|
||||
2026-07-23 18:30:00+00:00,737.4149780273438,737.6599731445312,737.3200073242188,737.5999755859375,279276
|
||||
2026-07-23 18:35:00+00:00,737.5800170898438,737.6900024414062,736.8300170898438,736.9299926757812,327191
|
||||
2026-07-23 18:40:00+00:00,736.9299926757812,737.02001953125,736.6099853515625,736.8400268554688,415699
|
||||
2026-07-23 18:45:00+00:00,736.8300170898438,737.4500122070312,736.8300170898438,737.2999877929688,583968
|
||||
2026-07-23 18:50:00+00:00,737.2999877929688,737.3699951171875,736.6868286132812,736.719970703125,295039
|
||||
2026-07-23 18:55:00+00:00,736.7100219726562,737.0599975585938,736.6300048828125,737.0499877929688,491634
|
||||
2026-07-23 19:00:00+00:00,737.0800170898438,737.3900146484375,736.780029296875,736.7999877929688,340647
|
||||
2026-07-23 19:05:00+00:00,736.7999877929688,736.9600219726562,736.7000122070312,736.72998046875,257690
|
||||
2026-07-23 19:10:00+00:00,736.719970703125,736.875,736.469970703125,736.5999755859375,201871
|
||||
2026-07-23 19:15:00+00:00,736.5999755859375,737.1599731445312,736.5900268554688,736.9500122070312,339486
|
||||
2026-07-23 19:20:00+00:00,736.969970703125,737.6300048828125,736.9500122070312,737.6199951171875,451009
|
||||
2026-07-23 19:25:00+00:00,737.6199951171875,737.9000244140625,737.3200073242188,737.47998046875,493815
|
||||
2026-07-23 19:30:00+00:00,737.489990234375,737.739990234375,736.6799926757812,737.1500244140625,580631
|
||||
2026-07-23 19:35:00+00:00,737.14501953125,737.219970703125,736.8300170898438,736.8900146484375,702327
|
||||
2026-07-23 19:40:00+00:00,736.8699951171875,736.97998046875,736.25,736.25,1754270
|
||||
2026-07-23 19:45:00+00:00,736.260009765625,736.4000244140625,735.719970703125,735.9249877929688,988958
|
||||
2026-07-23 19:50:00+00:00,735.9299926757812,736.97998046875,735.9299926757812,736.7100219726562,2437408
|
||||
2026-07-23 19:55:00+00:00,736.72998046875,738.6400146484375,736.72998046875,738.1699829101562,5113800
|
||||
2026-07-24 13:30:00+00:00,738.510009765625,739.5,738.4199829101562,739.277099609375,1467113
|
||||
2026-07-24 13:35:00+00:00,739.2899780273438,739.6599731445312,738.8800048828125,739.1099853515625,415105
|
||||
2026-07-24 13:40:00+00:00,739.0999755859375,739.1400756835938,738.6599731445312,738.8101196289062,406628
|
||||
2026-07-24 13:45:00+00:00,738.8099975585938,739.8800048828125,738.5700073242188,739.8200073242188,482528
|
||||
2026-07-24 13:50:00+00:00,739.8099975585938,739.8099975585938,737.6900024414062,737.8400268554688,758616
|
||||
2026-07-24 13:55:00+00:00,737.8300170898438,738.2000122070312,737.4299926757812,737.530029296875,480275
|
||||
2026-07-24 14:00:00+00:00,738.1099853515625,738.6701049804688,737.6799926757812,737.7440185546875,777067
|
||||
2026-07-24 14:05:00+00:00,737.7899780273438,739.489990234375,737.5700073242188,739.2999877929688,489734
|
||||
2026-07-24 14:10:00+00:00,739.2650146484375,739.905029296875,738.8250122070312,739.5394897460938,932941
|
||||
2026-07-24 14:15:00+00:00,739.5399780273438,739.7100219726562,738.0,738.5349731445312,993924
|
||||
2026-07-24 14:20:00+00:00,738.530029296875,739.9500122070312,738.1599731445312,739.5399780273438,401286
|
||||
2026-07-24 14:25:00+00:00,739.5599975585938,739.719970703125,738.969970703125,739.1799926757812,390349
|
||||
2026-07-24 14:30:00+00:00,739.1900024414062,739.22998046875,738.3300170898438,738.3400268554688,412680
|
||||
2026-07-24 14:35:00+00:00,738.3350219726562,738.8200073242188,737.9099731445312,738.0599975585938,331563
|
||||
2026-07-24 14:40:00+00:00,738.030029296875,738.3499755859375,737.8400268554688,737.905029296875,667881
|
||||
2026-07-24 14:45:00+00:00,737.8900146484375,738.5499877929688,737.4299926757812,738.530029296875,341313
|
||||
2026-07-24 14:50:00+00:00,738.5349731445312,739.3200073242188,738.530029296875,739.010009765625,368611
|
||||
2026-07-24 14:55:00+00:00,739.010009765625,740.7000122070312,739.010009765625,740.5900268554688,457825
|
||||
2026-07-24 15:00:00+00:00,740.5900268554688,740.9600219726562,740.2100219726562,740.239990234375,1066983
|
||||
2026-07-24 15:05:00+00:00,740.22998046875,740.6400146484375,739.6599731445312,739.6998901367188,310429
|
||||
2026-07-24 15:10:00+00:00,739.6900024414062,742.1699829101562,739.5800170898438,741.8800048828125,772738
|
||||
2026-07-24 15:15:00+00:00,741.8599853515625,743.0,741.3400268554688,741.4774780273438,736171
|
||||
2026-07-24 15:20:00+00:00,741.4949951171875,742.6500244140625,741.4099731445312,742.3699951171875,453802
|
||||
2026-07-24 15:25:00+00:00,742.39501953125,743.1300048828125,742.3300170898438,742.6099853515625,413289
|
||||
2026-07-24 15:30:00+00:00,742.5999755859375,742.9000244140625,742.0499877929688,742.0800170898438,242596
|
||||
2026-07-24 15:35:00+00:00,742.0449829101562,743.1580200195312,742.030029296875,743.0700073242188,318123
|
||||
2026-07-24 15:40:00+00:00,743.0599975585938,743.25,742.0289916992188,742.2899780273438,273038
|
||||
2026-07-24 15:45:00+00:00,742.2899780273438,742.6500244140625,742.0,742.1300048828125,215877
|
||||
2026-07-24 15:50:00+00:00,742.135009765625,742.7899780273438,742.1300048828125,742.5599975585938,351940
|
||||
2026-07-24 15:55:00+00:00,742.5599975585938,743.1099853515625,742.52001953125,743.0150146484375,310650
|
||||
2026-07-24 16:00:00+00:00,743.0399780273438,743.239990234375,742.6400146484375,743.2100219726562,667546
|
||||
2026-07-24 16:05:00+00:00,743.2000122070312,743.5499877929688,742.9400024414062,743.280029296875,454257
|
||||
2026-07-24 16:10:00+00:00,743.2999877929688,743.719970703125,742.8499755859375,742.969970703125,340403
|
||||
2026-07-24 16:15:00+00:00,743.0,743.3200073242188,742.8099975585938,743.1347045898438,255553
|
||||
2026-07-24 16:20:00+00:00,743.1448974609375,743.5,743.0999755859375,743.3599243164062,200917
|
||||
2026-07-24 16:25:00+00:00,743.3599853515625,743.5700073242188,743.1799926757812,743.1900024414062,178237
|
||||
2026-07-24 16:30:00+00:00,743.1699829101562,743.1900024414062,742.3599853515625,742.5,390307
|
||||
2026-07-24 16:35:00+00:00,742.5150146484375,742.739990234375,742.22998046875,742.27001953125,210497
|
||||
2026-07-24 16:40:00+00:00,742.260009765625,742.260009765625,741.6400146484375,741.6500244140625,236074
|
||||
2026-07-24 16:45:00+00:00,741.6400146484375,741.8099975585938,740.9500122070312,741.1898803710938,405413
|
||||
2026-07-24 16:50:00+00:00,741.1900024414062,741.27001953125,740.739990234375,741.0700073242188,279526
|
||||
2026-07-24 16:55:00+00:00,741.0490112304688,741.760009765625,740.8699951171875,741.4500122070312,199361
|
||||
2026-07-24 17:00:00+00:00,741.469970703125,741.5999755859375,741.010009765625,741.1749877929688,182715
|
||||
2026-07-24 17:05:00+00:00,741.1699829101562,741.3300170898438,740.3800048828125,740.4182739257812,386540
|
||||
2026-07-24 17:10:00+00:00,740.4000244140625,741.0599975585938,740.2301025390625,740.9299926757812,255564
|
||||
2026-07-24 17:15:00+00:00,740.9500122070312,741.469970703125,740.6599731445312,741.4099731445312,184125
|
||||
2026-07-24 17:20:00+00:00,741.41650390625,741.489990234375,740.9072265625,741.2999877929688,327532
|
||||
2026-07-24 17:25:00+00:00,741.2899780273438,741.8400268554688,740.989990234375,741.7100219726562,196903
|
||||
2026-07-24 17:30:00+00:00,741.75,742.169189453125,741.530029296875,742.11181640625,156146
|
||||
2026-07-24 17:35:00+00:00,742.1099853515625,742.1199951171875,741.4299926757812,741.510009765625,162452
|
||||
2026-07-24 17:40:00+00:00,741.52001953125,741.5399780273438,741.0700073242188,741.0800170898438,184684
|
||||
2026-07-24 17:45:00+00:00,741.0700073242188,741.3900146484375,740.9199829101562,741.3099975585938,162181
|
||||
2026-07-24 17:50:00+00:00,741.3099975585938,741.5349731445312,740.969970703125,741.27001953125,270661
|
||||
2026-07-24 17:55:00+00:00,741.2899780273438,741.3900146484375,740.969970703125,740.97998046875,244499
|
||||
2026-07-24 18:00:00+00:00,740.989990234375,741.2899780273438,740.77001953125,740.8900146484375,188906
|
||||
2026-07-24 18:05:00+00:00,740.8800048828125,741.27001953125,740.75,740.969970703125,338674
|
||||
2026-07-24 18:10:00+00:00,741.02001953125,741.0750122070312,740.47998046875,740.5900268554688,257613
|
||||
2026-07-24 18:15:00+00:00,740.594970703125,740.6500244140625,740.1400146484375,740.22998046875,387838
|
||||
2026-07-24 18:20:00+00:00,740.1900024414062,740.2000122070312,739.719970703125,739.8099975585938,433579
|
||||
2026-07-24 18:25:00+00:00,739.7999877929688,739.8800048828125,739.0,739.0349731445312,473174
|
||||
2026-07-24 18:30:00+00:00,739.0499877929688,739.1599731445312,738.0700073242188,738.173828125,593684
|
||||
2026-07-24 18:35:00+00:00,738.1900024414062,738.6300048828125,737.8300170898438,738.27001953125,879485
|
||||
2026-07-24 18:40:00+00:00,738.27001953125,738.27001953125,737.6400146484375,737.8499755859375,439073
|
||||
2026-07-24 18:45:00+00:00,737.8699951171875,738.7000122070312,737.7899780273438,738.3099975585938,392359
|
||||
2026-07-24 18:50:00+00:00,738.2899780273438,738.5,738.030029296875,738.2050170898438,417134
|
||||
2026-07-24 18:55:00+00:00,738.1900024414062,738.7100219726562,738.1400146484375,738.3800048828125,302475
|
||||
2026-07-24 19:00:00+00:00,738.3900146484375,738.89501953125,738.2100219726562,738.280029296875,895698
|
||||
2026-07-24 19:05:00+00:00,738.25,738.6500244140625,737.5599975585938,737.60498046875,779209
|
||||
2026-07-24 19:10:00+00:00,737.6099853515625,738.22998046875,737.469970703125,737.5999755859375,705769
|
||||
2026-07-24 19:15:00+00:00,737.5999755859375,737.8300170898438,737.4099731445312,737.7999877929688,1036183
|
||||
2026-07-24 19:20:00+00:00,737.8200073242188,738.02001953125,737.2899780273438,737.4299926757812,521023
|
||||
2026-07-24 19:25:00+00:00,737.4400024414062,737.8699951171875,737.2999877929688,737.6500244140625,459031
|
||||
2026-07-24 19:30:00+00:00,737.719970703125,738.3800048828125,737.6599731445312,738.0900268554688,822160
|
||||
2026-07-24 19:35:00+00:00,738.0800170898438,738.6599731445312,737.989990234375,738.1099853515625,1074359
|
||||
2026-07-24 19:40:00+00:00,738.1099853515625,738.3400268554688,737.7899780273438,738.280029296875,800742
|
||||
2026-07-24 19:45:00+00:00,738.2999877929688,738.4400024414062,737.75,738.3399047851562,1069183
|
||||
2026-07-24 19:50:00+00:00,738.3400268554688,738.5800170898438,737.6400146484375,738.469970703125,1055109
|
||||
2026-07-24 19:55:00+00:00,738.489990234375,739.0999755859375,738.0499877929688,738.8599853515625,3826336
|
||||
2026-07-24 20:00:00+00:00,738.9299926757812,738.9299926757812,738.9299926757812,738.9299926757812,0
|
||||
|
@@ -0,0 +1,16 @@
|
||||
services:
|
||||
adaptive-barrier-webapp:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: webapp/Dockerfile
|
||||
container_name: adaptive-barrier-webapp
|
||||
restart: unless-stopped
|
||||
expose:
|
||||
- "8055"
|
||||
networks:
|
||||
- proxy
|
||||
|
||||
networks:
|
||||
proxy:
|
||||
external: true
|
||||
name: caddy
|
||||
@@ -0,0 +1,9 @@
|
||||
services:
|
||||
adaptive-barrier-webapp:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: webapp/Dockerfile
|
||||
container_name: adaptive-barrier-webapp
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "127.0.0.1:8055:8055"
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,39 @@
|
||||
[build-system]
|
||||
requires = ["setuptools>=69"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "adaptive-barrier-monitor"
|
||||
version = "0.4.7"
|
||||
description = "Brownian first-passage models and a state-dependent barrier-monitoring demo"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.11"
|
||||
dependencies = [
|
||||
"numpy>=1.26,<3",
|
||||
"scipy>=1.12,<2",
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
notebooks = [
|
||||
"matplotlib>=3.8,<4",
|
||||
"pandas>=2.2,<4",
|
||||
"sympy>=1.12,<2",
|
||||
"requests>=2.31,<3",
|
||||
"jupyterlab>=4,<5",
|
||||
]
|
||||
webapp = [
|
||||
"fastapi>=0.110,<1",
|
||||
"uvicorn[standard]>=0.27,<1",
|
||||
"pydantic>=2,<3",
|
||||
]
|
||||
dev = [
|
||||
"pytest>=8,<9",
|
||||
"httpx>=0.27,<1",
|
||||
]
|
||||
|
||||
[tool.setuptools.packages.find]
|
||||
where = ["src"]
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
addopts = "-q"
|
||||
testpaths = ["tests"]
|
||||
@@ -0,0 +1,2 @@
|
||||
# Tests plus all project extras
|
||||
-e .[notebooks,webapp,dev]
|
||||
@@ -0,0 +1,2 @@
|
||||
# Webapp runtime
|
||||
-e .[webapp]
|
||||
@@ -0,0 +1,2 @@
|
||||
# Notebook environment
|
||||
-e .[notebooks]
|
||||
@@ -0,0 +1,37 @@
|
||||
"""Public API for the Adaptive Barrier Monitor engine."""
|
||||
|
||||
__version__ = "0.4.7"
|
||||
|
||||
from adaptive_barrier.engine import (
|
||||
DROP_FRACTION,
|
||||
LOG_BARRIER,
|
||||
LOG_UPPER_BARRIER,
|
||||
RISE_FRACTION,
|
||||
TRADING_DAYS_PER_YEAR,
|
||||
TRADING_MINUTES_PER_YEAR,
|
||||
TYPICAL_ANNUAL_VOL,
|
||||
WINDOW_MINUTES,
|
||||
WINDOW_YEARS,
|
||||
adaptive_schedule,
|
||||
barrier_miss_prob,
|
||||
brownian_bridge,
|
||||
brownian_motion,
|
||||
brownian_motion_cholesky,
|
||||
detect_barrier_event,
|
||||
estimate_bridge_breach_prob,
|
||||
estimate_first_passage_prob,
|
||||
estimate_first_passage_prob_bb,
|
||||
first_passage_cdf,
|
||||
first_passage_cdf_zero_drift,
|
||||
fixed_cadence_indices,
|
||||
fixed_uniform_indices,
|
||||
geometric_brownian_motion,
|
||||
horizon_vol,
|
||||
max_safe_dt,
|
||||
merton_jump_diffusion,
|
||||
minutes_to_years,
|
||||
run_monte_carlo_simulation,
|
||||
time_grid,
|
||||
)
|
||||
|
||||
__all__ = [name for name in globals() if not name.startswith("_")]
|
||||
@@ -0,0 +1,781 @@
|
||||
"""Core stochastic-process and barrier-monitoring utilities.
|
||||
|
||||
The closed-form Brownian-motion results are exact under their stated diffusion
|
||||
assumptions. The adaptive schedule is deliberately described as a *local
|
||||
heuristic*: before the next observation is known, it substitutes the current
|
||||
barrier distance for both Brownian-bridge endpoint distances. Consequently,
|
||||
``eps`` is a design parameter, not an unconditional real-time miss guarantee,
|
||||
and it does not control jump risk.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Optional
|
||||
|
||||
import numpy as np
|
||||
from scipy.stats import norm
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Trading-time and volatility conventions
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
TRADING_DAYS_PER_YEAR = 252
|
||||
TRADING_HOURS_PER_DAY = 6.5
|
||||
TRADING_MINUTES_PER_HOUR = 60
|
||||
TRADING_MINUTES_PER_DAY = TRADING_HOURS_PER_DAY * TRADING_MINUTES_PER_HOUR
|
||||
TRADING_MINUTES_PER_YEAR = TRADING_DAYS_PER_YEAR * TRADING_MINUTES_PER_DAY
|
||||
|
||||
DROP_FRACTION = 0.10
|
||||
RISE_FRACTION = 0.10
|
||||
WINDOW_MINUTES = 5
|
||||
WINDOW_YEARS = WINDOW_MINUTES / TRADING_MINUTES_PER_YEAR
|
||||
LOG_BARRIER = float(np.log1p(-DROP_FRACTION))
|
||||
LOG_UPPER_BARRIER = float(np.log1p(RISE_FRACTION))
|
||||
TYPICAL_ANNUAL_VOL = 0.30
|
||||
|
||||
|
||||
def _require_positive(name: str, value: float) -> None:
|
||||
if not np.isfinite(value) or value <= 0:
|
||||
raise ValueError(f"{name} must be finite and positive")
|
||||
|
||||
|
||||
def _require_positive_int(name: str, value: int) -> None:
|
||||
if isinstance(value, bool) or int(value) != value or value <= 0:
|
||||
raise ValueError(f"{name} must be a positive integer")
|
||||
|
||||
|
||||
def minutes_to_years(minutes: float | np.ndarray) -> float | np.ndarray:
|
||||
"""Convert trading minutes to years."""
|
||||
values = np.asarray(minutes, dtype=float)
|
||||
if np.any(~np.isfinite(values)) or np.any(values < 0):
|
||||
raise ValueError("minutes must be finite and non-negative")
|
||||
result = values / TRADING_MINUTES_PER_YEAR
|
||||
return float(result) if result.ndim == 0 else result
|
||||
|
||||
|
||||
def horizon_vol(sigma_annual: float, minutes: float | np.ndarray) -> float | np.ndarray:
|
||||
"""Return the diffusion standard deviation over ``minutes`` of trading time."""
|
||||
_require_positive("sigma_annual", sigma_annual)
|
||||
result = sigma_annual * np.sqrt(minutes_to_years(minutes))
|
||||
return float(result) if np.ndim(result) == 0 else result
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Process samplers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _as_rng(rng: Optional[np.random.Generator] = None) -> np.random.Generator:
|
||||
return np.random.default_rng() if rng is None else rng
|
||||
|
||||
|
||||
def time_grid(T: float, n_steps: int) -> np.ndarray:
|
||||
"""Uniform grid from 0 to ``T`` with ``n_steps + 1`` points."""
|
||||
_require_positive("T", T)
|
||||
_require_positive_int("n_steps", n_steps)
|
||||
return np.linspace(0.0, T, int(n_steps) + 1)
|
||||
|
||||
|
||||
def brownian_motion(
|
||||
T: float,
|
||||
n_steps: int,
|
||||
n_paths: int = 1,
|
||||
drift: float = 0.0,
|
||||
sigma: float = 1.0,
|
||||
rng: Optional[np.random.Generator] = None,
|
||||
) -> tuple[np.ndarray, np.ndarray]:
|
||||
"""Sample ``dX = drift dt + sigma dW`` using independent Gaussian increments."""
|
||||
_require_positive("sigma", sigma)
|
||||
_require_positive_int("n_paths", n_paths)
|
||||
if not np.isfinite(drift):
|
||||
raise ValueError("drift must be finite")
|
||||
rng = _as_rng(rng)
|
||||
t = time_grid(T, n_steps)
|
||||
dt = T / n_steps
|
||||
increments = rng.normal(
|
||||
loc=drift * dt,
|
||||
scale=sigma * np.sqrt(dt),
|
||||
size=(int(n_paths), int(n_steps)),
|
||||
)
|
||||
paths = np.zeros((int(n_paths), int(n_steps) + 1))
|
||||
paths[:, 1:] = np.cumsum(increments, axis=1)
|
||||
return t, paths
|
||||
|
||||
|
||||
def brownian_motion_cholesky(
|
||||
T: float,
|
||||
n_steps: int,
|
||||
n_paths: int = 1,
|
||||
rng: Optional[np.random.Generator] = None,
|
||||
) -> tuple[np.ndarray, np.ndarray]:
|
||||
"""Sample standard Brownian motion from the ``min(s,t)`` covariance matrix."""
|
||||
_require_positive_int("n_paths", n_paths)
|
||||
rng = _as_rng(rng)
|
||||
t = time_grid(T, n_steps)
|
||||
inner = t[1:]
|
||||
covariance = np.minimum(inner[:, None], inner[None, :])
|
||||
factor = np.linalg.cholesky(covariance)
|
||||
normals = rng.standard_normal(size=(int(n_paths), int(n_steps)))
|
||||
paths = np.zeros((int(n_paths), int(n_steps) + 1))
|
||||
paths[:, 1:] = normals @ factor.T
|
||||
return t, paths
|
||||
|
||||
|
||||
def geometric_brownian_motion(
|
||||
S0: float,
|
||||
T: float,
|
||||
n_steps: int,
|
||||
n_paths: int = 1,
|
||||
mu: float = 0.0,
|
||||
sigma: float = 1.0,
|
||||
rng: Optional[np.random.Generator] = None,
|
||||
) -> tuple[np.ndarray, np.ndarray]:
|
||||
"""Sample GBM exactly in log space, without Euler discretisation error."""
|
||||
_require_positive("S0", S0)
|
||||
if not np.isfinite(mu):
|
||||
raise ValueError("mu must be finite")
|
||||
_require_positive("sigma", sigma)
|
||||
t, log_returns = brownian_motion(
|
||||
T,
|
||||
n_steps,
|
||||
n_paths,
|
||||
drift=mu - 0.5 * sigma**2,
|
||||
sigma=sigma,
|
||||
rng=rng,
|
||||
)
|
||||
return t, S0 * np.exp(log_returns)
|
||||
|
||||
|
||||
def brownian_bridge(
|
||||
T: float,
|
||||
n_steps: int,
|
||||
n_paths: int = 1,
|
||||
start: float = 0.0,
|
||||
end: float = 0.0,
|
||||
sigma: float = 1.0,
|
||||
rng: Optional[np.random.Generator] = None,
|
||||
) -> tuple[np.ndarray, np.ndarray]:
|
||||
"""Sample Brownian motion conditioned on its two endpoints."""
|
||||
if not np.isfinite(start) or not np.isfinite(end):
|
||||
raise ValueError("bridge endpoints must be finite")
|
||||
t, motion = brownian_motion(T, n_steps, n_paths, sigma=sigma, rng=rng)
|
||||
linear = start + (end - start) * (t / T)
|
||||
bridge = motion - np.outer(motion[:, -1], t / T) + linear[None, :]
|
||||
return t, bridge
|
||||
|
||||
|
||||
def merton_jump_diffusion(
|
||||
S0: float,
|
||||
T: float,
|
||||
n_steps: int,
|
||||
n_paths: int = 1,
|
||||
mu: float = 0.0,
|
||||
sigma: float = 0.2,
|
||||
jump_intensity: float = 0.0,
|
||||
jump_mean: float = -0.10,
|
||||
jump_sigma: float = 0.15,
|
||||
rng: Optional[np.random.Generator] = None,
|
||||
) -> tuple[np.ndarray, np.ndarray]:
|
||||
"""Sample a Merton jump diffusion with annualised Poisson intensity."""
|
||||
_require_positive("S0", S0)
|
||||
_require_positive("sigma", sigma)
|
||||
_require_positive_int("n_paths", n_paths)
|
||||
if not np.isfinite(mu):
|
||||
raise ValueError("mu must be finite")
|
||||
if not np.isfinite(jump_intensity) or jump_intensity < 0:
|
||||
raise ValueError("jump_intensity must be finite and non-negative")
|
||||
if not np.isfinite(jump_mean):
|
||||
raise ValueError("jump_mean must be finite")
|
||||
if not np.isfinite(jump_sigma) or jump_sigma < 0:
|
||||
raise ValueError("jump_sigma must be finite and non-negative")
|
||||
|
||||
rng = _as_rng(rng)
|
||||
dt = T / n_steps
|
||||
compensator = np.exp(jump_mean + 0.5 * jump_sigma**2) - 1.0
|
||||
log_drift = mu - 0.5 * sigma**2 - jump_intensity * compensator
|
||||
t, diffusion = brownian_motion(
|
||||
T,
|
||||
n_steps,
|
||||
n_paths,
|
||||
drift=log_drift,
|
||||
sigma=sigma,
|
||||
rng=rng,
|
||||
)
|
||||
|
||||
counts = rng.poisson(jump_intensity * dt, size=(int(n_paths), int(n_steps)))
|
||||
jump_log = np.zeros((int(n_paths), int(n_steps)))
|
||||
max_count = int(counts.max()) if counts.size else 0
|
||||
if max_count:
|
||||
log_jumps = rng.normal(
|
||||
jump_mean,
|
||||
jump_sigma,
|
||||
size=(int(n_paths), int(n_steps), max_count),
|
||||
)
|
||||
mask = np.arange(max_count) < counts[:, :, None]
|
||||
jump_log = (log_jumps * mask).sum(axis=2)
|
||||
|
||||
cumulative_jumps = np.zeros((int(n_paths), int(n_steps) + 1))
|
||||
cumulative_jumps[:, 1:] = np.cumsum(jump_log, axis=1)
|
||||
return t, S0 * np.exp(diffusion + cumulative_jumps)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Exact diffusion barrier formulae
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def first_passage_cdf_zero_drift(B: float, T: float, sigma: float) -> float:
|
||||
"""Return ``P(inf_{s<=T} X_s <= B)`` for zero-drift BM started at zero.
|
||||
|
||||
``B`` must be negative. For extremely remote barriers the floating-point
|
||||
result can underflow to zero; this correctly means "below machine precision".
|
||||
"""
|
||||
if not np.isfinite(B) or B >= 0:
|
||||
raise ValueError("B must be a finite negative lower barrier")
|
||||
_require_positive("T", T)
|
||||
_require_positive("sigma", sigma)
|
||||
return float(2.0 * norm.cdf(B / (sigma * np.sqrt(T))))
|
||||
|
||||
|
||||
def first_passage_cdf(B: float, T: float, nu: float, sigma: float) -> float:
|
||||
"""Bachelier--Lévy lower-barrier CDF for drifted BM started at zero."""
|
||||
if not np.isfinite(B) or B >= 0:
|
||||
raise ValueError("B must be a finite negative lower barrier")
|
||||
_require_positive("T", T)
|
||||
_require_positive("sigma", sigma)
|
||||
if not np.isfinite(nu):
|
||||
raise ValueError("nu must be finite")
|
||||
scale = sigma * np.sqrt(T)
|
||||
probability = norm.cdf((B - nu * T) / scale) + np.exp(
|
||||
2.0 * nu * B / sigma**2
|
||||
) * norm.cdf((B + nu * T) / scale)
|
||||
return float(np.clip(probability, 0.0, 1.0))
|
||||
|
||||
|
||||
def barrier_miss_prob(
|
||||
x0: float,
|
||||
xT: float,
|
||||
B: float,
|
||||
sigma: float,
|
||||
dt: float,
|
||||
) -> float:
|
||||
"""Conditional Brownian-bridge probability of crossing a lower barrier."""
|
||||
for name, value in (("x0", x0), ("xT", xT), ("B", B)):
|
||||
if not np.isfinite(value):
|
||||
raise ValueError(f"{name} must be finite")
|
||||
_require_positive("sigma", sigma)
|
||||
_require_positive("dt", dt)
|
||||
if x0 <= B or xT <= B:
|
||||
return 1.0
|
||||
exponent = -2.0 * (x0 - B) * (xT - B) / (sigma**2 * dt)
|
||||
return float(np.exp(exponent))
|
||||
|
||||
|
||||
def max_safe_dt(
|
||||
D: float | np.ndarray,
|
||||
sigma: float,
|
||||
epsilon: float,
|
||||
) -> float | np.ndarray:
|
||||
"""Invert the symmetric-endpoint bridge formula for ``dt``.
|
||||
|
||||
This is exact *conditional on both endpoint distances being ``D``*. In a
|
||||
live scheduler the future endpoint is unknown, so using the current distance
|
||||
for both endpoints is a local design approximation rather than a guarantee.
|
||||
"""
|
||||
_require_positive("sigma", sigma)
|
||||
if not np.isfinite(epsilon) or not 0.0 < epsilon < 1.0:
|
||||
raise ValueError("epsilon must lie strictly between 0 and 1")
|
||||
distances = np.asarray(D, dtype=float)
|
||||
if np.any(~np.isfinite(distances)) or np.any(distances < 0):
|
||||
raise ValueError("D must be finite and non-negative")
|
||||
result = 2.0 * distances**2 / (sigma**2 * np.log(1.0 / epsilon))
|
||||
return float(result) if result.ndim == 0 else result
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Monte Carlo estimators used by the notebooks
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def estimate_first_passage_prob(
|
||||
B: float,
|
||||
T: float,
|
||||
nu: float,
|
||||
sigma: float,
|
||||
n_paths: int,
|
||||
n_steps: int,
|
||||
rng: Optional[np.random.Generator] = None,
|
||||
) -> float:
|
||||
"""Naive grid estimator; it has downward discretisation bias."""
|
||||
_, paths = brownian_motion(T, n_steps, n_paths, drift=nu, sigma=sigma, rng=rng)
|
||||
return float(np.mean(np.any(paths <= B, axis=1)))
|
||||
|
||||
|
||||
def estimate_first_passage_prob_bb(
|
||||
B: float,
|
||||
T: float,
|
||||
nu: float,
|
||||
sigma: float,
|
||||
n_paths: int,
|
||||
n_steps: int,
|
||||
rng: Optional[np.random.Generator] = None,
|
||||
) -> float:
|
||||
"""Brownian-bridge-corrected first-passage Monte Carlo estimator."""
|
||||
rng = _as_rng(rng)
|
||||
_, paths = brownian_motion(T, n_steps, n_paths, drift=nu, sigma=sigma, rng=rng)
|
||||
dt = T / n_steps
|
||||
x0, x1 = paths[:, :-1], paths[:, 1:]
|
||||
both_above = (x0 > B) & (x1 > B)
|
||||
probabilities = np.where(
|
||||
both_above,
|
||||
np.exp(-2.0 * (x0 - B) * (x1 - B) / (sigma**2 * dt)),
|
||||
1.0,
|
||||
)
|
||||
uniforms = rng.uniform(size=probabilities.shape)
|
||||
return float(np.mean(np.any(uniforms < probabilities, axis=1)))
|
||||
|
||||
|
||||
def estimate_bridge_breach_prob(
|
||||
x0: float,
|
||||
xT: float,
|
||||
B: float,
|
||||
sigma: float,
|
||||
dt: float,
|
||||
n_paths: int,
|
||||
n_inner: int,
|
||||
rng: Optional[np.random.Generator] = None,
|
||||
) -> float:
|
||||
"""Monte Carlo check of :func:`barrier_miss_prob`."""
|
||||
_, paths = brownian_bridge(
|
||||
dt,
|
||||
n_inner,
|
||||
n_paths,
|
||||
start=x0,
|
||||
end=xT,
|
||||
sigma=sigma,
|
||||
rng=rng,
|
||||
)
|
||||
return float(np.mean(np.any(paths <= B, axis=1)))
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Adaptive schedule and detector evaluation
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def adaptive_schedule(
|
||||
t: np.ndarray,
|
||||
X: np.ndarray,
|
||||
B: float,
|
||||
sigma: float,
|
||||
eps: float,
|
||||
dt_cap: Optional[float] = None,
|
||||
B_upper: Optional[float] = None,
|
||||
) -> np.ndarray:
|
||||
"""Choose sample indices from a pre-generated path using a local proxy.
|
||||
|
||||
``t`` and ``sigma`` must use matching units: when ``t`` is measured in
|
||||
minutes, ``sigma`` must be the diffusion standard deviation per
|
||||
``sqrt(minute)``. At each observation, the current distance is substituted
|
||||
for both bridge endpoint distances in :func:`max_safe_dt`.
|
||||
"""
|
||||
times = np.asarray(t, dtype=float).ravel()
|
||||
values = np.asarray(X, dtype=float)
|
||||
if values.ndim > 1:
|
||||
if values.shape[0] != 1:
|
||||
raise ValueError("X must be one-dimensional or contain one path")
|
||||
values = values[0]
|
||||
values = values.ravel()
|
||||
if len(times) != len(values) or len(times) < 2:
|
||||
raise ValueError("t and X must have the same length of at least two")
|
||||
if np.any(~np.isfinite(times)) or np.any(np.diff(times) <= 0):
|
||||
raise ValueError("t must be finite and strictly increasing")
|
||||
if np.any(~np.isfinite(values)) or not np.isfinite(B):
|
||||
raise ValueError("X and B must be finite")
|
||||
_require_positive("sigma", sigma)
|
||||
if not 0.0 < eps < 1.0:
|
||||
raise ValueError("eps must lie strictly between 0 and 1")
|
||||
if dt_cap is not None:
|
||||
_require_positive("dt_cap", dt_cap)
|
||||
if B_upper is not None and (not np.isfinite(B_upper) or B_upper <= B):
|
||||
raise ValueError("B_upper must be finite and greater than B")
|
||||
|
||||
indices = [0]
|
||||
i = 0
|
||||
while i < len(times) - 1:
|
||||
lower_distance = values[i] - B
|
||||
distance = lower_distance
|
||||
if B_upper is not None:
|
||||
distance = min(lower_distance, B_upper - values[i])
|
||||
interval = max_safe_dt(max(float(distance), 1e-12), sigma, eps)
|
||||
if dt_cap is not None:
|
||||
interval = min(interval, dt_cap)
|
||||
target = times[i] + interval
|
||||
j = int(np.searchsorted(times, target, side="left"))
|
||||
j = min(max(j, i + 1), len(times) - 1)
|
||||
indices.append(j)
|
||||
i = j
|
||||
return np.asarray(indices, dtype=int)
|
||||
|
||||
|
||||
def fixed_uniform_indices(n: int, k: int) -> np.ndarray:
|
||||
"""Return exactly ``k`` approximately uniform indices from ``0`` to ``n-1``."""
|
||||
_require_positive_int("n", n)
|
||||
_require_positive_int("k", k)
|
||||
if k > n:
|
||||
raise ValueError("k cannot exceed n")
|
||||
if k == 1:
|
||||
return np.array([0], dtype=int)
|
||||
return np.rint(np.linspace(0, n - 1, k)).astype(int)
|
||||
|
||||
|
||||
def fixed_cadence_indices(t: np.ndarray, cadence: float) -> np.ndarray:
|
||||
"""Return grid indices for a fixed monitoring cadence.
|
||||
|
||||
The first and final grid points are always included. Intermediate target
|
||||
times are mapped to the first available grid point at or after each cadence
|
||||
tick. If the requested cadence is finer than the simulation grid, the
|
||||
resulting schedule is limited to one observation per grid point.
|
||||
"""
|
||||
times = np.asarray(t, dtype=float).ravel()
|
||||
if times.size < 2:
|
||||
raise ValueError("t must contain at least two points")
|
||||
if np.any(~np.isfinite(times)) or np.any(np.diff(times) <= 0):
|
||||
raise ValueError("t must be finite and strictly increasing")
|
||||
_require_positive("cadence", cadence)
|
||||
|
||||
targets = np.arange(times[0], times[-1] + cadence, cadence, dtype=float)
|
||||
targets = targets[targets <= times[-1] + 1e-12]
|
||||
indices = np.searchsorted(times, targets, side="left")
|
||||
indices = np.clip(indices, 0, len(times) - 1)
|
||||
indices = np.unique(indices.astype(int))
|
||||
if indices[0] != 0:
|
||||
indices = np.insert(indices, 0, 0)
|
||||
if indices[-1] != len(times) - 1:
|
||||
indices = np.append(indices, len(times) - 1)
|
||||
return indices
|
||||
|
||||
|
||||
def detect_barrier_event(
|
||||
sample_indices: np.ndarray,
|
||||
values: np.ndarray,
|
||||
breach_idx: Optional[int],
|
||||
barrier: float,
|
||||
direction: str,
|
||||
max_lag_steps: Optional[int] = None,
|
||||
) -> tuple[bool, Optional[int], Optional[int]]:
|
||||
"""Check whether a sampled point confirms a barrier event in time.
|
||||
|
||||
Returns ``(detected, lag_steps, detection_index)``. A detection must still
|
||||
lie beyond the barrier. When ``max_lag_steps`` is set, later observations
|
||||
do not count as catching the original event.
|
||||
"""
|
||||
if breach_idx is None:
|
||||
return False, None, None
|
||||
indices = np.asarray(sample_indices, dtype=int).ravel()
|
||||
path = np.asarray(values, dtype=float).ravel()
|
||||
if direction not in {"down", "up"}:
|
||||
raise ValueError("direction must be 'down' or 'up'")
|
||||
if max_lag_steps is not None:
|
||||
if isinstance(max_lag_steps, bool) or int(max_lag_steps) != max_lag_steps or max_lag_steps < 0:
|
||||
raise ValueError("max_lag_steps must be a non-negative integer or None")
|
||||
deadline = breach_idx + int(max_lag_steps)
|
||||
else:
|
||||
deadline = len(path) - 1
|
||||
|
||||
for sample_idx in indices:
|
||||
if sample_idx < breach_idx:
|
||||
continue
|
||||
if sample_idx > deadline:
|
||||
break
|
||||
beyond = path[sample_idx] <= barrier if direction == "down" else path[sample_idx] >= barrier
|
||||
if beyond:
|
||||
return True, int(sample_idx - breach_idx), int(sample_idx)
|
||||
return False, None, None
|
||||
|
||||
|
||||
def run_monte_carlo_simulation(
|
||||
S0: float = 100.0,
|
||||
sigma_annual: float = 0.30,
|
||||
mu_annual: float = 0.07,
|
||||
window_minutes: float = 1950.0,
|
||||
n_paths: int = 20,
|
||||
n_steps: int = 500,
|
||||
use_jumps: bool = False,
|
||||
jump_intensity: float = 25.0,
|
||||
jump_mean: float = -0.02,
|
||||
jump_sigma: float = 0.05,
|
||||
eps: float = 1e-3,
|
||||
dt_cap_minutes: Optional[float] = None,
|
||||
rng_seed: Optional[int] = None,
|
||||
drop_fraction: float = DROP_FRACTION,
|
||||
rise_fraction: float = RISE_FRACTION,
|
||||
max_detection_lag_steps: Optional[int] = 3,
|
||||
comparison_mode: str = "equal_budget",
|
||||
fixed_cadence_minutes: float = 60.0,
|
||||
) -> dict:
|
||||
"""Compare adaptive and fixed samplers on identical simulated paths.
|
||||
|
||||
``comparison_mode='equal_budget'`` gives the fixed baseline exactly the
|
||||
adaptive schedule's sample count on each path, isolating placement quality.
|
||||
``comparison_mode='fixed_cadence'`` samples independently at the requested
|
||||
cadence, exposing the detection-versus-observation-cost trade-off. Lower and
|
||||
upper breaches are evaluated independently, and a detection must occur
|
||||
within ``max_detection_lag_steps`` grid steps when that limit is not ``None``.
|
||||
"""
|
||||
_require_positive("S0", S0)
|
||||
_require_positive("sigma_annual", sigma_annual)
|
||||
_require_positive("window_minutes", window_minutes)
|
||||
_require_positive_int("n_paths", n_paths)
|
||||
_require_positive_int("n_steps", n_steps)
|
||||
if not np.isfinite(mu_annual):
|
||||
raise ValueError("mu_annual must be finite")
|
||||
if not 0.0 < drop_fraction < 1.0:
|
||||
raise ValueError("drop_fraction must lie between 0 and 1")
|
||||
if rise_fraction <= 0 or not np.isfinite(rise_fraction):
|
||||
raise ValueError("rise_fraction must be finite and positive")
|
||||
if max_detection_lag_steps is not None:
|
||||
if (
|
||||
isinstance(max_detection_lag_steps, bool)
|
||||
or int(max_detection_lag_steps) != max_detection_lag_steps
|
||||
or max_detection_lag_steps < 0
|
||||
):
|
||||
raise ValueError("max_detection_lag_steps must be a non-negative integer or None")
|
||||
if comparison_mode not in {"equal_budget", "fixed_cadence"}:
|
||||
raise ValueError("comparison_mode must be 'equal_budget' or 'fixed_cadence'")
|
||||
_require_positive("fixed_cadence_minutes", fixed_cadence_minutes)
|
||||
|
||||
rng = np.random.default_rng(rng_seed)
|
||||
horizon_years = minutes_to_years(window_minutes)
|
||||
if use_jumps:
|
||||
t_years, prices = merton_jump_diffusion(
|
||||
S0,
|
||||
horizon_years,
|
||||
n_steps,
|
||||
n_paths,
|
||||
mu=mu_annual,
|
||||
sigma=sigma_annual,
|
||||
jump_intensity=jump_intensity,
|
||||
jump_mean=jump_mean,
|
||||
jump_sigma=jump_sigma,
|
||||
rng=rng,
|
||||
)
|
||||
else:
|
||||
t_years, prices = geometric_brownian_motion(
|
||||
S0,
|
||||
horizon_years,
|
||||
n_steps,
|
||||
n_paths,
|
||||
mu=mu_annual,
|
||||
sigma=sigma_annual,
|
||||
rng=rng,
|
||||
)
|
||||
|
||||
times_minutes = np.asarray(t_years) * TRADING_MINUTES_PER_YEAR
|
||||
if dt_cap_minutes is None:
|
||||
dt_cap_minutes = max(window_minutes / 15.0, 5.0)
|
||||
_require_positive("dt_cap_minutes", dt_cap_minutes)
|
||||
|
||||
lower_price = float(S0 * (1.0 - drop_fraction))
|
||||
upper_price = float(S0 * (1.0 + rise_fraction))
|
||||
sigma_per_sqrt_minute = horizon_vol(sigma_annual, 1.0)
|
||||
|
||||
path_records: list[dict] = []
|
||||
adaptive_lags_steps: list[int] = []
|
||||
fixed_lags_steps: list[int] = []
|
||||
adaptive_lags_minutes: list[float] = []
|
||||
fixed_lags_minutes: list[float] = []
|
||||
adaptive_lags_by_direction: dict[str, list[int]] = {"lower": [], "upper": []}
|
||||
fixed_lags_by_direction: dict[str, list[int]] = {"lower": [], "upper": []}
|
||||
counts = {
|
||||
"lower_events": 0,
|
||||
"upper_events": 0,
|
||||
"adaptive_lower": 0,
|
||||
"adaptive_upper": 0,
|
||||
"fixed_lower": 0,
|
||||
"fixed_upper": 0,
|
||||
}
|
||||
|
||||
for path_prices in prices:
|
||||
log_relative = np.log(path_prices / path_prices[0])
|
||||
lower_log = float(np.log1p(-drop_fraction))
|
||||
upper_log = float(np.log1p(rise_fraction))
|
||||
adaptive_idx = adaptive_schedule(
|
||||
times_minutes,
|
||||
log_relative,
|
||||
lower_log,
|
||||
sigma_per_sqrt_minute,
|
||||
eps,
|
||||
dt_cap=dt_cap_minutes,
|
||||
B_upper=upper_log,
|
||||
)
|
||||
if comparison_mode == "equal_budget":
|
||||
fixed_idx = fixed_uniform_indices(len(times_minutes), len(adaptive_idx))
|
||||
else:
|
||||
fixed_idx = fixed_cadence_indices(times_minutes, fixed_cadence_minutes)
|
||||
|
||||
below = np.flatnonzero(path_prices <= lower_price)
|
||||
above = np.flatnonzero(path_prices >= upper_price)
|
||||
lower_breach_idx = int(below[0]) if below.size else None
|
||||
upper_breach_idx = int(above[0]) if above.size else None
|
||||
|
||||
lower_adapt = detect_barrier_event(
|
||||
adaptive_idx,
|
||||
path_prices,
|
||||
lower_breach_idx,
|
||||
lower_price,
|
||||
"down",
|
||||
max_detection_lag_steps,
|
||||
)
|
||||
upper_adapt = detect_barrier_event(
|
||||
adaptive_idx,
|
||||
path_prices,
|
||||
upper_breach_idx,
|
||||
upper_price,
|
||||
"up",
|
||||
max_detection_lag_steps,
|
||||
)
|
||||
lower_fixed = detect_barrier_event(
|
||||
fixed_idx,
|
||||
path_prices,
|
||||
lower_breach_idx,
|
||||
lower_price,
|
||||
"down",
|
||||
max_detection_lag_steps,
|
||||
)
|
||||
upper_fixed = detect_barrier_event(
|
||||
fixed_idx,
|
||||
path_prices,
|
||||
upper_breach_idx,
|
||||
upper_price,
|
||||
"up",
|
||||
max_detection_lag_steps,
|
||||
)
|
||||
|
||||
event_specs = [
|
||||
("lower", lower_breach_idx, lower_adapt, lower_fixed),
|
||||
("upper", upper_breach_idx, upper_adapt, upper_fixed),
|
||||
]
|
||||
for label, breach_idx, adaptive_result, fixed_result in event_specs:
|
||||
if breach_idx is None:
|
||||
continue
|
||||
counts[f"{label}_events"] += 1
|
||||
if adaptive_result[0]:
|
||||
counts[f"adaptive_{label}"] += 1
|
||||
adaptive_lags_steps.append(adaptive_result[1])
|
||||
adaptive_lags_by_direction[label].append(adaptive_result[1])
|
||||
adaptive_lags_minutes.append(
|
||||
float(times_minutes[adaptive_result[2]] - times_minutes[breach_idx])
|
||||
)
|
||||
if fixed_result[0]:
|
||||
counts[f"fixed_{label}"] += 1
|
||||
fixed_lags_steps.append(fixed_result[1])
|
||||
fixed_lags_by_direction[label].append(fixed_result[1])
|
||||
fixed_lags_minutes.append(
|
||||
float(times_minutes[fixed_result[2]] - times_minutes[breach_idx])
|
||||
)
|
||||
|
||||
first_candidates = [
|
||||
(idx, direction)
|
||||
for idx, direction in ((lower_breach_idx, "down"), (upper_breach_idx, "up"))
|
||||
if idx is not None
|
||||
]
|
||||
if first_candidates:
|
||||
first_breach_idx, first_breach_dir = min(first_candidates, key=lambda item: item[0])
|
||||
else:
|
||||
first_breach_idx, first_breach_dir = None, None
|
||||
|
||||
if first_breach_dir == "down":
|
||||
first_adapt, first_fixed = lower_adapt, lower_fixed
|
||||
elif first_breach_dir == "up":
|
||||
first_adapt, first_fixed = upper_adapt, upper_fixed
|
||||
else:
|
||||
first_adapt = first_fixed = (False, None, None)
|
||||
|
||||
path_records.append(
|
||||
{
|
||||
"prices": path_prices.tolist(),
|
||||
"log_prices": log_relative.tolist(),
|
||||
"sample_indices": adaptive_idx.tolist(),
|
||||
"sample_times": times_minutes[adaptive_idx].tolist(),
|
||||
"sample_prices": path_prices[adaptive_idx].tolist(),
|
||||
"fixed_sample_indices": fixed_idx.tolist(),
|
||||
"fixed_sample_times": times_minutes[fixed_idx].tolist(),
|
||||
"fixed_sample_prices": path_prices[fixed_idx].tolist(),
|
||||
"breach_idx": first_breach_idx,
|
||||
"breach_dir": first_breach_dir,
|
||||
"lower_breach_idx": lower_breach_idx,
|
||||
"upper_breach_idx": upper_breach_idx,
|
||||
"adaptive_detected": first_adapt[0],
|
||||
"fixed_detected": first_fixed[0],
|
||||
"adaptive_detection_lag": first_adapt[1],
|
||||
"fixed_detection_lag": first_fixed[1],
|
||||
"adaptive_lower_detected": lower_adapt[0],
|
||||
"adaptive_upper_detected": upper_adapt[0],
|
||||
"fixed_lower_detected": lower_fixed[0],
|
||||
"fixed_upper_detected": upper_fixed[0],
|
||||
"adaptive_lower_detection_lag": lower_adapt[1],
|
||||
"adaptive_upper_detection_lag": upper_adapt[1],
|
||||
"fixed_lower_detection_lag": lower_fixed[1],
|
||||
"fixed_upper_detection_lag": upper_fixed[1],
|
||||
}
|
||||
)
|
||||
|
||||
adaptive_total_samples = sum(len(record["sample_indices"]) for record in path_records)
|
||||
fixed_total_samples = sum(len(record["fixed_sample_indices"]) for record in path_records)
|
||||
n_events = counts["lower_events"] + counts["upper_events"]
|
||||
n_paths_with_any_breach = sum(record["breach_idx"] is not None for record in path_records)
|
||||
adaptive_detections = counts["adaptive_lower"] + counts["adaptive_upper"]
|
||||
fixed_detections = counts["fixed_lower"] + counts["fixed_upper"]
|
||||
|
||||
def _mean(values: list[float | int]) -> Optional[float]:
|
||||
return float(np.mean(values)) if values else None
|
||||
|
||||
return {
|
||||
"paths": path_records,
|
||||
"times_minutes": times_minutes.tolist(),
|
||||
"lower_barrier_price": lower_price,
|
||||
"upper_barrier_price": upper_price,
|
||||
"S0": S0,
|
||||
"sigma_annual": sigma_annual,
|
||||
"mu_annual": mu_annual,
|
||||
"window_minutes": window_minutes,
|
||||
"use_jumps": use_jumps,
|
||||
"eps": eps,
|
||||
"drop_fraction": float(drop_fraction),
|
||||
"rise_fraction": float(rise_fraction),
|
||||
"dt_cap_minutes": float(dt_cap_minutes),
|
||||
"max_detection_lag_steps": max_detection_lag_steps,
|
||||
"comparison_mode": comparison_mode,
|
||||
"fixed_cadence_minutes": float(fixed_cadence_minutes),
|
||||
"grid_step_minutes": float(times_minutes[1] - times_minutes[0]),
|
||||
"n_paths": n_paths,
|
||||
"n_paths_with_any_breach": n_paths_with_any_breach,
|
||||
"n_barrier_events": n_events,
|
||||
"n_lower_events": counts["lower_events"],
|
||||
"n_upper_events": counts["upper_events"],
|
||||
# Backward-compatible aliases retained for existing clients.
|
||||
"n_breaches": n_events,
|
||||
"n_lower_breaches": counts["lower_events"],
|
||||
"n_upper_breaches": counts["upper_events"],
|
||||
"adaptive_detections": adaptive_detections,
|
||||
"adaptive_lower_detections": counts["adaptive_lower"],
|
||||
"adaptive_upper_detections": counts["adaptive_upper"],
|
||||
"fixed_detections": fixed_detections,
|
||||
"fixed_lower_detections": counts["fixed_lower"],
|
||||
"fixed_upper_detections": counts["fixed_upper"],
|
||||
"adaptive_total_samples": adaptive_total_samples,
|
||||
"fixed_total_samples": fixed_total_samples,
|
||||
"mean_detection_lag": _mean(adaptive_lags_steps),
|
||||
"mean_lower_detection_lag": _mean(adaptive_lags_by_direction["lower"]),
|
||||
"mean_upper_detection_lag": _mean(adaptive_lags_by_direction["upper"]),
|
||||
"mean_fixed_detection_lag": _mean(fixed_lags_steps),
|
||||
"mean_fixed_lower_detection_lag": _mean(fixed_lags_by_direction["lower"]),
|
||||
"mean_fixed_upper_detection_lag": _mean(fixed_lags_by_direction["upper"]),
|
||||
"mean_detection_lag_minutes": _mean(adaptive_lags_minutes),
|
||||
"mean_fixed_detection_lag_minutes": _mean(fixed_lags_minutes),
|
||||
"model_scope": (
|
||||
"eps controls a local Brownian-diffusion scheduling proxy; it is not an "
|
||||
"unconditional miss guarantee and does not control jump risk."
|
||||
),
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
"""Test configuration for running directly from a source checkout."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
SRC_DIR = Path(__file__).resolve().parents[1] / "src"
|
||||
if str(SRC_DIR) not in sys.path:
|
||||
sys.path.insert(0, str(SRC_DIR))
|
||||
@@ -0,0 +1,146 @@
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from adaptive_barrier.engine import (
|
||||
TRADING_MINUTES_PER_YEAR,
|
||||
adaptive_schedule,
|
||||
barrier_miss_prob,
|
||||
detect_barrier_event,
|
||||
fixed_uniform_indices,
|
||||
geometric_brownian_motion,
|
||||
horizon_vol,
|
||||
max_safe_dt,
|
||||
merton_jump_diffusion,
|
||||
run_monte_carlo_simulation,
|
||||
)
|
||||
|
||||
|
||||
def test_symmetric_bridge_inversion_matches_epsilon():
|
||||
distance = 0.04
|
||||
sigma = 0.01
|
||||
epsilon = 1e-3
|
||||
dt = max_safe_dt(distance, sigma, epsilon)
|
||||
assert barrier_miss_prob(distance, distance, 0.0, sigma, dt) == pytest.approx(epsilon)
|
||||
|
||||
|
||||
def test_vectorized_safe_interval_and_validation():
|
||||
result = max_safe_dt(np.array([0.01, 0.02]), 0.1, 1e-3)
|
||||
assert result[1] == pytest.approx(4.0 * result[0])
|
||||
with pytest.raises(ValueError):
|
||||
max_safe_dt(-0.01, 0.1, 1e-3)
|
||||
|
||||
|
||||
def test_adaptive_schedule_uses_matching_minute_units():
|
||||
times = np.linspace(0.0, 10.0, 101)
|
||||
path = np.zeros_like(times)
|
||||
sigma_per_sqrt_minute = horizon_vol(0.30, 1.0)
|
||||
indices = adaptive_schedule(
|
||||
times,
|
||||
path,
|
||||
np.log(0.9),
|
||||
sigma_per_sqrt_minute,
|
||||
1e-3,
|
||||
dt_cap=2.0,
|
||||
)
|
||||
assert np.array_equal(indices, np.array([0, 20, 40, 60, 80, 100]))
|
||||
|
||||
|
||||
def test_detection_deadline_is_enforced():
|
||||
values = np.array([100.0, 89.0, 88.0, 87.0, 86.0])
|
||||
samples = np.array([0, 4])
|
||||
assert detect_barrier_event(samples, values, 1, 90.0, "down", 2) == (False, None, None)
|
||||
assert detect_barrier_event(samples, values, 1, 90.0, "down", 3) == (True, 3, 4)
|
||||
|
||||
|
||||
def test_fixed_indices_have_exact_requested_count():
|
||||
for n in (2, 10, 101):
|
||||
for k in range(1, n + 1):
|
||||
indices = fixed_uniform_indices(n, k)
|
||||
assert len(indices) == k
|
||||
assert len(np.unique(indices)) == k
|
||||
assert indices[0] == 0
|
||||
if k > 1:
|
||||
assert indices[-1] == n - 1
|
||||
|
||||
|
||||
def test_zero_jump_intensity_matches_gbm_for_same_seed():
|
||||
kwargs = dict(S0=100.0, T=5 / TRADING_MINUTES_PER_YEAR, n_steps=100, n_paths=3, mu=0.05, sigma=0.2)
|
||||
_, gbm = geometric_brownian_motion(**kwargs, rng=np.random.default_rng(9))
|
||||
_, jump = merton_jump_diffusion(
|
||||
**kwargs,
|
||||
jump_intensity=0.0,
|
||||
rng=np.random.default_rng(9),
|
||||
)
|
||||
assert np.array_equal(gbm, jump)
|
||||
|
||||
|
||||
def test_simulation_baseline_has_exact_per_path_budget():
|
||||
result = run_monte_carlo_simulation(
|
||||
n_paths=12,
|
||||
n_steps=200,
|
||||
rng_seed=7,
|
||||
drop_fraction=0.05,
|
||||
rise_fraction=0.08,
|
||||
)
|
||||
assert result["adaptive_total_samples"] == result["fixed_total_samples"]
|
||||
for path in result["paths"]:
|
||||
assert len(path["sample_indices"]) == len(path["fixed_sample_indices"])
|
||||
|
||||
|
||||
def test_simulation_lags_respect_deadline():
|
||||
deadline = 3
|
||||
result = run_monte_carlo_simulation(
|
||||
n_paths=20,
|
||||
n_steps=300,
|
||||
rng_seed=3,
|
||||
drop_fraction=0.04,
|
||||
rise_fraction=0.06,
|
||||
max_detection_lag_steps=deadline,
|
||||
)
|
||||
for path in result["paths"]:
|
||||
for key in (
|
||||
"adaptive_lower_detection_lag",
|
||||
"adaptive_upper_detection_lag",
|
||||
"fixed_lower_detection_lag",
|
||||
"fixed_upper_detection_lag",
|
||||
):
|
||||
lag = path[key]
|
||||
assert lag is None or 0 <= lag <= deadline
|
||||
|
||||
|
||||
def test_simulation_exposes_unambiguous_event_counts():
|
||||
result = run_monte_carlo_simulation(
|
||||
n_paths=8,
|
||||
n_steps=150,
|
||||
rng_seed=11,
|
||||
drop_fraction=0.04,
|
||||
rise_fraction=0.06,
|
||||
)
|
||||
assert result["n_barrier_events"] == result["n_lower_events"] + result["n_upper_events"]
|
||||
assert result["n_breaches"] == result["n_barrier_events"]
|
||||
assert 0 <= result["n_paths_with_any_breach"] <= result["n_paths"]
|
||||
|
||||
|
||||
def test_fixed_cadence_indices_include_window_end_and_respect_grid():
|
||||
from adaptive_barrier.engine import fixed_cadence_indices
|
||||
|
||||
times = np.linspace(0.0, 100.0, 11)
|
||||
indices = fixed_cadence_indices(times, 25.0)
|
||||
assert np.array_equal(indices, np.array([0, 3, 5, 8, 10]))
|
||||
|
||||
|
||||
def test_fixed_cadence_mode_uses_independent_sample_count():
|
||||
result = run_monte_carlo_simulation(
|
||||
n_paths=4,
|
||||
n_steps=200,
|
||||
window_minutes=200.0,
|
||||
dt_cap_minutes=2.0,
|
||||
comparison_mode="fixed_cadence",
|
||||
fixed_cadence_minutes=20.0,
|
||||
rng_seed=5,
|
||||
)
|
||||
assert result["comparison_mode"] == "fixed_cadence"
|
||||
assert result["fixed_cadence_minutes"] == 20.0
|
||||
assert result["adaptive_total_samples"] != result["fixed_total_samples"]
|
||||
fixed_counts = {len(path["fixed_sample_indices"]) for path in result["paths"]}
|
||||
assert fixed_counts == {11}
|
||||
@@ -0,0 +1,82 @@
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from webapp.app import app
|
||||
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
|
||||
def test_health_endpoint():
|
||||
response = client.get("/api/health")
|
||||
assert response.status_code == 200
|
||||
payload = response.json()
|
||||
assert payload["ok"] is True
|
||||
assert payload["version"] == "0.4.7"
|
||||
|
||||
|
||||
def test_simulation_endpoint_preserves_equal_budget():
|
||||
response = client.post(
|
||||
"/api/simulate",
|
||||
json={
|
||||
"rng_seed": 3,
|
||||
"n_paths": 5,
|
||||
"n_steps": 100,
|
||||
"drop_fraction": 0.05,
|
||||
"rise_fraction": 0.08,
|
||||
"max_detection_lag_steps": 3,
|
||||
},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
payload = response.json()
|
||||
assert payload["adaptive_total_samples"] == payload["fixed_total_samples"]
|
||||
assert payload["max_detection_lag_steps"] == 3
|
||||
|
||||
def test_webapp_imports_from_source_checkout_without_installed_package(tmp_path):
|
||||
"""The documented direct uvicorn command must resolve the src-layout package."""
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
repo_root = Path(__file__).resolve().parents[1]
|
||||
env = os.environ.copy()
|
||||
env.pop("PYTHONPATH", None)
|
||||
completed = subprocess.run(
|
||||
[sys.executable, "-c", "import webapp.app; print(webapp.app.app.title)"],
|
||||
cwd=repo_root,
|
||||
env=env,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
assert completed.returncode == 0, completed.stderr
|
||||
assert "Adaptive Barrier Monitor" in completed.stdout
|
||||
|
||||
|
||||
|
||||
def test_simulation_endpoint_supports_fixed_cadence():
|
||||
response = client.post(
|
||||
"/api/simulate",
|
||||
json={
|
||||
"rng_seed": 3,
|
||||
"n_paths": 5,
|
||||
"n_steps": 200,
|
||||
"window_minutes": 200.0,
|
||||
"dt_cap_minutes": 2.0,
|
||||
"comparison_mode": "fixed_cadence",
|
||||
"fixed_cadence_minutes": 20.0,
|
||||
},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
payload = response.json()
|
||||
assert payload["comparison_mode"] == "fixed_cadence"
|
||||
assert payload["fixed_cadence_minutes"] == 20.0
|
||||
assert payload["adaptive_total_samples"] != payload["fixed_total_samples"]
|
||||
|
||||
|
||||
def test_simulation_endpoint_rejects_unknown_comparison_mode():
|
||||
response = client.post(
|
||||
"/api/simulate",
|
||||
json={"comparison_mode": "unknown"},
|
||||
)
|
||||
assert response.status_code == 422
|
||||
@@ -0,0 +1,89 @@
|
||||
# Webapp Reference
|
||||
|
||||
The web application is a focused Monte Carlo comparison built with FastAPI,
|
||||
vanilla JavaScript, and Plotly.js.
|
||||
|
||||
## Purpose
|
||||
|
||||
For each simulated price path, the backend constructs an adaptive schedule
|
||||
using the current distance to the nearer log-price barrier and the local
|
||||
symmetric-endpoint Brownian-bridge proxy. The fixed baseline has two modes:
|
||||
|
||||
1. **Equal budget:** use exactly the adaptive schedule's sample count on each
|
||||
path, distributed uniformly.
|
||||
2. **Fixed cadence:** sample at an independently selected interval in minutes,
|
||||
with the start and end of the window included.
|
||||
|
||||
Lower and upper barrier events are evaluated independently. A detection must
|
||||
occur within the configured number of simulation-grid steps and the sampled
|
||||
price must still be beyond the relevant barrier.
|
||||
|
||||
## Routes
|
||||
|
||||
### `GET /`
|
||||
|
||||
Serves the single-page frontend and replaces static asset query strings with
|
||||
mtime/size cache-busting values.
|
||||
|
||||
### `GET /api/health`
|
||||
|
||||
Returns runtime readiness, package version, and default model parameters.
|
||||
|
||||
### `POST /api/simulate`
|
||||
|
||||
Validated request fields include:
|
||||
|
||||
- initial price, annual drift, and annual volatility;
|
||||
- simulation horizon, number of paths, and grid resolution;
|
||||
- lower and upper barrier percentages;
|
||||
- local diffusion parameter `eps`;
|
||||
- hard maximum polling interval;
|
||||
- detection deadline in grid steps;
|
||||
- comparison mode and fixed cadence in minutes;
|
||||
- optional Merton jump parameters;
|
||||
- optional random seed.
|
||||
|
||||
The response contains path data, both schedules, direction-specific breach and
|
||||
detection fields, exact sample totals, detection-lag summaries, and an explicit
|
||||
model-scope warning.
|
||||
|
||||
## Important interpretation
|
||||
|
||||
`eps` is **not** an unconditional probability guarantee. The exact Brownian
|
||||
bridge formula conditions on two endpoints, whereas an online scheduler does
|
||||
not know the future endpoint. The implementation substitutes the current
|
||||
barrier distance for both endpoint distances. Jumps are outside that diffusion
|
||||
calculation entirely.
|
||||
|
||||
The demo therefore presents a controlled scheduling comparison, not a promise
|
||||
that adaptive sampling always outperforms fixed sampling.
|
||||
|
||||
## Frontend
|
||||
|
||||
The interface provides controls for all main simulation and detector parameters.
|
||||
It displays:
|
||||
|
||||
- simulated paths and both barriers;
|
||||
- adaptive observations as open cyan circles;
|
||||
- equal-budget or fixed-cadence observations as grey dots;
|
||||
- first lower and upper breach markers;
|
||||
- direction-specific detection counts;
|
||||
- exact adaptive/fixed sample totals;
|
||||
- adaptive and fixed mean detection lags;
|
||||
- raw run statistics and model-scope warnings.
|
||||
|
||||
Pure GBM is the default. Merton jump diffusion is an optional stress mode and
|
||||
triggers an on-screen warning about the limits of the Brownian proxy.
|
||||
|
||||
## Local development
|
||||
|
||||
```bash
|
||||
pip install -e ".[webapp]"
|
||||
uvicorn webapp.app:app --reload --host 127.0.0.1 --port 8055
|
||||
```
|
||||
|
||||
## API smoke test
|
||||
|
||||
```bash
|
||||
curl -s http://127.0.0.1:8055/api/health
|
||||
```
|
||||
@@ -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"]
|
||||
@@ -0,0 +1 @@
|
||||
"""FastAPI application package for the Adaptive Barrier Monitor demo."""
|
||||
+123
@@ -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)
|
||||
@@ -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