feat(selection): M6.0 选股契约与评分引擎(SelectionQuery/Result + select(as_of))
- domain/entities/selection.py:SelectionQuery(universe+method+factors+top_n/top_pct/ min_score+as_of+预热)与 SelectionResult/Candidate/Statistics(v2 §14.2/§21.1 DTO); ConditionSpec 字段就位供 M6.2 条件选股 - quant/selection.py:Selection Engine method=score —— 复合分(zscore×权重×方向) → TopN/Top% 截断;observation_date=<=as_of 最近交易日(防未来函数,v2 §9); 候选带 factor_values 与 selection_reason(可解释) - application/services/selection_service.py:选股用例(universe 过滤 → 装配 → 引擎) - quant/service.py:抽取公共 load_daily_df 供研究/选股共用(行为不变) - tests/test_selection.py:11 例 —— TopN/排序/理由、as_of 防未来函数、ST/上市天数/ 退市过滤、top_pct/min_score、空数据与查询校验;全量 pytest 通过
This commit is contained in:
@@ -0,0 +1,50 @@
|
||||
"""选股用例入口(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)
|
||||
Reference in New Issue
Block a user