feat: add block-bootstrap backtest intervals

This commit is contained in:
Hermes Agent
2026-07-26 23:05:05 +00:00
parent a54dec357f
commit 1f754ed85d
2 changed files with 119 additions and 0 deletions
+85
View File
@@ -0,0 +1,85 @@
"""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),
}