"""Convert raw CSV/Parquet into qlib-compatible per-symbol CSV, then dump to .bin format.""" from __future__ import annotations import argparse from pathlib import Path import pandas as pd from config.settings import load_settings REQUIRED_QLIB_FIELDS = ["open", "close", "high", "low", "volume", "factor"] def normalize_columns(df: pd.DataFrame, aliases: dict[str, str]) -> pd.DataFrame: rename = {} for col in df.columns: key = str(col).strip() if key in aliases: rename[col] = aliases[key] out = df.rename(columns=rename) return out.loc[:, ~out.columns.duplicated()] def parquet_to_symbol_csv( parquet_path: Path, output_dir: Path, aliases: dict[str, str], chunk_size: int = 500_000, ) -> int: """Split a wide parquet into per-symbol CSV files for dump_bin.""" import pyarrow.parquet as pq output_dir.mkdir(parents=True, exist_ok=True) pf = pq.ParquetFile(parquet_path) writers: dict[str, list[pd.DataFrame]] = {} for batch in pf.iter_batches(batch_size=chunk_size): chunk = batch.to_pandas() chunk = normalize_columns(chunk, aliases) if "date" not in chunk.columns or "symbol" not in chunk.columns: raise KeyError(f"Missing date/symbol after alias map. columns={chunk.columns.tolist()}") chunk["date"] = pd.to_datetime(chunk["date"]) chunk["symbol"] = chunk["symbol"].astype(str).str.lower() if "factor" not in chunk.columns: chunk["factor"] = 1.0 for sym, grp in chunk.groupby("symbol"): writers.setdefault(sym, []).append(grp) count = 0 for sym, parts in writers.items(): df = pd.concat(parts, ignore_index=True) df = df.sort_values("date").drop_duplicates("date") out_path = output_dir / f"{sym}.csv" keep = ["date", "symbol"] + [c for c in REQUIRED_QLIB_FIELDS if c in df.columns] df[keep].to_csv(out_path, index=False) count += 1 return count def dump_csv_dir_to_qlib( csv_dir: Path, qlib_dir: Path, include_fields: str, freq: str = "day", max_workers: int = 8, mode: str = "dump_all", ): from scripts.dump_bin import DumpDataAll, DumpDataUpdate kwargs = dict( data_path=str(csv_dir), qlib_dir=str(qlib_dir), include_fields=include_fields, file_suffix=".csv", date_field_name="date", symbol_field_name="symbol", freq=freq, max_workers=max_workers, ) if mode == "dump_update": DumpDataUpdate(**kwargs)() else: DumpDataAll(**kwargs)() def run_from_parquet(parquet_path: Path | None = None, mode: str = "dump_all") -> Path: settings = load_settings() dump_cfg = settings.dump_config csv_dir = settings.path(dump_cfg.get("raw_data_dir", "data/raw/csv_by_symbol")) qlib_dir = settings.path(dump_cfg.get("qlib_dir", "data/qlib_data/cn_data")) aliases = dump_cfg.get("column_aliases", {}) if parquet_path is not None: n = parquet_to_symbol_csv(Path(parquet_path), csv_dir, aliases) print(f"Converted {n} symbols to {csv_dir}") dump_csv_dir_to_qlib( csv_dir=csv_dir, qlib_dir=qlib_dir, include_fields=dump_cfg.get("include_fields", "open,close,high,low,volume,factor"), freq=dump_cfg.get("freq", "day"), max_workers=int(dump_cfg.get("max_workers", 8)), mode=mode, ) print(f"Qlib bin data ready at: {qlib_dir}") return qlib_dir if __name__ == "__main__": import sys root = Path(__file__).resolve().parents[1] if str(root) not in sys.path: sys.path.insert(0, str(root)) parser = argparse.ArgumentParser(description="Convert parquet/csv to qlib bin format") parser.add_argument("--parquet", type=str, default=None, help="Source parquet path") parser.add_argument("--mode", choices=["dump_all", "dump_update"], default="dump_all") args = parser.parse_args() run_from_parquet(args.parquet, mode=args.mode)