quant_test / integrations /quantaalpha /llm_strategy.py
lucky-loster's picture
Upload folder using huggingface_hub
590a501 verified
Raw
History Blame Contribute Delete
4.55 kB
"""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