| """Build qlib-compatible handler/dataset from GP mining outputs.""" |
|
|
| from __future__ import annotations |
|
|
| from pathlib import Path |
|
|
| import pandas as pd |
|
|
| from config.settings import load_settings |
| from data_pipeline.init_qlib import init_qlib |
| from qlib.contrib.data.handler import check_transform_proc |
| from qlib.data import D |
| from qlib.data.dataset import DatasetH |
| from qlib.data.dataset.handler import DataHandlerLP |
| from qlib.data.dataset.loader import StaticDataLoader |
|
|
|
|
| def _normalize_symbol(code: str) -> str: |
| code = str(code).upper() |
| if code.startswith(("SH", "SZ", "BJ")): |
| return code |
| if code[0] == "6": |
| return f"SH{code}" |
| return f"SZ{code}" |
|
|
|
|
| def load_gp_feature_frame(run_id: str | None = None) -> pd.DataFrame: |
| settings = load_settings() |
| run_id = run_id or settings.raw.get("experiment", {}).get("run_id", "qlib_gp_run_0") |
| gp_dir = settings.gp_output_dir(run_id) |
|
|
| parquet_path = gp_dir / "ML_Features_qlib.parquet" |
| csv_path = gp_dir / "ML_Features_qlib.csv" |
| if parquet_path.exists(): |
| df = pd.read_parquet(parquet_path) |
| elif csv_path.exists(): |
| df = pd.read_csv(csv_path) |
| else: |
| raise FileNotFoundError(f"GP features not found under {gp_dir}") |
|
|
| df["date"] = pd.to_datetime(df["date"]) |
| df["symbol"] = df["symbol"].map(_normalize_symbol) |
| return df |
|
|
|
|
| def build_qlib_label(instruments, start: str, end: str, label_expr: str) -> pd.Series: |
| settings = load_settings() |
| label_df = D.features( |
| instruments, |
| [label_expr], |
| start_time=start, |
| end_time=end, |
| freq=settings.freq, |
| ) |
| label_df.columns = ["LABEL0"] |
| return label_df["LABEL0"] |
|
|
|
|
| def gp_features_to_qlib_df( |
| gp_df: pd.DataFrame, |
| label: pd.Series | None = None, |
| target_col: str = "target_return", |
| ) -> pd.DataFrame: |
| factor_cols = [c for c in gp_df.columns if c.startswith("factor_")] |
| if not factor_cols: |
| raise ValueError("No factor_* columns found in GP feature frame") |
|
|
| panel = gp_df[["date", "symbol", *factor_cols]].copy() |
| panel["date"] = pd.to_datetime(panel["date"]) |
| panel = panel.set_index(["date", "symbol"]).sort_index() |
| panel.index.names = ["datetime", "instrument"] |
|
|
| if label is None: |
| aligned = gp_df.set_index(["date", "symbol"])[target_col] |
| aligned.index.names = ["datetime", "instrument"] |
| panel["LABEL0"] = aligned.reindex(panel.index) |
| else: |
| label = label.copy() |
| label.index.names = ["datetime", "instrument"] |
| panel = panel.join(label.rename("LABEL0"), how="left") |
|
|
| out = pd.concat( |
| { |
| "feature": panel[factor_cols], |
| "label": panel[["LABEL0"]], |
| }, |
| axis=1, |
| ) |
| out.index.names = ["datetime", "instrument"] |
| out = out.sort_index() |
| out = out.sort_index(level=["datetime", "instrument"], sort_remaining=True) |
| return out |
|
|
|
|
| def build_gp_handler(run_id: str | None = None) -> DataHandlerLP: |
| settings = load_settings() |
| init_qlib() |
| market = settings.market |
| inst_config = D.instruments(market) |
|
|
| gp_df = load_gp_feature_frame(run_id) |
| start = settings.raw["data"]["start_time"] |
| end = settings.raw["data"]["end_time"] |
| label_expr = settings.raw["data"].get("label_expr", "Ref($close, -2)/Ref($close, -1) - 1") |
|
|
| inst_list = D.list_instruments(inst_config, start_time=start, end_time=end, as_list=True) |
| gp_df = gp_df[gp_df["symbol"].isin(inst_list)] |
|
|
| try: |
| label = build_qlib_label(inst_list, start, end, label_expr) |
| except Exception: |
| label = None |
|
|
| qlib_df = gp_features_to_qlib_df(gp_df, label=label) |
| fit_start, fit_end = settings.fit_segment |
|
|
| infer_processors = check_transform_proc( |
| [ |
| {"class": "ProcessInf"}, |
| {"class": "ZScoreNorm"}, |
| {"class": "Fillna"}, |
| ], |
| fit_start, |
| fit_end, |
| ) |
| learn_processors = [ |
| {"class": "DropnaLabel"}, |
| {"class": "CSZScoreNorm", "kwargs": {"fields_group": "label"}}, |
| ] |
|
|
| handler = DataHandlerLP( |
| instruments=None, |
| start_time=start, |
| end_time=end, |
| data_loader=StaticDataLoader(qlib_df), |
| infer_processors=infer_processors, |
| learn_processors=learn_processors, |
| process_type=DataHandlerLP.PTYPE_A, |
| ) |
| return handler |
|
|
|
|
| def build_gp_dataset(run_id: str | None = None) -> DatasetH: |
| settings = load_settings() |
| handler = build_gp_handler(run_id) |
| dataset = DatasetH(handler=handler, segments=settings.segments) |
| return dataset |
|
|
|
|
| def export_gp_artifacts(run_id: str | None = None) -> dict[str, Path]: |
| settings = load_settings() |
| out_dir = settings.gp_output_dir(run_id) |
| out_dir.mkdir(parents=True, exist_ok=True) |
|
|
| handler = build_gp_handler(run_id) |
| handler_path = out_dir / "gp_qlib_handler.pkl" |
| handler.to_pickle(str(handler_path), dump_all=True) |
|
|
| dataset = DatasetH(handler=handler, segments=settings.segments) |
| dataset_path = out_dir / "gp_qlib_dataset.pkl" |
| dataset.config(dump_all=True, recursive=True) |
| dataset.to_pickle(str(dataset_path)) |
|
|
| gp_df = load_gp_feature_frame(run_id) |
| qlib_df = gp_features_to_qlib_df(gp_df, label=None) |
| feature_path = out_dir / "gp_qlib_features.parquet" |
| qlib_df.to_parquet(feature_path) |
|
|
| paths = { |
| "handler": handler_path, |
| "dataset": dataset_path, |
| "features": feature_path, |
| } |
| print("Exported GP qlib artifacts:") |
| for k, p in paths.items(): |
| print(f" {k}: {p}") |
| return paths |
|
|