# ============================================================================= # Project : anomaly-atlas # File : src/anomaly_atlas/stats/bootstrap.py # Purpose : Moving-block bootstrap and percentile confidence intervals # Author : Simon-Pierre Boucher # Contact : contact@spboucher.ai # Data src : hfmarketdata.io (sole data source) # Created : 2026-08-12 # Modified : 2026-08-12 # Platform : macOS / Apple Silicon (arm64) # License : All rights reserved (research code) # ============================================================================= """Moving-block bootstrap for serially dependent data (Künsch 1989). Blocks preserve short-range dependence, so statistics like AC1 or variance ratios get honest sampling distributions. Every call takes an explicit seed. """ from __future__ import annotations from collections.abc import Callable import numpy as np def moving_block_bootstrap( x: np.ndarray, stat: Callable[[np.ndarray], float], block: int, n_boot: int = 500, seed: int = 0, ) -> np.ndarray: """Bootstrap distribution of `stat` using moving blocks of length `block`.""" x = np.asarray(x, dtype=float) n = len(x) if n < 2 * block: return np.array([]) rng = np.random.default_rng(seed) n_blocks = int(np.ceil(n / block)) starts_max = n - block + 1 out = np.empty(n_boot) for i in range(n_boot): starts = rng.integers(0, starts_max, n_blocks) sample = np.concatenate([x[s : s + block] for s in starts])[:n] out[i] = stat(sample) return out def percentile_ci(samples: np.ndarray, alpha: float = 0.05) -> tuple[float, float]: """Two-sided percentile confidence interval.""" s = np.asarray(samples, dtype=float) s = s[np.isfinite(s)] if len(s) == 0: return (float("nan"), float("nan")) return ( float(np.percentile(s, 100 * alpha / 2)), float(np.percentile(s, 100 * (1 - alpha / 2))), )