"""QuantaAlpha integration: factor library and signal building.""" from __future__ import annotations import json from pathlib import Path from typing import Any import pandas as pd from config.settings import PROJECT_ROOT from data_pipeline.factor_loader import load_qlib_expression_factor def load_library(path: str | Path) -> dict[str, Any]: path = Path(path) if not path.is_absolute(): path = PROJECT_ROOT / path with open(path, encoding="utf-8") as f: return json.load(f) def list_factors( library_path: str | Path, quality_filter: str | None = None, min_icir: float | None = None, ) -> pd.DataFrame: data = load_library(library_path) rows = [] for fid, info in data.get("factors", {}).items(): bt = info.get("backtest_results", {}) or {} icir = bt.get("ICIR", bt.get("icir", bt.get("Rank ICIR"))) rows.append( { "factor_id": fid, "factor_name": info.get("factor_name", fid), "factor_expression": info.get("factor_expression", ""), "factor_description": info.get("factor_description", ""), "icir": icir, "ic": bt.get("IC", bt.get("ic")), "quality": info.get("quality", info.get("metadata", {}).get("quality")), } ) df = pd.DataFrame(rows) if min_icir is not None and "icir" in df.columns: df = df[df["icir"].fillna(-999) >= min_icir] if quality_filter and "quality" in df.columns: df = df[df["quality"].astype(str).str.lower() == quality_filter.lower()] return df.sort_values("icir", ascending=False, na_position="last") def build_signal_from_library( library_path: str | Path, factor_ids: list[str] | None = None, top_k: int | None = None, combine: str = "ic_weighted", quality_filter: str | None = None, min_icir: float | None = None, start_time: str | None = None, end_time: str | None = None, ) -> pd.Series: """Build combined signal from QuantaAlpha factor library JSON via qlib expressions.""" catalog = list_factors(library_path, quality_filter=quality_filter, min_icir=min_icir) if factor_ids: catalog = catalog[catalog["factor_id"].isin(factor_ids) | catalog["factor_name"].isin(factor_ids)] if top_k: catalog = catalog.head(top_k) if catalog.empty: raise ValueError("No factors selected from QuantaAlpha library") series_list = [] weights = [] for _, row in catalog.iterrows(): expr = row["factor_expression"] if not expr: continue name = row["factor_name"] or row["factor_id"] s = load_qlib_expression_factor( expression=expr, start_time=start_time, end_time=end_time, name=name, ) series_list.append(s.rename(name)) w = abs(float(row["icir"])) if pd.notna(row["icir"]) else 1.0 weights.append(w) if not series_list: raise ValueError("Selected QuantaAlpha factors have no qlib expressions") mat = pd.concat(series_list, axis=1).sort_index() if combine == "equal": combined = mat.groupby(level="datetime").transform(lambda x: (x - x.mean()) / (x.std() + 1e-8)).mean(axis=1) elif combine == "rank_mean": combined = mat.groupby(level="datetime").rank(pct=True).mean(axis=1) else: w = pd.Series(weights, index=mat.columns) w = w / w.sum() normed = mat.groupby(level="datetime").transform(lambda x: (x - x.mean()) / (x.std() + 1e-8)) combined = normed.mul(w, axis=1).sum(axis=1) combined.name = "score" combined.index.names = ["instrument", "datetime"] return combined