#!/usr/bin/env python """一次性清理脚本:移除已入库的停牌脏数据。 背景:历史停牌过滤条件为 "OHLC 全零",会漏过 close 被数据源填充为 前收盘价的停牌记录(如 *ST 撤销风险警示的停牌日),导致日 K 图出现 开盘价为 0 的异常蜡烛。停牌过滤已改用 "open==0 且 high==0",本脚本 负责清理既有脏数据,可重复执行(幂等)。 用法(从 backend/ 目录运行): .venv/bin/python -m scripts.cleanup_halt_days # 清理 + dry-run 关闭 .venv/bin/python -m scripts.cleanup_halt_days --dry-run # 仅扫描,不写盘 """ from __future__ import annotations import argparse import logging from pathlib import Path import polars as pl logger = logging.getLogger(__name__) # kline_daily 原始表与 enriched 表的脏数据都在这些分区里 HALT_TABLES = ["kline_daily", "kline_daily_enriched"] DATA_DIR = Path(__file__).resolve().parent.parent.parent / "data" HALT_PRED = (pl.col("open") == 0) & (pl.col("high") == 0) def _scan_dirty(table: str) -> pl.DataFrame: glob = str(DATA_DIR / table / "**" / "*.parquet") cast = pl.ScanCastOptions(integer_cast="allow-float") return ( pl.scan_parquet(glob, hive_partitioning=True, cast_options=cast) .filter(HALT_PRED) .select("symbol", "date") .collect() ) def _clean_table(table: str, dry_run: bool) -> int: """清理单张表所有脏分区,返回被删除的行数。""" dirty = _scan_dirty(table) if dirty.is_empty(): logger.info("[%s] 无脏数据", table) return 0 removed = 0 base = DATA_DIR / table for dt in dirty["date"].unique().sort(): part = base / f"date={dt}" / "part.parquet" if not part.exists(): logger.warning("[%s] 分区文件不存在: %s", table, part) continue df = pl.read_parquet(part) before = df.height cleaned = df.filter(~HALT_PRED) after = cleaned.height diff = before - after if diff == 0: continue removed += diff if dry_run: logger.info("[%s %s] 将删除 %d 行 (停牌), 剩余 %d 行 [dry-run]", table, dt, diff, after) continue if after == 0: part.unlink() logger.info("[%s %s] 删除 %d 行后分区为空, 已移除文件", table, dt, diff) else: cleaned.write_parquet(part) logger.info("[%s %s] 删除 %d 行 (停牌), 剩余 %d 行已重写", table, dt, diff, after) return removed def main() -> None: ap = argparse.ArgumentParser(description="清理已入库的停牌脏数据") ap.add_argument("--dry-run", action="store_true", help="仅扫描不写盘") args = ap.parse_args() logging.basicConfig( level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s", datefmt="%H:%M:%S", ) total = 0 for table in HALT_TABLES: total += _clean_table(table, args.dry_run) mode = "dry-run 扫描" if args.dry_run else "已清理" logger.info("完成: 共 %s %d 行停牌脏数据", mode, total) if __name__ == "__main__": main()