feat(selection): M6.2 条件选股(method=condition + 财务可见性防护)

- quant/selection.run_condition_selection:结构化条件 AND 求值 —— 字段域
  static.*(行业/市场…)、技术列与派生量(close/volume/ma20/ma60)、已注册因子
  (momentum_60 等)、fundamental.*(announce_date<=as_of 的最新已公告财务值);
  条件支持 value 字面量与 ref 字段比较(如 close > ma60);结果带 filter_status/reason
- SelectionQuery 校验调整:condition 模式为纯过滤(不再强制 top_n/top_pct)
- FinancialRepository 新增 list_announced_many(批量防未来函数读取)+ SQLAlchemy 实现;
  SelectionService 注入 financial_repo 并按 announce_date 取每股最新一版
- tests/test_selection_condition.py:8 例(行业 in/ne、动量>0、close>ma60 ref、阈值、
  ROE 过滤且未来公告不可见、缺财务 repo 报错、更早 as_of 排除);全量 pytest 通过
This commit is contained in:
Simon
2026-09-09 00:16:45 +08:00
parent 25a1d9531a
commit 75c5472c31
6 changed files with 530 additions and 13 deletions
+5 -4
View File
@@ -43,12 +43,13 @@ class SelectionQuery(BaseModel):
@model_validator(mode="after")
def _check_method_args(self) -> SelectionQuery:
if self.method == "score" and not self.factors:
raise ValueError("method=score 需要至少一个 factors")
if self.method == "score":
if not self.factors:
raise ValueError("method=score 需要至少一个 factors")
if self.top_n is None and self.top_pct is None:
raise ValueError("method=score 需要 top_n 与 top_pct 至少提供一个")
if self.method == "condition" and not self.conditions:
raise ValueError("method=condition 需要至少一个 conditions")
if self.top_n is None and self.top_pct is None:
raise ValueError("top_n 与 top_pct 至少提供一个")
return self
@model_validator(mode="after")
+11
View File
@@ -86,6 +86,17 @@ class FinancialRepository(Protocol):
) -> list[FinancialIndicator]:
"""只返回 announce_date <= as_of_date 的记录 —— 未来函数红线。"""
def list_announced_many(
self,
symbols: Sequence[str],
as_of_date: date,
) -> list[FinancialIndicator]:
"""批量版:返回这些股票 announce_date <= as_of_date 的全部记录。
供选股/截面研究一次性取财务字段(调用方按需取每 symbol 最新一版)。
实现可选 —— 未提供时 SelectionService 回退逐只 list_announced。
"""
class SyncLogRepository(Protocol):
def add(self, log: SyncLog) -> SyncLog: ...