"""Statistical helpers for honest time-series backtest reporting.""" from __future__ import annotations import math import random import statistics as stdlib_statistics from collections.abc import Iterable def _quantile(sorted_values: list[float], probability: float) -> float: position = (len(sorted_values) - 1) * probability lower = math.floor(position) upper = math.ceil(position) if lower == upper: return sorted_values[lower] fraction = position - lower return sorted_values[lower] * (1 - fraction) + sorted_values[upper] * fraction def moving_block_bootstrap_ci( values: Iterable[float], *, block_size: int = 30, n_resamples: int = 1_000, confidence: float = 0.95, seed: int = 42, ) -> dict[str, float | int]: """Estimate a mean and CI while preserving local serial dependence.""" series = [float(value) for value in values] if not series: raise ValueError("values must not be empty") if block_size < 1 or block_size > len(series): raise ValueError("block_size must be between 1 and the number of values") if n_resamples < 2: raise ValueError("n_resamples must be at least 2") if not 0 < confidence < 1: raise ValueError("confidence must be between 0 and 1") rng = random.Random(seed) sample_means: list[float] = [] final_start = len(series) - block_size for _ in range(n_resamples): sample: list[float] = [] while len(sample) < len(series): start = rng.randint(0, final_start) sample.extend(series[start:start + block_size]) sample = sample[:len(series)] sample_means.append(sum(sample) / len(sample)) sample_means.sort() tail = (1 - confidence) / 2 return { "estimate": sum(series) / len(series), "ci_low": _quantile(sample_means, tail), "ci_high": _quantile(sample_means, 1 - tail), "n": len(series), } def summarize_returns( values: Iterable[float], *, block_size: int = 30, n_resamples: int = 1_000, confidence: float = 0.95, seed: int = 42, ) -> dict[str, float | int]: """Summarize realized returns with an autocorrelation-aware mean CI.""" series = [float(value) for value in values] interval = moving_block_bootstrap_ci( series, block_size=min(block_size, len(series)), n_resamples=n_resamples, confidence=confidence, seed=seed, ) return { "n": len(series), "mean": round(float(interval["estimate"]), 2), "median": round(stdlib_statistics.median(series), 2), "win_rate": round(sum(value > 0 for value in series) / len(series) * 100, 1), "mean_ci_low": round(float(interval["ci_low"]), 2), "mean_ci_high": round(float(interval["ci_high"]), 2), }