from __future__ import annotations from typing import Any from backend.features.screener.engine import execute_formula MINIMUM_STABLE_SAMPLES = 20 FORWARD_TRADING_DAYS = 3 def rolling_backtest( snapshots: list[dict[str, Any]], formula: dict[str, Any] ) -> dict[str, Any]: returns: list[float] = [] evaluated_dates: list[str] = [] for index in range(max(0, len(snapshots) - FORWARD_TRADING_DAYS)): current = snapshots[index] future = snapshots[index + FORWARD_TRADING_DAYS] outcome = execute_formula(current["rows"], formula, current["coverage"]) if outcome["status"] not in {"completed", "no_signal"}: continue evaluated_dates.append(str(current["trade_date"])) future_closes = { str(row.get("identifier")): _positive(row.get("close")) for row in future["rows"] } for candidate in outcome["items"]: entry = _positive(candidate.get("close")) future_close = future_closes.get(str(candidate.get("identifier"))) if entry is None or future_close is None: continue returns.append((future_close / entry - 1) * 100) sample_size = len(returns) stable = sample_size >= MINIMUM_STABLE_SAMPLES period_start = evaluated_dates[0] if evaluated_dates else None period_end = evaluated_dates[-1] if evaluated_dates else None return { "sample_size": sample_size, "evaluated_dates": len(evaluated_dates), "stable": stable, "win_rate": ( round(sum(value > 0 for value in returns) / sample_size * 100, 1) if stable else None ), "average_return_3d": ( round(sum(returns) / sample_size, 2) if stable else None ), "period_start": period_start, "period_end": period_end, "message": ( "历史估计已达到最低样本要求" if stable else ( f"小样本:当前{sample_size}个有效样本," f"满{MINIMUM_STABLE_SAMPLES}个后显示胜率与平均收益" ) ), } def attach_historical_estimate( run: dict[str, Any], backtest: dict[str, Any] | None ) -> dict[str, Any]: run["backtest"] = backtest if not backtest or not backtest.get("stable"): return run win_rate = float(backtest["win_rate"]) for item in run.get("items") or []: score = item.get("score_display") item["historical_estimate"] = ( round(win_rate * 0.65 + float(score) * 0.35, 1) if isinstance(score, (int, float)) else None ) return run def _positive(value: Any) -> float | None: try: number = float(value) except (TypeError, ValueError): return None return number if number > 0 else None