Touched up notebooks + webapp
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
"""Test configuration for running directly from a source checkout."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
SRC_DIR = Path(__file__).resolve().parents[1] / "src"
|
||||
if str(SRC_DIR) not in sys.path:
|
||||
sys.path.insert(0, str(SRC_DIR))
|
||||
@@ -0,0 +1,146 @@
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from adaptive_barrier.engine import (
|
||||
TRADING_MINUTES_PER_YEAR,
|
||||
adaptive_schedule,
|
||||
barrier_miss_prob,
|
||||
detect_barrier_event,
|
||||
fixed_uniform_indices,
|
||||
geometric_brownian_motion,
|
||||
horizon_vol,
|
||||
max_safe_dt,
|
||||
merton_jump_diffusion,
|
||||
run_monte_carlo_simulation,
|
||||
)
|
||||
|
||||
|
||||
def test_symmetric_bridge_inversion_matches_epsilon():
|
||||
distance = 0.04
|
||||
sigma = 0.01
|
||||
epsilon = 1e-3
|
||||
dt = max_safe_dt(distance, sigma, epsilon)
|
||||
assert barrier_miss_prob(distance, distance, 0.0, sigma, dt) == pytest.approx(epsilon)
|
||||
|
||||
|
||||
def test_vectorized_safe_interval_and_validation():
|
||||
result = max_safe_dt(np.array([0.01, 0.02]), 0.1, 1e-3)
|
||||
assert result[1] == pytest.approx(4.0 * result[0])
|
||||
with pytest.raises(ValueError):
|
||||
max_safe_dt(-0.01, 0.1, 1e-3)
|
||||
|
||||
|
||||
def test_adaptive_schedule_uses_matching_minute_units():
|
||||
times = np.linspace(0.0, 10.0, 101)
|
||||
path = np.zeros_like(times)
|
||||
sigma_per_sqrt_minute = horizon_vol(0.30, 1.0)
|
||||
indices = adaptive_schedule(
|
||||
times,
|
||||
path,
|
||||
np.log(0.9),
|
||||
sigma_per_sqrt_minute,
|
||||
1e-3,
|
||||
dt_cap=2.0,
|
||||
)
|
||||
assert np.array_equal(indices, np.array([0, 20, 40, 60, 80, 100]))
|
||||
|
||||
|
||||
def test_detection_deadline_is_enforced():
|
||||
values = np.array([100.0, 89.0, 88.0, 87.0, 86.0])
|
||||
samples = np.array([0, 4])
|
||||
assert detect_barrier_event(samples, values, 1, 90.0, "down", 2) == (False, None, None)
|
||||
assert detect_barrier_event(samples, values, 1, 90.0, "down", 3) == (True, 3, 4)
|
||||
|
||||
|
||||
def test_fixed_indices_have_exact_requested_count():
|
||||
for n in (2, 10, 101):
|
||||
for k in range(1, n + 1):
|
||||
indices = fixed_uniform_indices(n, k)
|
||||
assert len(indices) == k
|
||||
assert len(np.unique(indices)) == k
|
||||
assert indices[0] == 0
|
||||
if k > 1:
|
||||
assert indices[-1] == n - 1
|
||||
|
||||
|
||||
def test_zero_jump_intensity_matches_gbm_for_same_seed():
|
||||
kwargs = dict(S0=100.0, T=5 / TRADING_MINUTES_PER_YEAR, n_steps=100, n_paths=3, mu=0.05, sigma=0.2)
|
||||
_, gbm = geometric_brownian_motion(**kwargs, rng=np.random.default_rng(9))
|
||||
_, jump = merton_jump_diffusion(
|
||||
**kwargs,
|
||||
jump_intensity=0.0,
|
||||
rng=np.random.default_rng(9),
|
||||
)
|
||||
assert np.array_equal(gbm, jump)
|
||||
|
||||
|
||||
def test_simulation_baseline_has_exact_per_path_budget():
|
||||
result = run_monte_carlo_simulation(
|
||||
n_paths=12,
|
||||
n_steps=200,
|
||||
rng_seed=7,
|
||||
drop_fraction=0.05,
|
||||
rise_fraction=0.08,
|
||||
)
|
||||
assert result["adaptive_total_samples"] == result["fixed_total_samples"]
|
||||
for path in result["paths"]:
|
||||
assert len(path["sample_indices"]) == len(path["fixed_sample_indices"])
|
||||
|
||||
|
||||
def test_simulation_lags_respect_deadline():
|
||||
deadline = 3
|
||||
result = run_monte_carlo_simulation(
|
||||
n_paths=20,
|
||||
n_steps=300,
|
||||
rng_seed=3,
|
||||
drop_fraction=0.04,
|
||||
rise_fraction=0.06,
|
||||
max_detection_lag_steps=deadline,
|
||||
)
|
||||
for path in result["paths"]:
|
||||
for key in (
|
||||
"adaptive_lower_detection_lag",
|
||||
"adaptive_upper_detection_lag",
|
||||
"fixed_lower_detection_lag",
|
||||
"fixed_upper_detection_lag",
|
||||
):
|
||||
lag = path[key]
|
||||
assert lag is None or 0 <= lag <= deadline
|
||||
|
||||
|
||||
def test_simulation_exposes_unambiguous_event_counts():
|
||||
result = run_monte_carlo_simulation(
|
||||
n_paths=8,
|
||||
n_steps=150,
|
||||
rng_seed=11,
|
||||
drop_fraction=0.04,
|
||||
rise_fraction=0.06,
|
||||
)
|
||||
assert result["n_barrier_events"] == result["n_lower_events"] + result["n_upper_events"]
|
||||
assert result["n_breaches"] == result["n_barrier_events"]
|
||||
assert 0 <= result["n_paths_with_any_breach"] <= result["n_paths"]
|
||||
|
||||
|
||||
def test_fixed_cadence_indices_include_window_end_and_respect_grid():
|
||||
from adaptive_barrier.engine import fixed_cadence_indices
|
||||
|
||||
times = np.linspace(0.0, 100.0, 11)
|
||||
indices = fixed_cadence_indices(times, 25.0)
|
||||
assert np.array_equal(indices, np.array([0, 3, 5, 8, 10]))
|
||||
|
||||
|
||||
def test_fixed_cadence_mode_uses_independent_sample_count():
|
||||
result = run_monte_carlo_simulation(
|
||||
n_paths=4,
|
||||
n_steps=200,
|
||||
window_minutes=200.0,
|
||||
dt_cap_minutes=2.0,
|
||||
comparison_mode="fixed_cadence",
|
||||
fixed_cadence_minutes=20.0,
|
||||
rng_seed=5,
|
||||
)
|
||||
assert result["comparison_mode"] == "fixed_cadence"
|
||||
assert result["fixed_cadence_minutes"] == 20.0
|
||||
assert result["adaptive_total_samples"] != result["fixed_total_samples"]
|
||||
fixed_counts = {len(path["fixed_sample_indices"]) for path in result["paths"]}
|
||||
assert fixed_counts == {11}
|
||||
@@ -0,0 +1,82 @@
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from webapp.app import app
|
||||
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
|
||||
def test_health_endpoint():
|
||||
response = client.get("/api/health")
|
||||
assert response.status_code == 200
|
||||
payload = response.json()
|
||||
assert payload["ok"] is True
|
||||
assert payload["version"] == "0.4.7"
|
||||
|
||||
|
||||
def test_simulation_endpoint_preserves_equal_budget():
|
||||
response = client.post(
|
||||
"/api/simulate",
|
||||
json={
|
||||
"rng_seed": 3,
|
||||
"n_paths": 5,
|
||||
"n_steps": 100,
|
||||
"drop_fraction": 0.05,
|
||||
"rise_fraction": 0.08,
|
||||
"max_detection_lag_steps": 3,
|
||||
},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
payload = response.json()
|
||||
assert payload["adaptive_total_samples"] == payload["fixed_total_samples"]
|
||||
assert payload["max_detection_lag_steps"] == 3
|
||||
|
||||
def test_webapp_imports_from_source_checkout_without_installed_package(tmp_path):
|
||||
"""The documented direct uvicorn command must resolve the src-layout package."""
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
repo_root = Path(__file__).resolve().parents[1]
|
||||
env = os.environ.copy()
|
||||
env.pop("PYTHONPATH", None)
|
||||
completed = subprocess.run(
|
||||
[sys.executable, "-c", "import webapp.app; print(webapp.app.app.title)"],
|
||||
cwd=repo_root,
|
||||
env=env,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
assert completed.returncode == 0, completed.stderr
|
||||
assert "Adaptive Barrier Monitor" in completed.stdout
|
||||
|
||||
|
||||
|
||||
def test_simulation_endpoint_supports_fixed_cadence():
|
||||
response = client.post(
|
||||
"/api/simulate",
|
||||
json={
|
||||
"rng_seed": 3,
|
||||
"n_paths": 5,
|
||||
"n_steps": 200,
|
||||
"window_minutes": 200.0,
|
||||
"dt_cap_minutes": 2.0,
|
||||
"comparison_mode": "fixed_cadence",
|
||||
"fixed_cadence_minutes": 20.0,
|
||||
},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
payload = response.json()
|
||||
assert payload["comparison_mode"] == "fixed_cadence"
|
||||
assert payload["fixed_cadence_minutes"] == 20.0
|
||||
assert payload["adaptive_total_samples"] != payload["fixed_total_samples"]
|
||||
|
||||
|
||||
def test_simulation_endpoint_rejects_unknown_comparison_mode():
|
||||
response = client.post(
|
||||
"/api/simulate",
|
||||
json={"comparison_mode": "unknown"},
|
||||
)
|
||||
assert response.status_code == 422
|
||||
Reference in New Issue
Block a user