"""选股用例入口(ARCHITECTURE_v2 §14 Selection Engine · 业务层)。 - 输入:SelectionQuery(universe + method + factors/conditions + top_n/pct + as_of) - 装配:股票池(universe 过滤)→ 行情长表(含预热窗口)→ Selection Engine - 输出:SelectionResult(可解释:factor_values / selection_reason) - 未来函数红线:全部数据只取到 <= as_of(v2 §9);财务条件(后续)只取已公告值 MVP 为同步执行(单日全市场因子计算量轻);如需异步可复用 Job 链路(M6.3 决策)。 """ from __future__ import annotations from datetime import date, timedelta import pandas as pd from app.domain.entities.selection import SelectionQuery, SelectionResult from app.domain.repositories.market import DailyBarRepository, StockRepository from app.quant.selection import factor_columns, run_score_selection from app.quant.service import filter_stocks, load_daily_df class SelectionService: """选股用例入口:select(query) → SelectionResult(当前或历史 as_of)。""" def __init__( self, stock_repo: StockRepository, daily_repo: DailyBarRepository, ) -> None: self._stock_repo = stock_repo self._daily_repo = daily_repo def select(self, query: SelectionQuery) -> SelectionResult: as_of = query.as_of or date.today() stocks = filter_stocks(self._stock_repo.list(), query.universe, as_of=as_of) if not stocks: return run_score_selection(pd.DataFrame(), query, as_of) symbols = [s.symbol for s in stocks] columns = sorted(factor_columns(query)) if query.method == "score" else ["close"] daily = load_daily_df( self._daily_repo, symbols, as_of - timedelta(days=query.warmup_days), as_of, columns, ) if daily.empty: return run_score_selection(daily, query, as_of) return run_score_selection(daily, query, as_of)