fix(HEL-494): 修复个股缺失指标、问天遮罩、四爻外显并回补250日K

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
总工
2026-09-08 16:26:13 +08:00
co-authored by Cursor multica-agent
parent c8a9376adb
commit 3e828b346c
24 changed files with 1011 additions and 160 deletions
+98 -10
View File
@@ -491,24 +491,96 @@ class Pipeline:
)
return sorted(str(row["cal_date"]) for row in rows)
def backfill_daily_history(
self,
end_date: str | None = None,
trading_days: int | None = None,
force: bool = False,
) -> dict[str, Any]:
"""Incrementally publish official daily bars for the website K-line window.
Same-day EOD still uses the atomic A-group. History backfill publishes
daily (with adj_factor) first so K-line coverage cannot be blocked by
the looser moneyflow universe, then valuation and moneyflow/auction
best-effort. Complete daily dates are skipped unless ``force``.
"""
end = yyyymmdd(end_date or self.clock())
limit = int(trading_days or self.settings.daily_history_trading_days)
open_dates = self.open_trade_dates(end, limit)
if not open_dates:
return {
"start": None,
"end": end,
"requested_days": 0,
"published": [],
"skipped": [],
"failed": [{"error": "calendar has no open dates on or before end"}],
"ok": False,
}
start = open_dates[0]
published: list[dict[str, Any]] = []
skipped: list[str] = []
failed: list[dict[str, Any]] = []
for day in open_dates:
if not force and self.active_batch("daily", day):
skipped.append(day)
continue
try:
daily = self.run_dataset("daily", day)
datasets = {"daily": daily.get("state")}
try:
valuation = self.run_dataset("valuation", day)
datasets["valuation"] = valuation.get("state")
except Exception as exc:
datasets["valuation"] = f"failed:{exc}"[:180]
for name in ("moneyflow", "auction"):
try:
extra = self.run_dataset(name, day)
datasets[name] = extra.get("state")
except Exception as exc:
datasets[name] = f"failed:{exc}"[:180]
published.append({"trade_date": day, "datasets": datasets})
except Exception as exc:
failed.append({"trade_date": day, "error": str(exc)})
return {
"start": start,
"end": end,
"requested_days": len(open_dates),
"published": published,
"skipped": skipped,
"failed": failed,
"ok": not failed,
}
def backfill_history(
self,
trade_date: str | None = None,
calendar_start: str | None = None,
index_days: int | None = None,
daily_days: int | None = None,
codes: tuple[str, ...] | None = None,
force: bool = False,
) -> dict[str, Any]:
"""Idempotent calendar + website-index history backfill."""
"""Idempotent calendar + stock daily + website-index history backfill."""
day = yyyymmdd(trade_date or self.clock())
calendar = self.ingest_reference(day, start=calendar_start)
daily = self.backfill_daily_history(
end_date=day,
trading_days=daily_days,
force=force,
)
index = self.backfill_index_history(
end_date=day,
trading_days=index_days,
codes=codes,
force=force,
)
return {"calendar": calendar, "index_daily": index, "ok": bool(index.get("ok"))}
return {
"calendar": calendar,
"daily": daily,
"index_daily": index,
"ok": bool(daily.get("ok")) and bool(index.get("ok")),
}
def backfill_index_history(
self,
@@ -803,10 +875,7 @@ class Pipeline:
"published_rows": len(published),
"upstream_rows": 0,
}
listed = self.db.fetchone(
"SELECT COUNT(*) AS n FROM stock_master WHERE list_status = 'L'",
)
listed_n = int((listed or {}).get("n") or 0)
listed_n = self._listed_count(day)
floor = float(self.settings.quality.get("daily_row_ratio") or 0.98)
if listed_n and len(upstream) / listed_n < floor:
return {
@@ -1314,14 +1383,33 @@ class Pipeline:
if isinstance(item, dict) and item.get("state") == "failed"
]
def _listed_count(self, trade_date: str = "") -> int:
"""Count listed names that already existed on ``trade_date``.
Historical daily bars must not be judged against later IPOs, or a
correct past session fails the 0.98 row-ratio gate.
"""
day = yyyymmdd(trade_date) if trade_date else ""
if day:
listed = self.db.fetchone(
"""
SELECT COUNT(*) AS n FROM stock_master
WHERE list_status = 'L'
AND (list_date IS NULL OR TRIM(list_date) = '' OR list_date <= ?)
""",
(day,),
)
else:
listed = self.db.fetchone(
"SELECT COUNT(*) AS n FROM stock_master WHERE list_status = 'L'"
)
return int((listed or {}).get("n") or 0)
def validate(self, dataset: str, batch_id: str, trade_date: str, rows: list[dict[str, Any]]) -> dict[str, Any]:
quality = self.settings.quality
errors: list[str] = []
warnings: list[str] = []
listed = self.db.fetchone(
"SELECT COUNT(*) AS n FROM stock_master WHERE list_status = 'L'",
)
listed_n = int((listed or {}).get("n") or 0)
listed_n = self._listed_count(trade_date)
row_n = len(rows)
if dataset == "limit_events":
keys = [(row.get("ts_code"), row.get("trade_date"), row.get("limit_type")) for row in rows]