Touched up notebooks + webapp

This commit is contained in:
2026-07-31 17:05:14 -04:00
commit 8e6c98945b
31 changed files with 10824 additions and 0 deletions
+27
View File
@@ -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/
+21
View File
@@ -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.
+195
View File
@@ -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, BachelierLé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:** bidask 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).
+54
View File
@@ -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.
+1952
View File
File diff suppressed because it is too large Load Diff
+1640
View File
File diff suppressed because it is too large Load Diff
+392
View File
@@ -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
1 timestamp open high low close volume
2 2026-07-20 13:30:00+00:00 747.0599975585938 748.0499877929688 746.7999877929688 748.0499877929688 1851776
3 2026-07-20 13:35:00+00:00 748.030029296875 748.7100219726562 747.72998046875 748.1699829101562 833394
4 2026-07-20 13:40:00+00:00 748.1699829101562 748.5900268554688 748.0599975585938 748.5599975585938 511709
5 2026-07-20 13:45:00+00:00 748.5700073242188 748.72998046875 747.530029296875 747.5800170898438 805592
6 2026-07-20 13:50:00+00:00 747.5800170898438 747.8400268554688 746.25 746.4550170898438 874876
7 2026-07-20 13:55:00+00:00 746.4500122070312 746.489990234375 745.52001953125 745.6900024414062 671795
8 2026-07-20 14:00:00+00:00 745.6799926757812 746.3599853515625 745.3900146484375 745.885009765625 524174
9 2026-07-20 14:05:00+00:00 745.9000244140625 746.1199951171875 745.27001953125 745.4000244140625 475704
10 2026-07-20 14:10:00+00:00 745.3400268554688 745.3800048828125 744.7650146484375 744.989990234375 414066
11 2026-07-20 14:15:00+00:00 745.02001953125 745.47998046875 744.6300048828125 744.9000244140625 420432
12 2026-07-20 14:20:00+00:00 744.8499755859375 745.2100219726562 744.260009765625 744.969970703125 561519
13 2026-07-20 14:25:00+00:00 744.969970703125 745.739990234375 744.9099731445312 745.6799926757812 355120
14 2026-07-20 14:30:00+00:00 745.6900024414062 745.9600219726562 745.0399780273438 745.260009765625 475390
15 2026-07-20 14:35:00+00:00 745.260009765625 745.530029296875 744.9598999023438 745.3300170898438 453223
16 2026-07-20 14:40:00+00:00 745.3499755859375 745.7899780273438 744.7520141601562 744.8900146484375 258874
17 2026-07-20 14:45:00+00:00 744.8800048828125 745.739990234375 744.4901123046875 744.6400146484375 323848
18 2026-07-20 14:50:00+00:00 744.6199951171875 744.9400024414062 744.1500244140625 744.3250122070312 539597
19 2026-07-20 14:55:00+00:00 744.3200073242188 744.489990234375 743.6900024414062 744.1099853515625 429824
20 2026-07-20 15:00:00+00:00 744.1400146484375 744.8800048828125 743.8049926757812 744.5800170898438 297794
21 2026-07-20 15:05:00+00:00 744.5599975585938 745.4849853515625 743.8499755859375 745.4099731445312 502571
22 2026-07-20 15:10:00+00:00 745.4000244140625 745.4600219726562 744.6699829101562 744.7999877929688 226403
23 2026-07-20 15:15:00+00:00 744.7999877929688 745.530029296875 744.6099853515625 745.27001953125 295430
24 2026-07-20 15:20:00+00:00 745.260009765625 745.969970703125 745.0700073242188 745.75 316075
25 2026-07-20 15:25:00+00:00 745.719970703125 745.969970703125 745.219970703125 745.8699951171875 503848
26 2026-07-20 15:30:00+00:00 745.8900146484375 746.02001953125 745.77001953125 745.8800048828125 192342
27 2026-07-20 15:35:00+00:00 745.9000244140625 745.9299926757812 745.3900146484375 745.4600219726562 214063
28 2026-07-20 15:40:00+00:00 745.469970703125 745.969970703125 745.280029296875 745.969970703125 214146
29 2026-07-20 15:45:00+00:00 745.97998046875 745.989990234375 745.0999755859375 745.2999877929688 254897
30 2026-07-20 15:50:00+00:00 745.2899780273438 746.3599853515625 745.1300048828125 746.219970703125 400298
31 2026-07-20 15:55:00+00:00 746.22998046875 746.4400024414062 745.9400024414062 745.989990234375 310885
32 2026-07-20 16:00:00+00:00 746.0 746.25 745.5800170898438 746.1599731445312 407969
33 2026-07-20 16:05:00+00:00 746.1599731445312 746.8350219726562 746.1599731445312 746.7999877929688 588741
34 2026-07-20 16:10:00+00:00 746.780029296875 747.0050048828125 746.6400146484375 746.7899780273438 297597
35 2026-07-20 16:15:00+00:00 746.8300170898438 746.844970703125 746.3099975585938 746.6799926757812 314922
36 2026-07-20 16:20:00+00:00 746.7000122070312 746.8800048828125 746.6199951171875 746.780029296875 227319
37 2026-07-20 16:25:00+00:00 746.780029296875 746.8300170898438 746.1699829101562 746.5 323898
38 2026-07-20 16:30:00+00:00 746.489990234375 746.780029296875 745.7999877929688 745.8300170898438 328389
39 2026-07-20 16:35:00+00:00 745.8200073242188 746.3699951171875 745.8099975585938 746.3499755859375 240742
40 2026-07-20 16:40:00+00:00 746.3300170898438 746.52001953125 744.5 745.4299926757812 1044388
41 2026-07-20 16:45:00+00:00 745.4099731445312 746.1799926757812 745.2100219726562 746.1300048828125 420926
42 2026-07-20 16:50:00+00:00 746.1199951171875 746.155029296875 744.9099731445312 744.969970703125 316124
43 2026-07-20 16:55:00+00:00 744.969970703125 745.1900024414062 744.72998046875 745.0 481423
44 2026-07-20 17:00:00+00:00 745.010009765625 745.2100219726562 744.5499877929688 744.552490234375 307945
45 2026-07-20 17:05:00+00:00 744.5399780273438 744.6500244140625 744.1300048828125 744.5900268554688 288703
46 2026-07-20 17:10:00+00:00 744.5900268554688 744.6400146484375 743.8400268554688 743.9000244140625 294244
47 2026-07-20 17:15:00+00:00 743.8900146484375 744.260009765625 743.5599975585938 744.1199951171875 383022
48 2026-07-20 17:20:00+00:00 744.114990234375 744.489990234375 743.969970703125 744.1099853515625 228512
49 2026-07-20 17:25:00+00:00 744.0999755859375 744.3699951171875 743.8900146484375 744.1199951171875 258856
50 2026-07-20 17:30:00+00:00 744.1300048828125 744.4201049804688 743.9500122070312 744.3250122070312 243517
51 2026-07-20 17:35:00+00:00 744.2999877929688 744.5399780273438 743.989990234375 744.010009765625 267253
52 2026-07-20 17:40:00+00:00 744.030029296875 744.0499877929688 743.5900268554688 743.760009765625 344669
53 2026-07-20 17:45:00+00:00 743.77001953125 744.3499755859375 743.6300048828125 744.25 281957
54 2026-07-20 17:50:00+00:00 744.219970703125 744.9199829101562 744.2100219726562 744.6500244140625 290349
55 2026-07-20 17:55:00+00:00 744.6500244140625 744.6900024414062 744.2100830078125 744.5499877929688 261761
56 2026-07-20 18:00:00+00:00 744.5499877929688 745.1699829101562 744.5499877929688 744.8499755859375 363635
57 2026-07-20 18:05:00+00:00 744.8400268554688 744.9400024414062 744.530029296875 744.875 239896
58 2026-07-20 18:10:00+00:00 744.9000244140625 745.219970703125 744.7100219726562 745.030029296875 219433
59 2026-07-20 18:15:00+00:00 745.02001953125 745.3800048828125 744.8800048828125 745.239990234375 387343
60 2026-07-20 18:20:00+00:00 745.219970703125 745.2899780273438 744.25 744.280029296875 618991
61 2026-07-20 18:25:00+00:00 744.2899780273438 744.7999877929688 744.1300048828125 744.469970703125 343916
62 2026-07-20 18:30:00+00:00 744.469970703125 744.489990234375 743.47998046875 743.6649780273438 473873
63 2026-07-20 18:35:00+00:00 743.6599731445312 744.2949829101562 743.4400024414062 743.5750122070312 340647
64 2026-07-20 18:40:00+00:00 743.5750122070312 743.6199951171875 742.530029296875 742.5449829101562 546380
65 2026-07-20 18:45:00+00:00 742.5499877929688 743.0499877929688 742.4299926757812 742.7000122070312 685176
66 2026-07-20 18:50:00+00:00 742.7100219726562 743.22998046875 742.5599975585938 743.0999755859375 302428
67 2026-07-20 18:55:00+00:00 743.094970703125 743.1400146484375 742.47998046875 742.719970703125 308207
68 2026-07-20 19:00:00+00:00 742.760009765625 743.1900024414062 742.4199829101562 742.5900268554688 616940
69 2026-07-20 19:05:00+00:00 742.5900268554688 742.8209838867188 742.25 742.6950073242188 525097
70 2026-07-20 19:10:00+00:00 742.6994018554688 743.0999755859375 741.7000122070312 741.7000122070312 591743
71 2026-07-20 19:15:00+00:00 741.7100219726562 742.2100219726562 741.5103149414062 741.9199829101562 970049
72 2026-07-20 19:20:00+00:00 741.9299926757812 742.4299926757812 741.7899780273438 742.2999877929688 614806
73 2026-07-20 19:25:00+00:00 742.3200073242188 742.9199829101562 741.8599853515625 742.72998046875 683468
74 2026-07-20 19:30:00+00:00 742.739990234375 743.2000122070312 742.3400268554688 742.97998046875 709918
75 2026-07-20 19:35:00+00:00 742.9600219726562 743.1300048828125 742.510009765625 742.97998046875 647393
76 2026-07-20 19:40:00+00:00 742.969970703125 743.3400268554688 742.7999877929688 742.989990234375 568417
77 2026-07-20 19:45:00+00:00 743.0 743.25 742.7000122070312 743.0800170898438 993664
78 2026-07-20 19:50:00+00:00 743.0700073242188 743.0700073242188 741.760009765625 742.2000122070312 1894670
79 2026-07-20 19:55:00+00:00 742.1799926757812 742.239990234375 741.6099853515625 742.1400146484375 4361321
80 2026-07-21 13:30:00+00:00 746.2899780273438 746.6799926757812 745.1199951171875 745.5999755859375 1901011
81 2026-07-21 13:35:00+00:00 745.6099853515625 746.231689453125 745.27001953125 745.280029296875 824912
82 2026-07-21 13:40:00+00:00 745.260009765625 745.3099975585938 744.2708740234375 744.510009765625 600352
83 2026-07-21 13:45:00+00:00 744.52001953125 745.0 744.1799926757812 744.9500122070312 420126
84 2026-07-21 13:50:00+00:00 744.969970703125 745.3499755859375 744.260009765625 745.0700073242188 386215
85 2026-07-21 13:55:00+00:00 745.0499877929688 745.364990234375 744.6099853515625 745.3599853515625 344126
86 2026-07-21 14:00:00+00:00 745.3699951171875 745.869873046875 744.9600219726562 745.4199829101562 348723
87 2026-07-21 14:05:00+00:00 745.4199829101562 745.77001953125 745.02001953125 745.1950073242188 212358
88 2026-07-21 14:10:00+00:00 745.1900024414062 745.8599853515625 744.9500122070312 745.7449951171875 284660
89 2026-07-21 14:15:00+00:00 745.719970703125 746.1699829101562 744.7899780273438 746.02001953125 302137
90 2026-07-21 14:20:00+00:00 746.0150146484375 746.1900024414062 745.4550170898438 745.97998046875 224452
91 2026-07-21 14:25:00+00:00 745.9600219726562 746.989990234375 745.9299926757812 746.8699951171875 450689
92 2026-07-21 14:30:00+00:00 746.8499755859375 747.25 746.5800170898438 746.7999877929688 340012
93 2026-07-21 14:35:00+00:00 746.7949829101562 747.02001953125 746.0900268554688 746.2999877929688 301549
94 2026-07-21 14:40:00+00:00 746.2999877929688 746.635009765625 745.77001953125 746.5999755859375 298164
95 2026-07-21 14:45:00+00:00 746.6099853515625 747.260009765625 746.4500122070312 746.9099731445312 401228
96 2026-07-21 14:50:00+00:00 746.9099731445312 746.9600219726562 746.5 746.6900024414062 166050
97 2026-07-21 14:55:00+00:00 746.6799926757812 746.7899780273438 746.260009765625 746.739990234375 202701
98 2026-07-21 15:00:00+00:00 746.77001953125 746.8900146484375 745.8300170898438 746.0009765625 310493
99 2026-07-21 15:05:00+00:00 746.0 747.0700073242188 745.8499755859375 746.97998046875 292853
100 2026-07-21 15:10:00+00:00 746.97998046875 747.52001953125 746.9400024414062 747.2097778320312 337144
101 2026-07-21 15:15:00+00:00 747.2100219726562 747.5499877929688 747.1588134765625 747.4099731445312 202955
102 2026-07-21 15:20:00+00:00 747.4099731445312 747.97998046875 747.3200073242188 747.656005859375 426494
103 2026-07-21 15:25:00+00:00 747.6544189453125 748.010009765625 747.6099853515625 748.0 191435
104 2026-07-21 15:30:00+00:00 747.97998046875 748.0 747.4099731445312 747.7750244140625 364371
105 2026-07-21 15:35:00+00:00 747.7899780273438 747.9299926757812 747.4299926757812 747.5800170898438 267226
106 2026-07-21 15:40:00+00:00 747.5599975585938 747.8588256835938 747.5198974609375 747.594970703125 235112
107 2026-07-21 15:45:00+00:00 747.580322265625 747.9199829101562 747.530029296875 747.8115234375 165894
108 2026-07-21 15:50:00+00:00 747.8200073242188 747.97998046875 747.7000122070312 747.719970703125 270841
109 2026-07-21 15:55:00+00:00 747.77001953125 747.9000244140625 747.469970703125 747.52001953125 178981
110 2026-07-21 16:00:00+00:00 747.52001953125 748.22998046875 747.489990234375 748.219970703125 235413
111 2026-07-21 16:05:00+00:00 748.2255249023438 748.3099975585938 748.0700073242188 748.1500244140625 157808
112 2026-07-21 16:10:00+00:00 748.1497802734375 748.27001953125 748.0399780273438 748.22998046875 163585
113 2026-07-21 16:15:00+00:00 748.239990234375 748.530029296875 748.219970703125 748.469970703125 456214
114 2026-07-21 16:20:00+00:00 748.47998046875 748.75 748.4099731445312 748.5800170898438 202970
115 2026-07-21 16:25:00+00:00 748.5800170898438 748.7000122070312 748.4400024414062 748.6599731445312 119457
116 2026-07-21 16:30:00+00:00 748.6400146484375 748.780029296875 748.4801025390625 748.6849975585938 186951
117 2026-07-21 16:35:00+00:00 748.680908203125 748.719970703125 748.489990234375 748.6699829101562 101468
118 2026-07-21 16:40:00+00:00 748.6699829101562 748.739990234375 748.5 748.6099853515625 120496
119 2026-07-21 16:45:00+00:00 748.6300048828125 748.6500244140625 748.4000244140625 748.489990234375 125200
120 2026-07-21 16:50:00+00:00 748.489990234375 748.9949951171875 748.4500122070312 748.9598999023438 215362
121 2026-07-21 16:55:00+00:00 748.9600219726562 749.0399780273438 748.6799926757812 748.6900024414062 620830
122 2026-07-21 17:00:00+00:00 748.6884155273438 748.8300170898438 748.5805053710938 748.6199951171875 170343
123 2026-07-21 17:05:00+00:00 748.6199951171875 748.85498046875 748.5800170898438 748.7100219726562 356355
124 2026-07-21 17:10:00+00:00 748.719970703125 748.739990234375 748.530029296875 748.5999755859375 478893
125 2026-07-21 17:15:00+00:00 748.6099853515625 748.6599731445312 748.3300170898438 748.35498046875 335585
126 2026-07-21 17:20:00+00:00 748.3499755859375 748.47998046875 748.25 748.4000244140625 192939
127 2026-07-21 17:25:00+00:00 748.4199829101562 748.47998046875 748.239990234375 748.3599853515625 175614
128 2026-07-21 17:30:00+00:00 748.3599853515625 748.5700073242188 748.2150268554688 748.3599853515625 225080
129 2026-07-21 17:35:00+00:00 748.3499755859375 748.7999877929688 748.3400268554688 748.7000122070312 247679
130 2026-07-21 17:40:00+00:00 748.7000122070312 748.72998046875 748.510009765625 748.669921875 142384
131 2026-07-21 17:45:00+00:00 748.6599731445312 748.8599853515625 748.5499877929688 748.8200073242188 161776
132 2026-07-21 17:50:00+00:00 748.8400268554688 748.9299926757812 748.6599731445312 748.6799926757812 306097
133 2026-07-21 17:55:00+00:00 748.6749877929688 748.719970703125 748.4400024414062 748.5900268554688 232952
134 2026-07-21 18:00:00+00:00 748.5900268554688 748.77001953125 748.260009765625 748.3200073242188 295577
135 2026-07-21 18:05:00+00:00 748.2999877929688 748.5499877929688 748.27001953125 748.4550170898438 163057
136 2026-07-21 18:10:00+00:00 748.4400024414062 748.5490112304688 748.1199951171875 748.1599731445312 245581
137 2026-07-21 18:15:00+00:00 748.1699829101562 748.27001953125 748.0900268554688 748.2100219726562 219304
138 2026-07-21 18:20:00+00:00 748.1900024414062 748.4000244140625 748.1099853515625 748.2999877929688 200541
139 2026-07-21 18:25:00+00:00 748.2899780273438 748.2999877929688 748.0650024414062 748.2100219726562 316580
140 2026-07-21 18:30:00+00:00 748.219970703125 748.260009765625 747.9400024414062 747.969970703125 218286
141 2026-07-21 18:35:00+00:00 747.969970703125 748.27001953125 747.9600219726562 748.1500244140625 227217
142 2026-07-21 18:40:00+00:00 748.1500244140625 748.6099853515625 748.02001953125 748.5399780273438 215354
143 2026-07-21 18:45:00+00:00 748.530029296875 748.6400146484375 748.3800048828125 748.4500122070312 161869
144 2026-07-21 18:50:00+00:00 748.469970703125 748.510009765625 748.25 748.25927734375 185937
145 2026-07-21 18:55:00+00:00 748.25 748.5700073242188 748.22998046875 748.3699951171875 314496
146 2026-07-21 19:00:00+00:00 748.3800048828125 748.469970703125 748.1500244140625 748.27001953125 225750
147 2026-07-21 19:05:00+00:00 748.25 748.4199829101562 748.1199951171875 748.135009765625 277072
148 2026-07-21 19:10:00+00:00 748.1400146484375 748.4299926757812 748.0900268554688 748.239990234375 308319
149 2026-07-21 19:15:00+00:00 748.25 748.4600219726562 748.1900024414062 748.3099975585938 190314
150 2026-07-21 19:20:00+00:00 748.3099975585938 748.3900146484375 748.1900024414062 748.3099975585938 183500
151 2026-07-21 19:25:00+00:00 748.2899780273438 748.4600219726562 748.219970703125 748.260009765625 375104
152 2026-07-21 19:30:00+00:00 748.25 748.3900146484375 748.1500244140625 748.1799926757812 361485
153 2026-07-21 19:35:00+00:00 748.1699829101562 748.5700073242188 748.1199951171875 748.4099731445312 332389
154 2026-07-21 19:40:00+00:00 748.4005126953125 748.5880126953125 748.239990234375 748.530029296875 742764
155 2026-07-21 19:45:00+00:00 748.530029296875 748.6400146484375 748.2449951171875 748.60498046875 605051
156 2026-07-21 19:50:00+00:00 748.5900268554688 748.7000122070312 748.2899780273438 748.3800048828125 1277177
157 2026-07-21 19:55:00+00:00 748.3800048828125 748.5800170898438 748.0900268554688 748.3300170898438 1743645
158 2026-07-22 13:30:00+00:00 746.6199951171875 747.5700073242188 746.3699951171875 747.3150024414062 1723196
159 2026-07-22 13:35:00+00:00 747.3099975585938 747.489990234375 747.02001953125 747.4000244140625 398272
160 2026-07-22 13:40:00+00:00 747.4099731445312 747.7000122070312 747.0900268554688 747.5700073242188 386033
161 2026-07-22 13:45:00+00:00 747.5999755859375 748.0599975585938 747.5700073242188 747.760009765625 387195
162 2026-07-22 13:50:00+00:00 747.739990234375 748.1799926757812 747.39501953125 748.1500244140625 499544
163 2026-07-22 13:55:00+00:00 748.1300048828125 748.7849731445312 748.02001953125 748.780029296875 496342
164 2026-07-22 14:00:00+00:00 748.77001953125 748.969970703125 748.52001953125 748.969970703125 393130
165 2026-07-22 14:05:00+00:00 748.969970703125 749.3800048828125 748.510009765625 748.5401000976562 386190
166 2026-07-22 14:10:00+00:00 748.5399780273438 748.7750244140625 748.110107421875 748.239990234375 289947
167 2026-07-22 14:15:00+00:00 748.260009765625 748.3200073242188 747.77001953125 748.1300048828125 251758
168 2026-07-22 14:20:00+00:00 748.1400146484375 748.4299926757812 747.3800048828125 747.5999755859375 390741
169 2026-07-22 14:25:00+00:00 747.6300048828125 747.8099975585938 747.0900268554688 747.2999877929688 311511
170 2026-07-22 14:30:00+00:00 747.2899780273438 747.7899780273438 747.1699829101562 747.239990234375 405856
171 2026-07-22 14:35:00+00:00 747.260009765625 747.4500122070312 747.05078125 747.3099975585938 396475
172 2026-07-22 14:40:00+00:00 747.2999877929688 747.4500122070312 746.760009765625 746.780029296875 250611
173 2026-07-22 14:45:00+00:00 746.7899780273438 747.469970703125 746.6799926757812 747.3699951171875 297686
174 2026-07-22 14:50:00+00:00 747.3800048828125 747.6500244140625 747.0900268554688 747.4199829101562 348917
175 2026-07-22 14:55:00+00:00 747.3900146484375 747.9769897460938 747.1500244140625 747.9349975585938 280430
176 2026-07-22 15:00:00+00:00 747.9299926757812 748.4400024414062 747.8900146484375 748.3049926757812 277938
177 2026-07-22 15:05:00+00:00 748.2999877929688 748.75 748.2000122070312 748.6099853515625 333941
178 2026-07-22 15:10:00+00:00 748.6099853515625 749.039306640625 748.5900268554688 748.875 275844
179 2026-07-22 15:15:00+00:00 748.8800048828125 748.9849853515625 748.52001953125 748.8599853515625 382048
180 2026-07-22 15:20:00+00:00 748.8699951171875 749.4000244140625 748.8300170898438 749.3099975585938 177372
181 2026-07-22 15:25:00+00:00 749.3200073242188 749.6799926757812 749.2606811523438 749.4099731445312 274008
182 2026-07-22 15:30:00+00:00 749.4199829101562 749.510009765625 748.9199829101562 748.9600219726562 258364
183 2026-07-22 15:35:00+00:00 748.9849853515625 749.1400146484375 748.7506713867188 748.9149780273438 184888
184 2026-07-22 15:40:00+00:00 748.9149780273438 749.2100219726562 748.8499755859375 749.0 123396
185 2026-07-22 15:45:00+00:00 749.02001953125 749.3800048828125 749.02001953125 749.3800048828125 251782
186 2026-07-22 15:50:00+00:00 749.3800048828125 749.5900268554688 749.22998046875 749.280029296875 154286
187 2026-07-22 15:55:00+00:00 749.260009765625 749.7899780273438 749.1799926757812 749.760009765625 157530
188 2026-07-22 16:00:00+00:00 749.760009765625 749.8200073242188 749.489990234375 749.6849975585938 191780
189 2026-07-22 16:05:00+00:00 749.6900024414062 749.9099731445312 749.5399780273438 749.7449951171875 230076
190 2026-07-22 16:10:00+00:00 749.739990234375 749.7899780273438 749.5999755859375 749.7100219726562 200353
191 2026-07-22 16:15:00+00:00 749.7100219726562 750.02001953125 749.6799926757812 749.9749755859375 195351
192 2026-07-22 16:20:00+00:00 749.969970703125 749.969970703125 749.780029296875 749.8699951171875 234835
193 2026-07-22 16:25:00+00:00 749.8699951171875 749.9400024414062 749.6799926757812 749.8499755859375 242635
194 2026-07-22 16:30:00+00:00 749.8599853515625 750.0048217773438 749.75 749.9299926757812 178756
195 2026-07-22 16:35:00+00:00 749.9199829101562 749.9600219726562 749.7899780273438 749.8699951171875 313848
196 2026-07-22 16:40:00+00:00 749.8900146484375 749.8900146484375 749.6400146484375 749.7550048828125 179533
197 2026-07-22 16:45:00+00:00 749.760009765625 749.9000244140625 749.52001953125 749.60498046875 179344
198 2026-07-22 16:50:00+00:00 749.6099853515625 749.6799926757812 749.47998046875 749.52001953125 132286
199 2026-07-22 16:55:00+00:00 749.510009765625 749.9500122070312 749.5 749.8300170898438 146439
200 2026-07-22 17:00:00+00:00 749.8350219726562 749.9600219726562 749.3699951171875 749.5 233695
201 2026-07-22 17:05:00+00:00 749.5 749.75 749.47998046875 749.6099853515625 580307
202 2026-07-22 17:10:00+00:00 749.5999755859375 749.6199951171875 749.25 749.3200073242188 361788
203 2026-07-22 17:15:00+00:00 749.3099975585938 749.4299926757812 749.1900024414062 749.25 502612
204 2026-07-22 17:20:00+00:00 749.239990234375 749.260009765625 748.9500122070312 749.1749877929688 195943
205 2026-07-22 17:25:00+00:00 749.1699829101562 749.3900146484375 749.0 749.0444946289062 196990
206 2026-07-22 17:30:00+00:00 749.0499877929688 749.2899780273438 748.8599853515625 748.8800048828125 244761
207 2026-07-22 17:35:00+00:00 748.8800048828125 749.4099731445312 748.8499755859375 749.3599853515625 210400
208 2026-07-22 17:40:00+00:00 749.3599853515625 749.3699951171875 749.0499877929688 749.1199951171875 175551
209 2026-07-22 17:45:00+00:00 749.1300048828125 749.22998046875 749.0150146484375 749.0499877929688 126164
210 2026-07-22 17:50:00+00:00 749.0399780273438 749.1500244140625 748.6920166015625 748.7100219726562 166578
211 2026-07-22 17:55:00+00:00 748.7000122070312 748.7000122070312 748.3900146484375 748.52001953125 645620
212 2026-07-22 18:00:00+00:00 748.5150146484375 748.7100219726562 748.2000122070312 748.3800048828125 297919
213 2026-07-22 18:05:00+00:00 748.3800048828125 748.6900024414062 748.27001953125 748.3289794921875 295069
214 2026-07-22 18:10:00+00:00 748.2999877929688 748.4000244140625 748.010009765625 748.0399780273438 232207
215 2026-07-22 18:15:00+00:00 748.030029296875 748.3900146484375 748.0250244140625 748.22998046875 203582
216 2026-07-22 18:20:00+00:00 748.25 748.5900268554688 748.1599731445312 748.2225952148438 238646
217 2026-07-22 18:25:00+00:00 748.22998046875 748.5499877929688 748.2100219726562 748.489990234375 188785
218 2026-07-22 18:30:00+00:00 748.489990234375 748.489990234375 748.0499877929688 748.2000122070312 276360
219 2026-07-22 18:35:00+00:00 748.1900024414062 748.469970703125 748.1599731445312 748.4099731445312 188524
220 2026-07-22 18:40:00+00:00 748.4299926757812 748.469970703125 748.22998046875 748.2901000976562 288221
221 2026-07-22 18:45:00+00:00 748.2899780273438 748.5999755859375 748.2048950195312 748.2224731445312 216078
222 2026-07-22 18:50:00+00:00 748.219970703125 748.5798950195312 748.1099853515625 748.52001953125 245576
223 2026-07-22 18:55:00+00:00 748.5 748.5900268554688 748.37890625 748.4099731445312 183536
224 2026-07-22 19:00:00+00:00 748.4400024414062 748.4600219726562 748.1500244140625 748.239990234375 208281
225 2026-07-22 19:05:00+00:00 748.2449951171875 748.3499755859375 748.1500244140625 748.1900024414062 162600
226 2026-07-22 19:10:00+00:00 748.2000122070312 748.239990234375 747.8300170898438 747.8900146484375 248523
227 2026-07-22 19:15:00+00:00 747.9000244140625 748.1900024414062 747.8300170898438 748.1099853515625 189199
228 2026-07-22 19:20:00+00:00 748.1099853515625 748.1199951171875 747.7999877929688 747.8499755859375 205636
229 2026-07-22 19:25:00+00:00 747.8499755859375 748.0999755859375 747.6500244140625 748.0 415094
230 2026-07-22 19:30:00+00:00 748.0 748.0 747.6699829101562 747.7100219726562 616066
231 2026-07-22 19:35:00+00:00 747.7100219726562 747.7999877929688 747.3599853515625 747.4099731445312 522654
232 2026-07-22 19:40:00+00:00 747.4215087890625 747.9199829101562 747.4199829101562 747.7899780273438 556526
233 2026-07-22 19:45:00+00:00 747.7949829101562 748.0700073242188 747.7550048828125 747.97998046875 742707
234 2026-07-22 19:50:00+00:00 747.989990234375 748.2999877929688 747.6900024414062 747.8099975585938 870972
235 2026-07-22 19:55:00+00:00 747.8200073242188 748.239990234375 747.2949829101562 747.3300170898438 3100017
236 2026-07-23 13:30:00+00:00 739.3699951171875 741.3699951171875 738.469970703125 741.3200073242188 4201871
237 2026-07-23 13:35:00+00:00 741.2999877929688 741.8699951171875 740.8499755859375 741.4450073242188 793095
238 2026-07-23 13:40:00+00:00 741.4199829101562 742.5599975585938 741.4199829101562 741.8200073242188 612613
239 2026-07-23 13:45:00+00:00 741.8499755859375 741.9949951171875 740.1699829101562 740.4000244140625 807521
240 2026-07-23 13:50:00+00:00 740.3699951171875 740.9450073242188 739.719970703125 740.9299926757812 1031948
241 2026-07-23 13:55:00+00:00 740.9500122070312 741.280029296875 739.2999877929688 739.5750122070312 676870
242 2026-07-23 14:00:00+00:00 739.5800170898438 740.6300048828125 739.5800170898438 740.344970703125 531714
243 2026-07-23 14:05:00+00:00 740.3599853515625 740.9299926757812 738.8699951171875 739.3599853515625 443580
244 2026-07-23 14:10:00+00:00 739.344970703125 740.5399780273438 739.2100219726562 739.6900024414062 445606
245 2026-07-23 14:15:00+00:00 739.6599731445312 740.3599853515625 739.4099731445312 739.510009765625 449562
246 2026-07-23 14:20:00+00:00 739.52001953125 740.4349975585938 739.1699829101562 739.4400024414062 577693
247 2026-07-23 14:25:00+00:00 739.4000244140625 739.9099731445312 738.989990234375 739.7999877929688 545442
248 2026-07-23 14:30:00+00:00 739.739990234375 740.0399780273438 738.6599731445312 738.969970703125 449770
249 2026-07-23 14:35:00+00:00 738.97998046875 740.510009765625 738.5900268554688 740.3800048828125 780782
250 2026-07-23 14:40:00+00:00 740.3800048828125 740.6400146484375 738.6699829101562 738.8699951171875 432500
251 2026-07-23 14:45:00+00:00 738.8599853515625 739.469970703125 738.6699829101562 738.8300170898438 422993
252 2026-07-23 14:50:00+00:00 738.8400268554688 739.0700073242188 738.010009765625 738.1400146484375 527804
253 2026-07-23 14:55:00+00:00 738.155029296875 738.5800170898438 738.0700073242188 738.3099975585938 354913
254 2026-07-23 15:00:00+00:00 738.3200073242188 738.47998046875 737.5 737.6599731445312 456987
255 2026-07-23 15:05:00+00:00 737.6699829101562 737.7000122070312 736.9400024414062 737.155029296875 831070
256 2026-07-23 15:10:00+00:00 737.1599731445312 737.4099731445312 736.4500122070312 736.6500244140625 985434
257 2026-07-23 15:15:00+00:00 736.6199951171875 737.2750244140625 736.5800170898438 737.1799926757812 637201
258 2026-07-23 15:20:00+00:00 737.2000122070312 737.6900024414062 735.97998046875 736.669921875 1020234
259 2026-07-23 15:25:00+00:00 736.6699829101562 736.7203979492188 735.8099975585938 735.9298706054688 857021
260 2026-07-23 15:30:00+00:00 735.9099731445312 736.4199829101562 735.2100219726562 736.4099731445312 810901
261 2026-07-23 15:35:00+00:00 736.4099731445312 737.4400024414062 736.3800048828125 737.3480224609375 696790
262 2026-07-23 15:40:00+00:00 737.3400268554688 737.6599731445312 736.6400146484375 736.6400146484375 544099
263 2026-07-23 15:45:00+00:00 736.6400146484375 736.969970703125 736.3599853515625 736.8961791992188 352859
264 2026-07-23 15:50:00+00:00 736.8900146484375 737.3300170898438 736.5900268554688 737.3200073242188 320277
265 2026-07-23 15:55:00+00:00 737.3300170898438 737.9000244140625 737.1199951171875 737.844970703125 333004
266 2026-07-23 16:00:00+00:00 737.844970703125 738.3099975585938 737.530029296875 738.0349731445312 421746
267 2026-07-23 16:05:00+00:00 738.0399780273438 738.469970703125 737.5499877929688 738.469970703125 343277
268 2026-07-23 16:10:00+00:00 738.469970703125 738.8350219726562 738.4199829101562 738.6199951171875 409071
269 2026-07-23 16:15:00+00:00 738.6199951171875 738.8300170898438 738.219970703125 738.5700073242188 1292232
270 2026-07-23 16:20:00+00:00 738.5828247070312 739.6599731445312 738.5700073242188 739.4600219726562 484093
271 2026-07-23 16:25:00+00:00 739.4500122070312 739.6199951171875 739.1500244140625 739.4500122070312 378385
272 2026-07-23 16:30:00+00:00 739.4500122070312 739.489990234375 739.0599975585938 739.1199951171875 328174
273 2026-07-23 16:35:00+00:00 739.0999755859375 739.719970703125 738.9400024414062 739.6998901367188 333622
274 2026-07-23 16:40:00+00:00 739.7000122070312 739.7100219726562 738.969970703125 738.969970703125 222903
275 2026-07-23 16:45:00+00:00 738.9600219726562 738.9600219726562 738.010009765625 738.0250244140625 351531
276 2026-07-23 16:50:00+00:00 738.02001953125 738.3400268554688 737.75 737.97998046875 299219
277 2026-07-23 16:55:00+00:00 737.97998046875 738.7100219726562 737.97998046875 738.6599731445312 271435
278 2026-07-23 17:00:00+00:00 738.6699829101562 738.9400024414062 738.2000122070312 738.2100219726562 175458
279 2026-07-23 17:05:00+00:00 738.219970703125 738.489990234375 737.7899780273438 738.489990234375 423821
280 2026-07-23 17:10:00+00:00 738.4600219726562 738.8400268554688 738.4299926757812 738.6099853515625 405299
281 2026-07-23 17:15:00+00:00 738.5599975585938 738.9000244140625 738.1599731445312 738.5250244140625 519411
282 2026-07-23 17:20:00+00:00 738.5650024414062 739.030029296875 738.4000244140625 738.989990234375 1506693
283 2026-07-23 17:25:00+00:00 738.97998046875 739.2000122070312 738.739990234375 738.75 374258
284 2026-07-23 17:30:00+00:00 738.739990234375 738.7999877929688 738.0499877929688 738.0900268554688 647896
285 2026-07-23 17:35:00+00:00 738.0549926757812 738.1199951171875 737.4650268554688 737.6199951171875 154118
286 2026-07-23 17:40:00+00:00 737.6199951171875 737.8599853515625 737.27001953125 737.7100219726562 189087
287 2026-07-23 17:45:00+00:00 737.7000122070312 738.2899780273438 737.5 738.1599731445312 179442
288 2026-07-23 17:50:00+00:00 738.1699829101562 738.2399291992188 737.7000122070312 737.7100219726562 140875
289 2026-07-23 17:55:00+00:00 737.6500244140625 737.7098999023438 737.1300048828125 737.1900024414062 149773
290 2026-07-23 18:00:00+00:00 737.1599731445312 737.3099975585938 736.9000244140625 736.97998046875 212506
291 2026-07-23 18:05:00+00:00 736.969970703125 737.1599731445312 736.8001098632812 736.8619995117188 209211
292 2026-07-23 18:10:00+00:00 736.8900146484375 737.3699951171875 736.8699951171875 737.02978515625 593695
293 2026-07-23 18:15:00+00:00 736.9719848632812 737.3599853515625 736.8200073242188 737.0 281077
294 2026-07-23 18:20:00+00:00 737.02001953125 737.4400024414062 736.9400024414062 737.4099731445312 183660
295 2026-07-23 18:25:00+00:00 737.4199829101562 737.489990234375 737.0349731445312 737.4199829101562 218263
296 2026-07-23 18:30:00+00:00 737.4149780273438 737.6599731445312 737.3200073242188 737.5999755859375 279276
297 2026-07-23 18:35:00+00:00 737.5800170898438 737.6900024414062 736.8300170898438 736.9299926757812 327191
298 2026-07-23 18:40:00+00:00 736.9299926757812 737.02001953125 736.6099853515625 736.8400268554688 415699
299 2026-07-23 18:45:00+00:00 736.8300170898438 737.4500122070312 736.8300170898438 737.2999877929688 583968
300 2026-07-23 18:50:00+00:00 737.2999877929688 737.3699951171875 736.6868286132812 736.719970703125 295039
301 2026-07-23 18:55:00+00:00 736.7100219726562 737.0599975585938 736.6300048828125 737.0499877929688 491634
302 2026-07-23 19:00:00+00:00 737.0800170898438 737.3900146484375 736.780029296875 736.7999877929688 340647
303 2026-07-23 19:05:00+00:00 736.7999877929688 736.9600219726562 736.7000122070312 736.72998046875 257690
304 2026-07-23 19:10:00+00:00 736.719970703125 736.875 736.469970703125 736.5999755859375 201871
305 2026-07-23 19:15:00+00:00 736.5999755859375 737.1599731445312 736.5900268554688 736.9500122070312 339486
306 2026-07-23 19:20:00+00:00 736.969970703125 737.6300048828125 736.9500122070312 737.6199951171875 451009
307 2026-07-23 19:25:00+00:00 737.6199951171875 737.9000244140625 737.3200073242188 737.47998046875 493815
308 2026-07-23 19:30:00+00:00 737.489990234375 737.739990234375 736.6799926757812 737.1500244140625 580631
309 2026-07-23 19:35:00+00:00 737.14501953125 737.219970703125 736.8300170898438 736.8900146484375 702327
310 2026-07-23 19:40:00+00:00 736.8699951171875 736.97998046875 736.25 736.25 1754270
311 2026-07-23 19:45:00+00:00 736.260009765625 736.4000244140625 735.719970703125 735.9249877929688 988958
312 2026-07-23 19:50:00+00:00 735.9299926757812 736.97998046875 735.9299926757812 736.7100219726562 2437408
313 2026-07-23 19:55:00+00:00 736.72998046875 738.6400146484375 736.72998046875 738.1699829101562 5113800
314 2026-07-24 13:30:00+00:00 738.510009765625 739.5 738.4199829101562 739.277099609375 1467113
315 2026-07-24 13:35:00+00:00 739.2899780273438 739.6599731445312 738.8800048828125 739.1099853515625 415105
316 2026-07-24 13:40:00+00:00 739.0999755859375 739.1400756835938 738.6599731445312 738.8101196289062 406628
317 2026-07-24 13:45:00+00:00 738.8099975585938 739.8800048828125 738.5700073242188 739.8200073242188 482528
318 2026-07-24 13:50:00+00:00 739.8099975585938 739.8099975585938 737.6900024414062 737.8400268554688 758616
319 2026-07-24 13:55:00+00:00 737.8300170898438 738.2000122070312 737.4299926757812 737.530029296875 480275
320 2026-07-24 14:00:00+00:00 738.1099853515625 738.6701049804688 737.6799926757812 737.7440185546875 777067
321 2026-07-24 14:05:00+00:00 737.7899780273438 739.489990234375 737.5700073242188 739.2999877929688 489734
322 2026-07-24 14:10:00+00:00 739.2650146484375 739.905029296875 738.8250122070312 739.5394897460938 932941
323 2026-07-24 14:15:00+00:00 739.5399780273438 739.7100219726562 738.0 738.5349731445312 993924
324 2026-07-24 14:20:00+00:00 738.530029296875 739.9500122070312 738.1599731445312 739.5399780273438 401286
325 2026-07-24 14:25:00+00:00 739.5599975585938 739.719970703125 738.969970703125 739.1799926757812 390349
326 2026-07-24 14:30:00+00:00 739.1900024414062 739.22998046875 738.3300170898438 738.3400268554688 412680
327 2026-07-24 14:35:00+00:00 738.3350219726562 738.8200073242188 737.9099731445312 738.0599975585938 331563
328 2026-07-24 14:40:00+00:00 738.030029296875 738.3499755859375 737.8400268554688 737.905029296875 667881
329 2026-07-24 14:45:00+00:00 737.8900146484375 738.5499877929688 737.4299926757812 738.530029296875 341313
330 2026-07-24 14:50:00+00:00 738.5349731445312 739.3200073242188 738.530029296875 739.010009765625 368611
331 2026-07-24 14:55:00+00:00 739.010009765625 740.7000122070312 739.010009765625 740.5900268554688 457825
332 2026-07-24 15:00:00+00:00 740.5900268554688 740.9600219726562 740.2100219726562 740.239990234375 1066983
333 2026-07-24 15:05:00+00:00 740.22998046875 740.6400146484375 739.6599731445312 739.6998901367188 310429
334 2026-07-24 15:10:00+00:00 739.6900024414062 742.1699829101562 739.5800170898438 741.8800048828125 772738
335 2026-07-24 15:15:00+00:00 741.8599853515625 743.0 741.3400268554688 741.4774780273438 736171
336 2026-07-24 15:20:00+00:00 741.4949951171875 742.6500244140625 741.4099731445312 742.3699951171875 453802
337 2026-07-24 15:25:00+00:00 742.39501953125 743.1300048828125 742.3300170898438 742.6099853515625 413289
338 2026-07-24 15:30:00+00:00 742.5999755859375 742.9000244140625 742.0499877929688 742.0800170898438 242596
339 2026-07-24 15:35:00+00:00 742.0449829101562 743.1580200195312 742.030029296875 743.0700073242188 318123
340 2026-07-24 15:40:00+00:00 743.0599975585938 743.25 742.0289916992188 742.2899780273438 273038
341 2026-07-24 15:45:00+00:00 742.2899780273438 742.6500244140625 742.0 742.1300048828125 215877
342 2026-07-24 15:50:00+00:00 742.135009765625 742.7899780273438 742.1300048828125 742.5599975585938 351940
343 2026-07-24 15:55:00+00:00 742.5599975585938 743.1099853515625 742.52001953125 743.0150146484375 310650
344 2026-07-24 16:00:00+00:00 743.0399780273438 743.239990234375 742.6400146484375 743.2100219726562 667546
345 2026-07-24 16:05:00+00:00 743.2000122070312 743.5499877929688 742.9400024414062 743.280029296875 454257
346 2026-07-24 16:10:00+00:00 743.2999877929688 743.719970703125 742.8499755859375 742.969970703125 340403
347 2026-07-24 16:15:00+00:00 743.0 743.3200073242188 742.8099975585938 743.1347045898438 255553
348 2026-07-24 16:20:00+00:00 743.1448974609375 743.5 743.0999755859375 743.3599243164062 200917
349 2026-07-24 16:25:00+00:00 743.3599853515625 743.5700073242188 743.1799926757812 743.1900024414062 178237
350 2026-07-24 16:30:00+00:00 743.1699829101562 743.1900024414062 742.3599853515625 742.5 390307
351 2026-07-24 16:35:00+00:00 742.5150146484375 742.739990234375 742.22998046875 742.27001953125 210497
352 2026-07-24 16:40:00+00:00 742.260009765625 742.260009765625 741.6400146484375 741.6500244140625 236074
353 2026-07-24 16:45:00+00:00 741.6400146484375 741.8099975585938 740.9500122070312 741.1898803710938 405413
354 2026-07-24 16:50:00+00:00 741.1900024414062 741.27001953125 740.739990234375 741.0700073242188 279526
355 2026-07-24 16:55:00+00:00 741.0490112304688 741.760009765625 740.8699951171875 741.4500122070312 199361
356 2026-07-24 17:00:00+00:00 741.469970703125 741.5999755859375 741.010009765625 741.1749877929688 182715
357 2026-07-24 17:05:00+00:00 741.1699829101562 741.3300170898438 740.3800048828125 740.4182739257812 386540
358 2026-07-24 17:10:00+00:00 740.4000244140625 741.0599975585938 740.2301025390625 740.9299926757812 255564
359 2026-07-24 17:15:00+00:00 740.9500122070312 741.469970703125 740.6599731445312 741.4099731445312 184125
360 2026-07-24 17:20:00+00:00 741.41650390625 741.489990234375 740.9072265625 741.2999877929688 327532
361 2026-07-24 17:25:00+00:00 741.2899780273438 741.8400268554688 740.989990234375 741.7100219726562 196903
362 2026-07-24 17:30:00+00:00 741.75 742.169189453125 741.530029296875 742.11181640625 156146
363 2026-07-24 17:35:00+00:00 742.1099853515625 742.1199951171875 741.4299926757812 741.510009765625 162452
364 2026-07-24 17:40:00+00:00 741.52001953125 741.5399780273438 741.0700073242188 741.0800170898438 184684
365 2026-07-24 17:45:00+00:00 741.0700073242188 741.3900146484375 740.9199829101562 741.3099975585938 162181
366 2026-07-24 17:50:00+00:00 741.3099975585938 741.5349731445312 740.969970703125 741.27001953125 270661
367 2026-07-24 17:55:00+00:00 741.2899780273438 741.3900146484375 740.969970703125 740.97998046875 244499
368 2026-07-24 18:00:00+00:00 740.989990234375 741.2899780273438 740.77001953125 740.8900146484375 188906
369 2026-07-24 18:05:00+00:00 740.8800048828125 741.27001953125 740.75 740.969970703125 338674
370 2026-07-24 18:10:00+00:00 741.02001953125 741.0750122070312 740.47998046875 740.5900268554688 257613
371 2026-07-24 18:15:00+00:00 740.594970703125 740.6500244140625 740.1400146484375 740.22998046875 387838
372 2026-07-24 18:20:00+00:00 740.1900024414062 740.2000122070312 739.719970703125 739.8099975585938 433579
373 2026-07-24 18:25:00+00:00 739.7999877929688 739.8800048828125 739.0 739.0349731445312 473174
374 2026-07-24 18:30:00+00:00 739.0499877929688 739.1599731445312 738.0700073242188 738.173828125 593684
375 2026-07-24 18:35:00+00:00 738.1900024414062 738.6300048828125 737.8300170898438 738.27001953125 879485
376 2026-07-24 18:40:00+00:00 738.27001953125 738.27001953125 737.6400146484375 737.8499755859375 439073
377 2026-07-24 18:45:00+00:00 737.8699951171875 738.7000122070312 737.7899780273438 738.3099975585938 392359
378 2026-07-24 18:50:00+00:00 738.2899780273438 738.5 738.030029296875 738.2050170898438 417134
379 2026-07-24 18:55:00+00:00 738.1900024414062 738.7100219726562 738.1400146484375 738.3800048828125 302475
380 2026-07-24 19:00:00+00:00 738.3900146484375 738.89501953125 738.2100219726562 738.280029296875 895698
381 2026-07-24 19:05:00+00:00 738.25 738.6500244140625 737.5599975585938 737.60498046875 779209
382 2026-07-24 19:10:00+00:00 737.6099853515625 738.22998046875 737.469970703125 737.5999755859375 705769
383 2026-07-24 19:15:00+00:00 737.5999755859375 737.8300170898438 737.4099731445312 737.7999877929688 1036183
384 2026-07-24 19:20:00+00:00 737.8200073242188 738.02001953125 737.2899780273438 737.4299926757812 521023
385 2026-07-24 19:25:00+00:00 737.4400024414062 737.8699951171875 737.2999877929688 737.6500244140625 459031
386 2026-07-24 19:30:00+00:00 737.719970703125 738.3800048828125 737.6599731445312 738.0900268554688 822160
387 2026-07-24 19:35:00+00:00 738.0800170898438 738.6599731445312 737.989990234375 738.1099853515625 1074359
388 2026-07-24 19:40:00+00:00 738.1099853515625 738.3400268554688 737.7899780273438 738.280029296875 800742
389 2026-07-24 19:45:00+00:00 738.2999877929688 738.4400024414062 737.75 738.3399047851562 1069183
390 2026-07-24 19:50:00+00:00 738.3400268554688 738.5800170898438 737.6400146484375 738.469970703125 1055109
391 2026-07-24 19:55:00+00:00 738.489990234375 739.0999755859375 738.0499877929688 738.8599853515625 3826336
392 2026-07-24 20:00:00+00:00 738.9299926757812 738.9299926757812 738.9299926757812 738.9299926757812 0
+16
View File
@@ -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
+9
View File
@@ -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
+39
View File
@@ -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"]
+2
View File
@@ -0,0 +1,2 @@
# Tests plus all project extras
-e .[notebooks,webapp,dev]
+2
View File
@@ -0,0 +1,2 @@
# Webapp runtime
-e .[webapp]
+2
View File
@@ -0,0 +1,2 @@
# Notebook environment
-e .[notebooks]
+37
View File
@@ -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("_")]
+781
View File
@@ -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."
),
}
+10
View File
@@ -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))
+146
View File
@@ -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}
+82
View File
@@ -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
+89
View File
@@ -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
```
+23
View File
@@ -0,0 +1,23 @@
FROM python:3.12-slim
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
PIP_NO_CACHE_DIR=1
WORKDIR /app
COPY pyproject.toml README.md ./
COPY src ./src
RUN pip install ".[webapp]"
COPY webapp ./webapp
RUN useradd --create-home --uid 10001 appuser && chown -R appuser:appuser /app
USER appuser
EXPOSE 8055
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD python -c "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8055/api/health', timeout=2)"
CMD ["uvicorn", "webapp.app:app", "--host", "0.0.0.0", "--port", "8055"]
+1
View File
@@ -0,0 +1 @@
"""FastAPI application package for the Adaptive Barrier Monitor demo."""
+123
View File
@@ -0,0 +1,123 @@
"""FastAPI web demo for the Adaptive Barrier Monitor."""
from __future__ import annotations
import re
import sys
from pathlib import Path
from typing import Any, Literal
import numpy as np
from fastapi import FastAPI
from fastapi.responses import HTMLResponse
from fastapi.staticfiles import StaticFiles
from pydantic import BaseModel, Field
# Support both an installed package (`pip install -e .`) and direct launches
# from a source checkout (`uvicorn webapp.app:app`).
REPO_ROOT = Path(__file__).resolve().parents[1]
SRC_DIR = REPO_ROOT / "src"
if str(SRC_DIR) not in sys.path:
sys.path.insert(0, str(SRC_DIR))
from adaptive_barrier import __version__
from adaptive_barrier.engine import (
DROP_FRACTION,
LOG_BARRIER,
RISE_FRACTION,
TYPICAL_ANNUAL_VOL,
WINDOW_MINUTES,
run_monte_carlo_simulation,
)
STATIC_DIR = REPO_ROOT / "webapp" / "static"
class SimulateRequest(BaseModel):
"""Validated Monte Carlo and detector-comparison parameters."""
S0: float = Field(100.0, gt=0.0, le=10000.0)
sigma_annual: float = Field(TYPICAL_ANNUAL_VOL, gt=0.0, le=2.0)
mu_annual: float = Field(0.0, ge=-0.5, le=0.5)
window_minutes: float = Field(1950.0, gt=0.0, le=39000.0)
n_paths: int = Field(20, ge=1, le=100)
n_steps: int = Field(500, ge=50, le=2000)
use_jumps: bool = Field(
False,
description="Use Merton jump diffusion. The Brownian miss proxy does not control jumps.",
)
jump_intensity: float = Field(25.0, ge=0.0, le=500.0)
jump_mean: float = Field(-0.02, ge=-0.5, le=0.5)
jump_sigma: float = Field(0.05, ge=0.0, le=0.5)
eps: float = Field(1e-3, gt=0.0, lt=1.0)
dt_cap_minutes: float | None = Field(None, gt=0.0, le=1440.0)
rng_seed: int | None = Field(None, ge=0, le=2_147_483_647)
drop_fraction: float = Field(DROP_FRACTION, gt=0.0, lt=1.0)
rise_fraction: float = Field(RISE_FRACTION, gt=0.0, le=1.0)
max_detection_lag_steps: int | None = Field(3, ge=0, le=100)
comparison_mode: Literal["equal_budget", "fixed_cadence"] = "equal_budget"
fixed_cadence_minutes: float = Field(60.0, gt=0.0, le=39000.0)
def _file_version(path: Path) -> str:
try:
stat = path.stat()
return f"{int(stat.st_mtime)}-{stat.st_size}"
except FileNotFoundError:
return "missing"
def _json_safe(obj: Any) -> Any:
if isinstance(obj, dict):
return {str(key): _json_safe(value) for key, value in obj.items()}
if isinstance(obj, (list, tuple)):
return [_json_safe(value) for value in obj]
if isinstance(obj, np.integer):
return int(obj)
if isinstance(obj, np.floating):
value = float(obj)
return value if np.isfinite(value) else None
if isinstance(obj, np.ndarray):
return _json_safe(obj.tolist())
if isinstance(obj, np.bool_):
return bool(obj)
return obj
app = FastAPI(title="Adaptive Barrier Monitor", version=__version__)
app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static")
@app.get("/")
def index() -> HTMLResponse:
html = (STATIC_DIR / "index.html").read_text(encoding="utf-8")
html = re.sub(
r'app\.css\?v=[^\s"\']+',
f"app.css?v={_file_version(STATIC_DIR / 'app.css')}",
html,
)
html = re.sub(
r'app\.js\?v=[^\s"\']+',
f"app.js?v={_file_version(STATIC_DIR / 'app.js')}",
html,
)
return HTMLResponse(content=html)
@app.get("/api/health")
def health() -> dict:
return {
"ok": True,
"version": __version__,
"default_sigma_annual": TYPICAL_ANNUAL_VOL,
"default_window_minutes": WINDOW_MINUTES,
"drop_fraction": DROP_FRACTION,
"rise_fraction": RISE_FRACTION,
"log_barrier": LOG_BARRIER,
}
@app.post("/api/simulate")
def simulate(req: SimulateRequest) -> dict:
result = run_monte_carlo_simulation(**req.model_dump())
return _json_safe(result)
+475
View File
@@ -0,0 +1,475 @@
/* Theme tokens and page structure intentionally mirror ClimbingBoardGPT. */
:root {
--base00: #1A1B26;
--base01: #16161E;
--base02: #2F3549;
--base03: #444B6A;
--base04: #787C99;
--base05: #A9B1D6;
--base07: #D5D6DB;
--base08: #F7768E;
--base0a: #0DB9D7;
--base0b: #9ECE6A;
--base0c: #B4F9F8;
--base0d: #2AC3DE;
--base0e: #BB9AF7;
--base0f: #F7768E;
--bg: var(--base00);
--off-bg: var(--base01);
--inner-bg: var(--base02);
--fg: var(--base05);
--off-fg: var(--base04);
--muted: var(--base03);
--link: var(--base0d);
--hover: var(--base0c);
--highlight: var(--base0a);
--logo: var(--base0b);
--danger: var(--base08);
--border: rgba(120, 124, 153, 0.3);
--sans: "Inter", sans-serif;
--mono: "Fira Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
}
* { box-sizing: border-box; }
body {
margin: 0;
font-family: var(--sans);
font-size: 16px;
line-height: 1.6rem;
background: var(--bg);
color: var(--fg);
}
.site-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 1.5rem;
max-width: 78rem;
margin: 1rem auto 0;
padding: 0 1rem;
}
.eyebrow {
margin: 0;
color: var(--logo);
font-size: 1rem;
}
.site-header h1 {
margin: 0;
font-size: 1rem;
font-weight: 600;
}
.site-header h1::before { content: none; }
.site-header p {
margin: 0;
color: var(--off-fg);
}
.health {
flex-shrink: 0;
font-size: 0.78rem;
color: var(--highlight);
white-space: nowrap;
border: 1px solid var(--border);
padding: 0.25rem 0.5rem;
background: var(--inner-bg);
}
.layout {
display: grid;
grid-template-columns: 22rem minmax(0, 1fr);
grid-template-rows: auto 1fr;
grid-template-areas:
"col-top col-viewer"
"col-info col-viewer";
gap: 2rem;
padding: 2rem 1rem 1rem;
max-width: 78rem;
margin: 0 auto;
align-items: start;
}
#col-top { grid-area: col-top; }
#col-viewer { grid-area: col-viewer; }
#col-info { grid-area: col-info; }
.controls {
display: flex;
flex-direction: column;
gap: 1rem;
}
.card, .result-card {
background: var(--off-bg);
border: 1px solid var(--border);
padding: 1rem;
}
.card h2, .result-card h2 {
margin: 0 0 1rem;
font-size: 1rem;
font-weight: 600;
color: var(--fg);
}
.card h2::before, .result-card h2::before { content: none; }
label {
display: block;
margin: 0.7rem 0;
font-size: 0.82rem;
color: var(--off-fg);
}
input, select, textarea {
display: block;
width: 100%;
margin-top: 0.28rem;
border: 1px solid var(--border);
padding: 0.6rem 0.7rem;
font: inherit;
color: var(--fg);
background: var(--inner-bg);
}
input:focus, select:focus, textarea:focus {
outline: 2px solid rgba(137, 221, 255, 0.28);
border-color: var(--hover);
}
button {
width: 100%;
border: 1px solid var(--link);
padding: 0.68rem 0.9rem;
margin-top: 0.4rem;
font-weight: 700;
color: var(--bg);
background: var(--link);
cursor: pointer;
font-family: var(--sans);
}
button:hover {
border-color: var(--hover);
background: var(--hover);
}
button:disabled { opacity: 0.55; cursor: not-allowed; }
/* Keep the run control reachable while scrolling through the long parameter card. */
#mc-run-btn {
position: sticky;
bottom: 0.6rem;
z-index: 5;
box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.3);
}
.card.collapsible > h2 {
display: flex;
align-items: center;
cursor: pointer;
user-select: none;
}
.card.collapsible > h2::after {
content: "▾";
font-size: 2rem;
color: var(--muted);
margin-left: auto;
padding-left: 0.5rem;
flex-shrink: 0;
}
.card.collapsible.collapsed > h2::after { content: "▸"; }
.card.collapsible > h2:hover::after { color: var(--off-fg); }
.card.collapsible.collapsed > *:not(h2) { display: none; }
.field-help {
display: block;
margin-top: 0.35rem;
color: var(--muted);
font-size: 0.72rem;
line-height: 1.35;
}
.checkbox-label {
display: flex;
align-items: center;
gap: 0.55rem;
color: var(--fg);
}
.checkbox-label input {
width: auto;
margin: 0;
}
input[type="range"] {
padding: 0;
height: 6px;
-webkit-appearance: none;
appearance: none;
background: var(--inner-bg);
border: 1px solid var(--border);
cursor: pointer;
}
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
width: 16px;
height: 16px;
background: var(--link);
border-radius: 50%;
cursor: pointer;
}
.range-row {
display: flex;
gap: 0.55rem;
align-items: center;
}
.range-row input[type="range"] { flex: 1; }
.range-row input[type="number"] {
width: 7.5rem;
flex-shrink: 0;
-moz-appearance: textfield;
}
.range-row input[type="number"]::-webkit-inner-spin-button,
.range-row input[type="number"]::-webkit-outer-spin-button {
-webkit-appearance: none;
margin: 0;
}
.note p, .small {
color: var(--off-fg);
font-size: 0.82rem;
line-height: 1.45;
}
.note p:first-of-type { margin-top: 0; }
.note p:last-child { margin-bottom: 0; }
.result-header {
text-align: center;
margin-bottom: 0.85rem;
}
.result-header h2 { margin-bottom: 0.25rem; }
.result-header p {
margin: 0;
color: var(--off-fg);
font-size: 0.84rem;
}
.headline {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 0.6rem;
max-width: 820px;
margin: 0 auto 1rem;
}
.tile {
display: flex;
flex-direction: column;
align-items: center;
padding: 0.6rem 0.4rem;
border: 1px solid var(--border);
background: var(--inner-bg);
text-align: center;
}
.tile-num {
font-size: 1.3rem;
font-weight: 700;
color: var(--highlight);
line-height: 1.1;
white-space: nowrap;
}
.tile-lab {
font-size: 0.74rem;
font-weight: 600;
color: var(--fg);
margin-top: 0.2rem;
}
.tile-sub {
font-size: 0.66rem;
color: var(--muted);
margin-top: 0.1rem;
line-height: 1.25;
}
.result-note {
max-width: 760px;
margin: 0 auto 0.85rem;
color: var(--off-fg);
font-size: 0.78rem;
line-height: 1.45;
text-align: center;
}
.chart-stage {
width: 100%;
max-width: 960px;
margin: 0 auto;
min-height: 420px;
border: 1px solid var(--border);
background: var(--bg);
overflow: hidden;
}
.chart-stage .js-plotly-plot { width: 100% !important; }
.warning-box {
margin: 0.7rem auto 0.85rem;
max-width: 760px;
border: 1px solid rgba(255, 203, 107, 0.55);
background: rgba(255, 203, 107, 0.12);
color: var(--highlight);
padding: 0.65rem 0.8rem;
font-size: 0.8rem;
text-align: left;
white-space: pre-line;
}
.advanced-opts {
margin-top: 0.9rem;
}
.advanced-opts summary {
cursor: pointer;
font-size: 0.78rem;
color: var(--muted);
user-select: none;
}
.advanced-opts summary:hover { color: var(--highlight); }
.method-note {
max-width: 900px;
margin-left: auto;
margin-right: auto;
color: var(--off-fg);
font-size: 0.8rem;
line-height: 1.45;
}
.method-note ol {
margin: 0.65rem 0 0.45rem;
padding-left: 1.25rem;
}
.method-note li + li { margin-top: 0.32rem; }
.method-note p { margin: 0.45rem 0 0; }
.method-note strong { color: var(--fg); }
.explain dl { margin: 0; }
.explain dt {
color: var(--highlight);
font-size: 0.78rem;
margin-top: 0.75rem;
}
.explain dt:first-child { margin-top: 0; }
.explain dd {
margin: 0.22rem 0 0;
color: var(--off-fg);
font-size: 0.78rem;
line-height: 1.45;
}
.explain p {
color: var(--off-fg);
font-size: 0.82rem;
line-height: 1.45;
}
.explain p:first-of-type { margin-top: 0; }
.explain p:last-child { margin-bottom: 0; }
.link-list {
margin: 0;
padding-left: 1.1rem;
color: var(--off-fg);
font-size: 0.82rem;
line-height: 1.6;
}
.link-list li::marker {
content: '·\00A0\00A0';
color: var(--muted);
}
.json-block {
margin-top: 1rem;
color: var(--off-fg);
}
.json-block summary {
cursor: pointer;
font-size: 0.78rem;
color: var(--muted);
}
.json-block summary:hover { color: var(--highlight); }
.json-block pre {
overflow: auto;
max-height: 300px;
padding: 1rem;
background: var(--inner-bg);
color: var(--off-fg);
border: 1px solid var(--border);
font-size: 0.76rem;
}
.site-footer {
display: flex;
flex-wrap: wrap;
gap: 0.5rem 1.5rem;
max-width: 78rem;
margin: 0 auto 1.5rem;
padding: 0 1rem;
color: var(--off-fg);
font-size: 0.82rem;
}
.site-footer a, .link-list a {
color: var(--link);
transition: color 0.15s ease;
}
.site-footer a:hover, .link-list a:hover { color: var(--hover); }
@media (max-width: 900px) {
.layout {
grid-template-columns: 1fr;
grid-template-rows: auto auto auto;
grid-template-areas:
"col-top"
"col-viewer"
"col-info";
}
.site-header { flex-direction: column; }
.headline { grid-template-columns: repeat(2, 1fr); }
}
@media (max-width: 520px) {
.headline { grid-template-columns: 1fr; }
.range-row { align-items: stretch; flex-direction: column; }
.range-row input[type="number"] { width: 100%; }
}
+373
View File
@@ -0,0 +1,373 @@
/*
* Browser-side controller for the Adaptive Barrier Monitor demo (Monte Carlo showcase).
*
* Runs a simulation and renders each path with adaptive sampling (cyan) and
* either an equal-budget or independently fixed-cadence baseline (grey).
*/
const state = { lastResult: null };
// ---- helpers ----
function $(id) { return document.getElementById(id); }
async function fetchJson(url, options = {}) {
const resp = await fetch(url, options);
const text = await resp.text();
let payload;
try { payload = text ? JSON.parse(text) : {}; } catch { payload = { detail: text }; }
if (!resp.ok) {
const detail = payload.detail ?? payload;
throw new Error(typeof detail === "string" ? detail : JSON.stringify(detail, null, 2));
}
return payload;
}
function setBusy(button, busy) {
button.disabled = busy;
button.textContent = busy ? "Working…" : (button.dataset.label || button.textContent);
}
function showWarnings(warnings) {
const box = $("warning-box");
const norm = (warnings || []).filter(Boolean).map(String);
if (norm.length === 0) { box.hidden = true; box.textContent = ""; return; }
box.hidden = false;
box.textContent = norm.join("\n");
}
// ---- slider ↔ number sync ----
function bindRange(id) {
const range = $(id);
const num = $(id + "-val");
if (!range || !num) return;
range.addEventListener("input", () => { num.value = parseFloat(range.value); });
num.addEventListener("change", () => { range.value = parseFloat(num.value); });
}
function bindLogRange(id) {
const range = $(id);
const num = $(id + "-val");
if (!range || !num) return;
range.addEventListener("input", () => { num.value = Number(range.value).toExponential(4); });
num.addEventListener("change", () => { range.value = parseFloat(num.value); });
}
function updateComparisonModeCopy() {
const mode = $("mc-comparison-mode").value;
const fixedCadence = mode === "fixed_cadence";
$("mc-fixed-cadence-row").hidden = !fixedCadence;
if (fixedCadence) {
$("fixed-explanation").innerHTML =
'A <strong>fixed-cadence</strong> sampler that observes every chosen number of minutes, independently of the adaptive sample count.';
$("headline-explanation").textContent =
'The schedules use independent sample counts. Compare detections, lag, and total observations to see the qualitycost trade-off.';
$("comparison-note").innerHTML =
'The fixed monitor samples at the selected cadence. The experiment compares <strong>detection quality and observation cost</strong>.';
$("allocation-method").innerHTML =
'<strong>Allocate samples:</strong> adaptive intervals shrink near the closest barrier; the fixed monitor samples at the selected cadence.';
$("method-conclusion").textContent =
'The headline compares detections, mean lag, and total observations under independent schedules.';
} else {
$("fixed-explanation").innerHTML =
'An exactly equal-budget <strong>fixed-rate</strong> sampler: each path receives the same number of observations as its adaptive counterpart, spread uniformly.';
$("headline-explanation").textContent =
'Same paths and exactly the same sample budget. The comparison reports which schedule confirms more lower and upper barrier events before the detection deadline.';
$("comparison-note").innerHTML =
'The adaptive and fixed monitors receive the <strong>same number of samples on every path</strong>. The experiment therefore compares sample placement—not computational cost.';
$("allocation-method").innerHTML =
'<strong>Allocate samples:</strong> adaptive intervals shrink near the closest barrier; the fixed monitor receives the exact same sample count, spaced uniformly.';
$("method-conclusion").textContent =
'The headline compares detections and mean lag under an equal sample budget.';
}
}
// ---- Plotly helpers ----
const PLOTLY_CONFIG = {
displayModeBar: true,
modeBarButtonsToRemove: ["lasso2d", "select2d"],
displaylogo: false,
responsive: true,
};
const PLOTLY_LAYOUT = {
font: { color: "#A9B1D6", family: "Inter, sans-serif" },
paper_bgcolor: "#1A1B26",
plot_bgcolor: "#1A1B26",
xaxis: { gridcolor: "rgba(120,124,153,0.15)", zerolinecolor: "rgba(120,124,153,0.3)" },
yaxis: { gridcolor: "rgba(120,124,153,0.15)", zerolinecolor: "rgba(120,124,153,0.3)" },
margin: { l: 60, r: 30, t: 20, b: 50 },
legend: { font: { color: "#A9B1D6" }, x: 0.01, y: 0.99, bgcolor: "rgba(26,27,38,0.6)" },
};
function renderPlot(data, layoutOverrides = {}) {
Plotly.newPlot("chart-stage", data, { ...PLOTLY_LAYOUT, ...layoutOverrides }, PLOTLY_CONFIG);
}
// ---- Monte Carlo ----
async function runMonteCarlo() {
const button = $("mc-run-btn");
setBusy(button, true);
showWarnings([]);
$("result-subtitle").textContent = "Running simulation…";
try {
const thresholdPct = parseFloat($("mc-threshold").value);
const risePct = parseFloat($("mc-rise-threshold").value);
const dtcapRaw = $("mc-dtcap").value.trim();
const seedRaw = $("mc-seed").value.trim();
const comparisonMode = $("mc-comparison-mode").value;
const payload = {
S0: 100.0,
sigma_annual: parseFloat($("mc-sigma").value),
mu_annual: parseFloat($("mc-mu").value),
window_minutes: parseFloat($("mc-window").value),
n_paths: parseInt($("mc-paths").value),
n_steps: parseInt($("mc-steps").value),
use_jumps: $("mc-jumps").checked,
jump_intensity: 25.0,
jump_mean: -0.02,
jump_sigma: 0.05,
eps: parseFloat($("mc-eps").value),
drop_fraction: thresholdPct / 100.0,
rise_fraction: risePct / 100.0,
dt_cap_minutes: dtcapRaw ? parseFloat(dtcapRaw) : null,
rng_seed: seedRaw ? parseInt(seedRaw) : null,
max_detection_lag_steps: parseInt($("mc-max-lag").value),
comparison_mode: comparisonMode,
fixed_cadence_minutes: parseFloat($("mc-fixed-cadence").value),
};
const r = await fetchJson("/api/simulate", {
method: "POST", headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
state.lastResult = r;
const colors = ["#2AC3DE", "#BB9AF7", "#9ECE6A", "#0DB9D7", "#B4F9F8",
"#FF9E64", "#7DCFFF", "#C0CAF5", "#73DACA", "#F7768E"];
const t = r.times_minutes;
const t0 = t[0], t1 = t[t.length - 1];
const traces = [];
for (let i = 0; i < r.paths.length; i++) {
const p = r.paths[i];
const c = colors[i % colors.length];
traces.push({
x: t, y: p.prices, type: "scatter", mode: "lines",
line: { color: c, width: 0.8, opacity: 0.5 }, name: `Path ${i + 1}`, showlegend: i < 5,
});
if (p.sample_times.length) {
traces.push({
x: p.sample_times, y: p.sample_prices, type: "scatter", mode: "markers",
marker: { color: "#2AC3DE", size: 6, symbol: "circle-open", opacity: 0.9 },
name: "adaptive", showlegend: i === 0,
});
}
if (p.fixed_sample_times.length) {
traces.push({
x: p.fixed_sample_times, y: p.fixed_sample_prices, type: "scatter", mode: "markers",
marker: { color: "#787C99", size: 3, opacity: 0.5 },
name: r.comparison_mode === "fixed_cadence" ? "fixed cadence" : "fixed equal-budget", showlegend: i === 0,
});
}
}
// Lower barrier (drop)
traces.push({
x: [t0, t1], y: [r.lower_barrier_price, r.lower_barrier_price], type: "scatter", mode: "lines",
line: { color: "#F7768E", width: 2, dash: "dash" }, name: `${thresholdPct}% lower`,
});
// Upper barrier (rise)
traces.push({
x: [t0, t1], y: [r.upper_barrier_price, r.upper_barrier_price], type: "scatter", mode: "lines",
line: { color: "#9ECE6A", width: 2, dash: "dash" }, name: `+${risePct}% upper`,
});
// Lower breach markers (red X)
const bxDown = [], byDown = [];
for (const p of r.paths) {
if (p.lower_breach_idx !== null) { bxDown.push(t[p.lower_breach_idx]); byDown.push(p.prices[p.lower_breach_idx]); }
}
if (bxDown.length) {
traces.push({
x: bxDown, y: byDown, type: "scatter", mode: "markers",
marker: { color: "#F7768E", size: 13, symbol: "x", line: { width: 3 } },
name: `↓ breaches (${bxDown.length})`,
});
}
// Upper breach markers (green triangles)
const bxUp = [], byUp = [];
for (const p of r.paths) {
if (p.upper_breach_idx !== null) { bxUp.push(t[p.upper_breach_idx]); byUp.push(p.prices[p.upper_breach_idx]); }
}
if (bxUp.length) {
traces.push({
x: bxUp, y: byUp, type: "scatter", mode: "markers",
marker: { color: "#9ECE6A", size: 13, symbol: "triangle-up", line: { width: 3 } },
name: `↑ breaches (${bxUp.length})`,
});
}
renderPlot(traces, {
xaxis: { title: "Time (minutes)" },
yaxis: { title: "Price ($)" },
showlegend: r.paths.length <= 10,
});
// Headline tiles — use explicit event counts and method labels.
// A barrier event is the first lower-barrier or upper-barrier crossing on a path.
// One path can therefore contribute up to two events.
const nEvents = r.n_barrier_events ?? r.n_breaches;
const nLowerEvents = r.n_lower_events ?? r.n_lower_breaches;
const nUpperEvents = r.n_upper_events ?? r.n_upper_breaches;
const number = new Intl.NumberFormat();
const directionSummary = (lowerCaught, upperCaught) => {
const lower = nLowerEvents > 0
? `${lowerCaught} of ${nLowerEvents} lower`
: "↓ no lower events";
const upper = nUpperEvents > 0
? `${upperCaught} of ${nUpperEvents} upper`
: "↑ no upper events";
return `${lower} · ${upper}`;
};
$("hl-adaptive").textContent = nEvents > 0
? `${r.adaptive_detections} of ${nEvents}` : "No events";
$("hl-fixed").textContent = nEvents > 0
? `${r.fixed_detections} of ${nEvents}` : "No events";
$("hl-adaptive-sub").textContent = directionSummary(
r.adaptive_lower_detections,
r.adaptive_upper_detections,
);
$("hl-fixed-sub").textContent = directionSummary(
r.fixed_lower_detections,
r.fixed_upper_detections,
);
const equalBudgetMode = r.comparison_mode === "equal_budget";
$("hl-samples").textContent = equalBudgetMode
? `${number.format(r.adaptive_total_samples)} each`
: `A ${number.format(r.adaptive_total_samples)} · F ${number.format(r.fixed_total_samples)}`;
$("hl-samples-sub").textContent = equalBudgetMode
? "same count on every path"
: `adaptive · fixed every ${number.format(r.fixed_cadence_minutes)} min`;
const adaptiveLag = (r.mean_detection_lag !== null && r.mean_detection_lag !== undefined)
? r.mean_detection_lag.toFixed(1) : "";
const fixedLag = (r.mean_fixed_detection_lag !== null && r.mean_fixed_detection_lag !== undefined)
? r.mean_fixed_detection_lag.toFixed(1) : "";
$("hl-lag").textContent = `${adaptiveLag} vs ${fixedLag}`;
const breachedPaths = r.n_paths_with_any_breach ?? r.paths.filter(
(path) => path.lower_breach_idx !== null || path.upper_breach_idx !== null,
).length;
$("result-subtitle").textContent =
`${nEvents} barrier events across ${breachedPaths} of ${r.n_paths} paths` +
` · ${nLowerEvents} lower, ${nUpperEvents} upper` +
` · deadline ≤${r.max_detection_lag_steps} grid steps` +
(r.comparison_mode === "fixed_cadence"
? ` · fixed every ${r.fixed_cadence_minutes} min`
: " · equal budget") +
(r.use_jumps ? " · Merton jumps" : " · pure GBM");
$("stats-json").textContent = JSON.stringify({
threshold_pct: thresholdPct,
rise_pct: risePct,
seed: payload.rng_seed,
n_paths: r.n_paths,
n_paths_with_any_breach: r.n_paths_with_any_breach,
n_barrier_events: r.n_barrier_events ?? r.n_breaches,
n_lower_events: r.n_lower_events ?? r.n_lower_breaches,
n_upper_events: r.n_upper_events ?? r.n_upper_breaches,
adaptive_detections: r.adaptive_detections,
adaptive_lower_detections: r.adaptive_lower_detections,
adaptive_upper_detections: r.adaptive_upper_detections,
fixed_detections: r.fixed_detections,
fixed_lower_detections: r.fixed_lower_detections,
fixed_upper_detections: r.fixed_upper_detections,
adaptive_total_samples: r.adaptive_total_samples,
fixed_total_samples: r.fixed_total_samples,
mean_detection_lag: r.mean_detection_lag,
mean_lower_detection_lag: r.mean_lower_detection_lag,
mean_upper_detection_lag: r.mean_upper_detection_lag,
sigma_annual: r.sigma_annual,
mu_annual: r.mu_annual,
eps: r.eps,
drop_fraction: r.drop_fraction,
rise_fraction: r.rise_fraction,
dt_cap_minutes: r.dt_cap_minutes,
max_detection_lag_steps: r.max_detection_lag_steps,
comparison_mode: r.comparison_mode,
fixed_cadence_minutes: r.fixed_cadence_minutes,
grid_step_minutes: r.grid_step_minutes,
model_scope: r.model_scope,
}, null, 2);
const warnings = [];
if (r.use_jumps) {
warnings.push("Jump stress test: ε is derived from a continuous diffusion and does not bound missed jump events.");
}
if (r.comparison_mode === "fixed_cadence" && r.fixed_cadence_minutes < r.grid_step_minutes) {
warnings.push(`The requested fixed cadence (${r.fixed_cadence_minutes} min) is finer than the simulation grid (${r.grid_step_minutes.toFixed(2)} min), so it is limited to one sample per grid point.`);
}
if (r.n_breaches === 0) {
warnings.push("No barrier events occurred in this run. Increase the horizon/volatility, lower the thresholds, or choose another seed.");
}
showWarnings(warnings);
} catch (err) {
showWarnings([err.message]);
} finally {
setBusy(button, false);
}
}
/** Initialize collapsible cards to match the ClimbingBoardGPT interaction. */
function initCollapsibleCards() {
document.querySelectorAll(".card.collapsible > h2").forEach((heading) => {
const card = heading.parentElement;
heading.addEventListener("click", () => {
card.classList.toggle("collapsed");
});
});
}
// ---- init ----
async function init() {
$("mc-run-btn").dataset.label = "Run simulation";
try {
await fetchJson("/api/health");
$("health").textContent = "ready";
} catch {
$("health").textContent = "offline";
}
bindRange("mc-sigma");
bindLogRange("mc-eps");
updateComparisonModeCopy();
initCollapsibleCards();
$("mc-comparison-mode").addEventListener("change", async () => {
updateComparisonModeCopy();
await runMonteCarlo();
});
$("mc-run-btn").addEventListener("click", runMonteCarlo);
// Enter opens on a compelling scenario immediately.
await runMonteCarlo();
}
init().catch(err => {
$("health").textContent = `Error: ${err.message}`;
showWarnings(["Initialization error: " + err.message]);
console.error(err);
});
+4
View File
@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<rect width="32" height="32" rx="4" fill="#1A1B26"/>
<text x="16" y="23" text-anchor="middle" font-size="20" fill="#F7768E" font-family="sans-serif"></text>
</svg>

After

Width:  |  Height:  |  Size: 232 B

+206
View File
@@ -0,0 +1,206 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Adaptive Barrier Monitor</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600&display=swap">
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg">
<script src="https://cdn.plot.ly/plotly-3.0.1.min.js"></script>
<link rel="stylesheet" href="/static/app.css?v=1" />
</head>
<body>
<header class="site-header">
<div>
<p class="eyebrow">Adaptive Barrier Monitor</p>
<h1>State-dependent sampling near price barriers</h1>
<p>Monte Carlo demo comparing adaptive sampling with equal-budget and fixed-cadence baselines.</p>
</div>
<div id="health" class="health">Loading…</div>
</header>
<main class="layout">
<!-- Left column, row 1: interactive controls. -->
<section class="controls" id="col-top">
<div class="card collapsible" id="card-simulation">
<h2>Simulation parameters</h2>
<label>Comparison mode
<select id="mc-comparison-mode">
<option value="equal_budget">Equal budget</option>
<option value="fixed_cadence">Fixed cadence</option>
</select>
<span class="field-help">Equal budget isolates sample placement. Fixed cadence compares detection and observation cost against an independently chosen interval.</span>
</label>
<label id="mc-fixed-cadence-row" hidden>Fixed cadence (minutes)
<input id="mc-fixed-cadence" type="number" min="0.1" max="39000" step="0.1" value="60" />
<span class="field-help">The fixed monitor samples at this interval, plus the start and end of the window. Smaller values use more observations.</span>
</label>
<label>Annual volatility (σ)
<div class="range-row">
<input id="mc-sigma" type="range" min="0.05" max="1.0" step="0.01" value="0.30" />
<input id="mc-sigma-val" type="number" min="0.05" max="1.0" step="0.01" value="0.30" />
</div>
<span class="field-help">Annualised volatility. Higher values produce wider price swings and more barrier events.</span>
</label>
<label>Annual drift (μ)
<input id="mc-mu" type="number" min="-0.5" max="0.5" step="0.01" value="0.07" />
<span class="field-help">Average annual return. Positive values tilt paths upward; negative values push them toward the lower barrier.</span>
</label>
<label>Window (minutes)
<input id="mc-window" type="number" min="5" max="39000" step="5" value="1950" />
<span class="field-help">1950 minutes is about five trading days. Longer windows tend to produce more events.</span>
</label>
<label>Number of paths
<input id="mc-paths" type="number" min="1" max="100" step="1" value="20" />
<span class="field-help">More paths give steadier aggregate results but take longer to render.</span>
</label>
<label>Steps per path
<input id="mc-steps" type="number" min="50" max="2000" step="10" value="500" />
<span class="field-help">The fine simulation grid used as the reference history. More steps resolve shorter-lived crossings.</span>
</label>
<label>Drop threshold (%)
<input id="mc-threshold" type="number" min="0.1" max="50" step="0.1" value="5" />
<span class="field-help">Percentage decline from the initial price that defines a lower-barrier event.</span>
</label>
<label>Rise threshold (%)
<input id="mc-rise-threshold" type="number" min="0.1" max="50" step="0.1" value="10" />
<span class="field-help">Percentage increase from the initial price that defines an upper-barrier event.</span>
</label>
<label>Miss budget (ε)
<div class="range-row">
<input id="mc-eps" type="range" min="1e-6" max="0.1" step="1e-6" value="0.001" />
<input id="mc-eps-val" type="number" min="1e-6" max="0.1" step="0.0001" value="0.001" />
</div>
<span class="field-help">Local diffusion-model design parameter. Lower ε produces denser adaptive sampling; it is not a global guarantee and does not cover jumps.</span>
</label>
<label>Max interval cap (minutes)
<input id="mc-dtcap" type="number" min="1" max="1440" step="1" placeholder="auto" />
<span class="field-help">Leave empty for an automatic cap of window/15. Lower values force denser sampling everywhere.</span>
</label>
<label>Detection deadline (grid steps)
<input id="mc-max-lag" type="number" min="0" max="100" step="1" value="3" />
<span class="field-help">An event counts as detected only if a sample still lies beyond the barrier within this many reference-grid steps.</span>
</label>
<label>RNG seed
<input id="mc-seed" type="number" min="0" max="100000" step="1" placeholder="random" />
<span class="field-help">Leave empty for a new run, or enter a seed to reproduce and share the same paths.</span>
</label>
<label>
<span class="checkbox-label">
<input id="mc-jumps" type="checkbox" /> Include jumps (Merton jump-diffusion)
</span>
<span class="field-help">Adds sudden moves that the diffusion-derived schedule cannot anticipate. Treat this as a model-risk stress test.</span>
</label>
<button id="mc-run-btn">Run simulation</button>
</div>
</section>
<!-- Right column: results and simulated paths. -->
<section class="viewer" id="col-viewer">
<div class="result-card">
<div class="result-header">
<h2 id="result-title">Monte Carlo Sandbox</h2>
<p id="result-subtitle">Running…</p>
</div>
<div class="headline">
<div class="tile">
<span class="tile-num" id="hl-adaptive"></span>
<span class="tile-lab">events detected — adaptive</span>
<span class="tile-sub" id="hl-adaptive-sub">↓ lower · ↑ upper</span>
</div>
<div class="tile">
<span class="tile-num" id="hl-fixed"></span>
<span class="tile-lab">events detected — fixed</span>
<span class="tile-sub" id="hl-fixed-sub">↓ lower · ↑ upper</span>
</div>
<div class="tile">
<span class="tile-num" id="hl-samples"></span>
<span class="tile-lab">total samples</span>
<span class="tile-sub" id="hl-samples-sub">matched path by path</span>
</div>
<div class="tile">
<span class="tile-num" id="hl-lag"></span>
<span class="tile-lab">mean detection lag</span>
<span class="tile-sub">adaptive vs fixed · detected events only</span>
</div>
</div>
<p class="result-note" id="comparison-note">
The adaptive and fixed monitors receive the <strong>same number of samples on every path</strong>.
The experiment therefore compares sample placement—not computational cost.
</p>
<div id="warning-box" class="warning-box" hidden></div>
<div id="chart-stage" class="chart-stage">
<div style="padding:3rem; text-align:center; color:var(--muted)">Running simulation…</div>
</div>
<details class="advanced-opts method-note" open>
<summary>How the Monte Carlo comparison works</summary>
<ol>
<li><strong>Simulate:</strong> generate independent GBM or Merton jump-diffusion price paths on a fine grid.</li>
<li><strong>Mark events:</strong> record each path's first lower- and upper-barrier crossing.</li>
<li id="allocation-method"><strong>Allocate samples:</strong> adaptive intervals shrink near the closest barrier; the fixed monitor receives the same sample count, spaced uniformly.</li>
<li><strong>Score:</strong> an event is detected when a sampled price remains beyond the barrier within the selected deadline.</li>
</ol>
<p>
<span id="method-conclusion">The headline compares detections and mean lag under an equal sample budget.</span>
The miss budget <strong>ε</strong> is a local diffusion-model design parameter, not a global guarantee.
</p>
</details>
<details class="json-block">
<summary>Raw result JSON</summary>
<pre id="stats-json">{}</pre>
</details>
</div>
</section>
<!-- Left column, row 2: supporting information, mirroring ClimbingBoardGPT. -->
<section class="controls" id="col-info">
<div class="card explain">
<h2>How to read it</h2>
<dl>
<dt>Open cyan circles</dt>
<dd>Adaptive observations. The interval shrinks quadratically as the log-price approaches the nearer barrier.</dd>
<dt>Grey dots</dt>
<dd id="fixed-explanation">An equal-budget fixed sampler with the same number of observations on each path, spaced uniformly.</dd>
<dt>Red × and green ▲</dt>
<dd>The first lower- and upper-barrier events on the fine reference grid.</dd>
<dt>Headline</dt>
<dd id="headline-explanation">Same paths and the same sample budget. Compare how many events each schedule confirms before the deadline.</dd>
</dl>
</div>
<div class="card note">
<h2>Research demo caveat</h2>
<p>The adaptive law is derived from a continuous diffusion model. Discrete grids, estimated volatility, latency, and jumps weaken any literal miss-probability guarantee.</p>
</div>
<div class="card explain">
<h2>How this works</h2>
<p>The app simulates price paths, marks first barrier crossings, and compares two observation schedules on exactly the same paths.</p>
<p>Adaptive intervals use a Brownian-bridge proxy and shrink with squared distance to the nearest barrier. Fixed observations are either equal-budget or independently spaced at a chosen cadence.</p>
</div>
<div class="card note">
<h2>Links</h2>
<ul class="link-list">
<li><a href="https://pawelsarkowicz.xyz" target="_blank" rel="noreferrer">pawelsarkowicz.xyz</a></li>
<li><a href="https://github.com/psark007/adaptive-barrier-monitor" target="_blank" rel="noreferrer">Adaptive Barrier Monitor repo</a></li>
<li><a href="https://github.com/psark007/adaptive-barrier-monitor/blob/main/LICENSE" target="_blank" rel="noreferrer">License</a></li>
</ul>
</div>
</section>
</main>
<footer class="site-footer">
<span>© Pawel Sarkowicz</span>
</footer>
<script src="/static/app.js?v=1"></script>
</body>
</html>