diff --git a/backtesting/engine.py b/backtesting/engine.py index cb10ba1..427f9a6 100644 --- a/backtesting/engine.py +++ b/backtesting/engine.py @@ -11,6 +11,7 @@ from datetime import datetime, timedelta from scoring.policy import SCORE_BRACKETS, SCORE_VERSION, score_in_bracket from ml.artifacts import validate_ml_artifact +from backtesting.statistics import summarize_returns log = logging.getLogger(__name__) @@ -419,6 +420,19 @@ def clear_backtest_cache(): _BACKTEST_CACHE.clear() +def _add_return_statistics(stats, period, returns): + """Add return summaries and a moving-block-bootstrap mean interval.""" + summary = summarize_returns(returns, block_size=min(30, len(returns)), n_resamples=400) + stats[f"avg_{period}"] = summary["mean"] + stats[f"median_{period}"] = summary["median"] + stats[f"win_rate_{period}"] = summary["win_rate"] + stats[f"avg_{period}_ci_low"] = summary["mean_ci_low"] + stats[f"avg_{period}_ci_high"] = summary["mean_ci_high"] + stats[f"max_gain_{period}"] = round(max(returns), 2) + stats[f"max_loss_{period}"] = round(min(returns), 2) + stats[f"n_{period}"] = summary["n"] + + def run_backtest(ml_mode=False): """Return an isolated cached result keyed by all material input files.""" signature = ( @@ -537,13 +551,7 @@ def _compute_backtest(ml_mode=False): for period in ["30d", "90d", "180d", "365d"]: returns = [d["forward_returns"][period] for d in days_in if period in d["forward_returns"]] if returns: - returns_sorted = sorted(returns) - stats[f"avg_{period}"] = round(sum(returns) / len(returns), 2) - stats[f"median_{period}"] = round(returns_sorted[len(returns_sorted) // 2], 2) - stats[f"win_rate_{period}"] = round(len([r for r in returns if r > 0]) / len(returns) * 100, 1) - stats[f"max_gain_{period}"] = round(max(returns), 2) - stats[f"max_loss_{period}"] = round(min(returns), 2) - stats[f"n_{period}"] = len(returns) + _add_return_statistics(stats, period, returns) # Average max drawdown within 90 days dd_list = [] diff --git a/tests/test_backtest_statistics.py b/tests/test_backtest_statistics.py index 7840342..22d15c2 100644 --- a/tests/test_backtest_statistics.py +++ b/tests/test_backtest_statistics.py @@ -1,4 +1,4 @@ -from backtesting import statistics +from backtesting import engine, statistics def test_moving_block_bootstrap_is_deterministic_and_handles_constant_series(): @@ -32,3 +32,14 @@ def test_summarize_returns_reports_observations_and_block_bootstrap_interval(): assert summary["median"] == 2.5 assert summary["win_rate"] == 50.0 assert summary["mean_ci_low"] <= summary["mean"] <= summary["mean_ci_high"] + + +def test_backtest_brackets_publish_bootstrap_confidence_intervals(): + stats = {} + + engine._add_return_statistics(stats, "90d", [10.0, -5.0, 20.0, -10.0]) + + assert stats["avg_90d"] == 3.75 + assert stats["median_90d"] == 2.5 + assert stats["win_rate_90d"] == 50.0 + assert stats["avg_90d_ci_low"] <= stats["avg_90d"] <= stats["avg_90d_ci_high"]