feat: unified backtest engine, LLM error handling, strategy refactor

- Add vbt_backtest.py as single source of truth for all metric formulas
  (Sharpe, drawdown, IC, transaction costs) — backtest_engine.py and
  strategy_orchestrator.py now delegate to it
- Add LLMUnavailableError to exception.py; rd_loop.py catches it at the
  proposal stage and raises LoopResumeError to avoid corrupting trace
  history with None hypotheses
- Guard record() against None exp/hypothesis so loop resets leave
  trace.hist in a consistent state
- Refactor strategy_orchestrator and optuna_optimizer to use unified
  backtest path; remove duplicate metric calculation code
- Add predix_rebacktest_unified.py script for offline re-evaluation
- Update tests and README

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
TPTBusiness
2026-04-17 22:52:07 +02:00
parent 2932f65eae
commit 3d2872c2fc
15 changed files with 1286 additions and 171 deletions
+25 -16
View File
@@ -86,20 +86,26 @@ class TestBacktestMetricsCalculateIC:
class TestBacktestMetricsCalculateSharpe:
"""Tests für BacktestMetrics.calculate_sharpe()"""
def test_calculate_sharpe_normal_data(self, backtest_metrics, sample_returns_data):
"""Sharpe Ratio mit normalen Daten sollte korrekt berechnet werden"""
returns, equity = sample_returns_data
sharpe = backtest_metrics.calculate_sharpe(returns)
# Sharpe sollte im typischen Bereich liegen (-5 bis 5)
def test_calculate_sharpe_normal_data(self, sample_returns_data):
"""Sharpe Ratio mit Daily-Daten sollte im typischen Bereich liegen."""
from rdagent.components.backtesting.backtest_engine import BacktestMetrics
returns, _ = sample_returns_data
# sample_returns_data is business-daily → use daily annualization.
bm_daily = BacktestMetrics(risk_free_rate=0.02, bars_per_year=252)
sharpe = bm_daily.calculate_sharpe(returns)
assert -5 <= sharpe <= 5, f"Sharpe {sharpe} liegt außerhalb typischen Bereichs"
def test_calculate_sharpe_annualized_vs_raw(self, backtest_metrics, sample_returns_data):
"""Annualisierte Sharpe sollte sqrt(252) * raw Sharpe sein"""
returns, equity = sample_returns_data
sharpe_raw = backtest_metrics.calculate_sharpe(returns, annualize=False)
sharpe_ann = backtest_metrics.calculate_sharpe(returns, annualize=True)
def test_calculate_sharpe_annualized_vs_raw(self, sample_returns_data):
"""Annualisierte Sharpe = √(bars_per_year) * raw Sharpe — convention-agnostic."""
from rdagent.components.backtesting.backtest_engine import BacktestMetrics
returns, _ = sample_returns_data
bm_daily = BacktestMetrics(risk_free_rate=0.02, bars_per_year=252)
sharpe_raw = bm_daily.calculate_sharpe(returns, annualize=False)
sharpe_ann = bm_daily.calculate_sharpe(returns, annualize=True)
expected_ann = sharpe_raw * np.sqrt(252)
assert abs(sharpe_ann - expected_ann) < 1e-10, \
f"Annualisierte Sharpe {sharpe_ann} != erwartet {expected_ann}"
@@ -127,13 +133,16 @@ class TestBacktestMetricsCalculateSharpe:
# Die Implementierung gibt keinen NaN zurück wenn std != 0
assert np.isfinite(sharpe) or np.isnan(sharpe), "Sharpe sollte finite oder NaN sein"
def test_calculate_sharpe_negative_returns(self, backtest_metrics):
"""Sharpe sollte mit negativen Returns korrekt umgehen"""
def test_calculate_sharpe_negative_returns(self):
"""Sharpe sollte mit negativen Daily-Returns korrekt umgehen"""
from rdagent.components.backtesting.backtest_engine import BacktestMetrics
n = 100
dates = pd.date_range(start='2024-01-01', periods=n, freq='B')
returns = pd.Series(np.random.randn(n) * 0.02 - 0.001, index=dates)
sharpe = backtest_metrics.calculate_sharpe(returns)
bm_daily = BacktestMetrics(risk_free_rate=0.02, bars_per_year=252)
sharpe = bm_daily.calculate_sharpe(returns)
assert -5 <= sharpe <= 5, f"Sharpe {sharpe} liegt außerhalb typischen Bereichs"