86 lines
2.8 KiB
Python
86 lines
2.8 KiB
Python
"""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),
|
|
}
|