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
+15 -2
View File
@@ -182,7 +182,15 @@ class RDLoop(LoopBase, metaclass=LoopMeta):
return modified_feedback
def _propose(self):
hypothesis = self.hypothesis_gen.gen(self.trace, self.plan)
from rdagent.core.exception import LLMUnavailableError
try:
hypothesis = self.hypothesis_gen.gen(self.trace, self.plan)
except LLMUnavailableError as e:
# LLM timeout at the proposal stage: skip_loop_error would leave
# hypothesis=None in trace.hist and corrupt all future iterations.
# Reset the whole loop instead so state stays consistent.
raise self.LoopResumeError("LLM unavailable during proposal, resetting loop") from e
# user can change the hypothesis here
hypothesis = self._interact_hypo(hypothesis)
@@ -237,5 +245,10 @@ class RDLoop(LoopBase, metaclass=LoopMeta):
def record(self, prev_out: dict[str, Any]):
feedback = prev_out["feedback"]
exp = prev_out.get("running") or prev_out.get("coding") or prev_out.get("direct_exp_gen", {}).get("exp_gen")
exp = prev_out.get("running") or prev_out.get("coding") or (prev_out.get("direct_exp_gen") or {}).get("exp_gen")
if exp is None or getattr(exp, "hypothesis", None) is None:
# Loop was reset or skipped — nothing valid to record in trace history.
# Storing None here would corrupt quant_proposal.py which reads
# trace.hist[-1][0].hypothesis on the next iteration.
return
self.trace.sync_dag_parent_and_hist((exp, feedback), prev_out[self.LOOP_IDX_KEY])