from __future__ import annotations from datetime import datetime from typing import Any from backend.bootstrap.config import display_compact_date as _display_date from backend.features.screener.backtest import BacktestRunner from backend.features.screener.catalog import REGIMES from backend.features.screener.factors import FactorBuilder from backend.features.screener.formula import FormulaEvaluator from database import ReviewDatabase class SelectionRunner: def __init__( self, database: ReviewDatabase, factor_builder: FactorBuilder, formula_evaluator: FormulaEvaluator, backtest_runner: BacktestRunner, ) -> None: self.database = database self.factor_builder = factor_builder self.formula_evaluator = formula_evaluator self.backtest_runner = backtest_runner def validate_formula(self, formula: dict[str, Any]) -> dict[str, Any]: return self.formula_evaluator.validate_formula(formula) def build_factors( self, trade_date: str, realtime_snapshot: dict[str, Any] | None, history_days: int, ) -> tuple[list[dict[str, Any]], str]: return self.factor_builder.build_factors( trade_date, realtime_snapshot, history_days ) def apply_formula( self, rows: list[dict[str, Any]], formula: dict[str, Any], regime: str ) -> list[dict[str, Any]]: return self.formula_evaluator.apply_formula(rows, formula, regime) def backtest(self, trade_date: str, formula: dict[str, Any]) -> dict[str, Any]: return self.backtest_runner.backtest(trade_date, formula) def screen( self, user_id: int, trade_date: str, formula: dict[str, Any], regime: str, strategy_name: str, run_backtest: bool = True, realtime_snapshot: dict[str, Any] | None = None, mode: str = "smart", prepared_factors: list[dict[str, Any]] | None = None, prepared_date: str = "", ) -> dict[str, Any]: mode = mode if mode in {"smart", "curated", "quant"} else "smart" formula = self.validate_formula(formula) if prepared_factors is None: history_days = int((formula.get("meta") or {}).get("history_days") or 80) factors, actual_date = self.build_factors( trade_date, realtime_snapshot, history_days ) else: factors = prepared_factors actual_date = prepared_date or trade_date candidates = self.apply_formula(factors, formula, regime) backtest = self.backtest(actual_date, formula) if run_backtest else None required_fields = sorted({ str(item.get("field") or "") for item in list(formula.get("filters") or []) + list(formula.get("score") or []) if item.get("field") }) complete_rows = sum( 1 for row in factors if all(row.get(field) is not None for field in required_fields) ) coverage = round(complete_rows / len(factors) * 100, 1) if factors else 0.0 health_status = "normal" if candidates else "no_signal" if backtest and backtest["samples"] >= 20: for candidate in candidates: estimate = backtest["win_rate"] * 0.65 + candidate["score"] * 100 * 0.35 candidate["historical_probability"] = round(min(95, max(5, estimate)), 1) candidate["probability_samples"] = backtest["samples"] else: for candidate in candidates: candidate["historical_probability"] = None candidate["probability_samples"] = backtest["samples"] if backtest else 0 result = { "meta": { "trade_date": _display_date(actual_date), "regime": regime, "regime_label": REGIMES.get(regime, regime), "strategy_name": strategy_name, "mode": mode, "library_version": int( (formula.get("meta") or {}).get("library_version") or 0 ), "universe_count": len(factors), "candidate_count": len(candidates), "updated_at": datetime.now().astimezone().isoformat(timespec="seconds"), "health": { "status": health_status, "required_field_count": len(required_fields), "complete_rows": complete_rows, "universe_rows": len(factors), "coverage": coverage, "signal_count": len(candidates), }, "selection_source": ( "tushare_rt_k+history" if realtime_snapshot else "historical_eod" ), "realtime": bool(realtime_snapshot), "history_cutoff": ( str(realtime_snapshot.get("previous_trade_date") or "") if realtime_snapshot else actual_date ), "factor_freshness": { "realtime": [ "价格", "涨跌幅", "成交量", "成交额", "换手率", "均线位置", "5/10日动量", "板块强度", "开盘竞价", ] if realtime_snapshot else [], "historical": ["历史波动率", "流通市值", "资金流", "竞价因子", "回测"], }, }, "formula": formula, "candidates": candidates, "backtest": backtest, "disclaimer": ( "候选仅由策略条件与当日数据计算;历史统计不代表未来收益。" if mode == "curated" else "概率为历史条件估计,不代表未来收益;退潮或样本不足时允许无候选。" ), } run_id = self.database.save_screener_run( user_id, actual_date, regime, strategy_name, formula, result, mode ) result["meta"]["run_id"] = run_id return result