| """LLM-driven strategy selection and factor weighting (QuantaAlpha API integration).""" |
|
|
| 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 integrations.quantaalpha.client import QuantaAlphaLLMClient, load_llm_config |
| from integrations.quantaalpha.factor_library import list_factors |
|
|
|
|
| STRATEGY_CATALOG = [ |
| "topk_dropout", |
| "long_short_quantile", |
| "score_weighted_topk", |
| "rank_weighted", |
| "soft_topk", |
| "enhanced_indexing", |
| "dynamic_risk_topk", |
| "factor_equal_topk", |
| "factor_ic_weighted_topk", |
| ] |
|
|
|
|
| def _build_factor_context(catalog: pd.DataFrame, max_factors: int = 30) -> str: |
| rows = catalog.head(max_factors).to_dict(orient="records") |
| return json.dumps(rows, ensure_ascii=False, indent=2) |
|
|
|
|
| def propose_strategy_with_llm( |
| factor_catalog: pd.DataFrame, |
| market_context: str | None = None, |
| client: QuantaAlphaLLMClient | None = None, |
| ) -> dict[str, Any]: |
| """ |
| Ask LLM to propose strategy type, parameters, and factor selection. |
| Returns a dict compatible with strategies/registry.yaml entries. |
| """ |
| client = client or QuantaAlphaLLMClient(load_llm_config()) |
| factor_json = _build_factor_context(factor_catalog) |
|
|
| system = ( |
| "You are a quantitative portfolio strategist. " |
| "Given factor metadata, choose the best strategy from the catalog and parameters. " |
| f"Available strategies: {', '.join(STRATEGY_CATALOG)}. " |
| "Respond in JSON with keys: strategy_name, strategy_kwargs, selected_factor_ids, " |
| "signal_combine, rationale." |
| ) |
| user = ( |
| f"Market context: {market_context or 'CSI300 daily alpha strategy, out-of-sample backtest'}\n\n" |
| f"Factor catalog:\n{factor_json}\n\n" |
| "Pick 3-10 factors if using multi-factor combine strategies." |
| ) |
|
|
| result = client.chat_json( |
| [ |
| {"role": "system", "content": system}, |
| {"role": "user", "content": user}, |
| ] |
| ) |
| return result |
|
|
|
|
| def propose_from_quantaalpha_library( |
| library_path: str | Path, |
| market_context: str | None = None, |
| quality_filter: str | None = "high", |
| ) -> dict[str, Any]: |
| catalog = list_factors(library_path, quality_filter=quality_filter) |
| if catalog.empty: |
| catalog = list_factors(library_path) |
| return propose_strategy_with_llm(catalog, market_context=market_context) |
|
|
|
|
| def build_llm_strategy_plan( |
| library_path: str | Path | None = None, |
| factor_panel_path: str | Path | None = None, |
| market_context: str | None = None, |
| ) -> dict[str, Any]: |
| """High-level entry: LLM plan -> signal source + strategy config.""" |
| if library_path: |
| llm_plan = propose_from_quantaalpha_library(library_path, market_context=market_context) |
| signal_source = { |
| "type": "quantaalpha_library", |
| "path": str(library_path), |
| "factor_ids": llm_plan.get("selected_factor_ids"), |
| "combine": llm_plan.get("signal_combine", "ic_weighted"), |
| } |
| elif factor_panel_path: |
| from data_pipeline.factor_loader import load_factor_panel |
|
|
| panel = load_factor_panel(factor_panel_path) |
| factor_cols = [c for c in panel.columns if c.startswith("factor_")] |
| pseudo = pd.DataFrame({"factor_id": factor_cols, "factor_name": factor_cols, "icir": 1.0}) |
| llm_plan = propose_strategy_with_llm(pseudo, market_context=market_context) |
| signal_source = { |
| "type": "factor_panel", |
| "path": str(factor_panel_path), |
| "factor_cols": llm_plan.get("selected_factor_ids") or factor_cols, |
| "combine": llm_plan.get("signal_combine", "equal"), |
| } |
| else: |
| raise ValueError("Provide library_path or factor_panel_path") |
|
|
| strategy_name = llm_plan.get("strategy_name", "topk_dropout") |
| if strategy_name not in STRATEGY_CATALOG: |
| strategy_name = "topk_dropout" |
|
|
| return { |
| "llm_plan": llm_plan, |
| "signal_source": signal_source, |
| "strategy": { |
| "name": strategy_name, |
| "kwargs": llm_plan.get("strategy_kwargs", {}), |
| }, |
| } |
|
|
|
|
| def save_strategy_plan(plan: dict[str, Any], output_path: str | Path) -> Path: |
| path = Path(output_path) |
| if not path.is_absolute(): |
| path = PROJECT_ROOT / path |
| path.parent.mkdir(parents=True, exist_ok=True) |
| with open(path, "w", encoding="utf-8") as f: |
| json.dump(plan, f, ensure_ascii=False, indent=2) |
| return path |
|
|