refactor: establish standalone application boundary
This commit is contained in:
@@ -0,0 +1,138 @@
|
||||
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
|
||||
Reference in New Issue
Block a user