feat(HEL-457): 估值字段级质量门、股票主档每日发布和资金流历史回补

- field_gates 按数据集配置关键字段非空率下限/非有限比例/相对上一批次的塌陷保护,
  字段大面积为空的批次拒发并保留上一正式批次,可读失败原因入 batches.error
- 股票主档交易日 20:00/23:10 自动刷新并发布版本化快照(eod_stocks + publications),
  覆盖新上市/简称变化/N前缀摘除;/v1/stocks 携带 batch_id/published_at,无变化跳过
- moneyflow 历史回补(默认 60 交易日,跳过已发布日期);未发布点查返回
  available_from/available_to 与 history_not_backfilled 标记,缺失不再静默
- eod-refresh 新增 --force --dataset 安全重发(仍走全部质量门,上一批次可回滚)
- 保持 HEL-435 盘后重试机制;新增 22 项测试覆盖字段拒发/正常通过/旧批保留/
  主档新增改名/资金流覆盖/重复执行幂等

Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
multica-agent
2026-09-04 21:36:20 +08:00
co-authored by multica-agent
parent c9892050c3
commit bed6450992
14 changed files with 1058 additions and 37 deletions
+8 -2
View File
@@ -49,6 +49,7 @@ class Scheduler:
"eod_a": self._eod_a,
"eod_b": self._eod_b,
"eod_retry": self._eod_retry,
"stocks_refresh": self._stocks_refresh,
"cleanup": self._cleanup,
"backup": self._backup,
"history_backfill": self._history_backfill,
@@ -89,6 +90,8 @@ class Scheduler:
("cleanup", time(0, 30)),
("backup", time(0, 40)),
]
for refresh_at in self.pipeline.settings.stocks_refresh_times:
plan.append(("stocks_refresh", _hhmm(refresh_at)))
open_day = is_open_day(self.db, day)
for job_id, at in plan:
if current < at:
@@ -96,7 +99,7 @@ class Scheduler:
key = (job_id, day, at.strftime("%H%M"))
if key in self._fired:
continue
if job_id in {"eod_a", "eod_b"} and not open_day:
if job_id in {"eod_a", "eod_b", "stocks_refresh"} and not open_day:
self._fired.add(key)
continue
self._fired.add(key)
@@ -107,7 +110,7 @@ class Scheduler:
try:
self.run_job(job_id, day)
except Exception:
if job_id not in {"eod_a", "eod_b"}:
if job_id not in {"eod_a", "eod_b", "stocks_refresh"}:
raise
# Keep the tick alive; evening retries take over.
LOGGER.exception("scheduled job %s failed for %s", job_id, day)
@@ -317,6 +320,9 @@ class Scheduler:
def _eod_retry(self, trade_date: str) -> dict[str, Any]:
return self.pipeline.run_eod_missing(trade_date)
def _stocks_refresh(self, trade_date: str) -> dict[str, Any]:
return self.pipeline.refresh_stocks(trade_date)
def _history_backfill(self, trade_date: str) -> dict[str, Any]:
return self.pipeline.backfill_history(trade_date)