mirror of
https://github.com/NicolasBohn/NexQuant.git
synced 2026-08-01 09:27:43 +00:00
173 lines
7.3 KiB
Python
173 lines
7.3 KiB
Python
import logging
|
|
import json
|
|
import os
|
|
from typing import List, Tuple
|
|
|
|
from rdagent.components.coder.factor_coder.factor import FactorExperiment, FactorTask
|
|
from rdagent.components.proposal import FactorHypothesis2Experiment, FactorHypothesisGen
|
|
from rdagent.core.proposal import Hypothesis, Scenario, Trace
|
|
from rdagent.scenarios.qlib.experiment.factor_experiment import QlibFactorExperiment
|
|
from rdagent.scenarios.qlib.experiment.model_experiment import QlibModelExperiment
|
|
from rdagent.scenarios.qlib.experiment.quant_experiment import QlibQuantScenario
|
|
from rdagent.utils.agent.tpl import T
|
|
|
|
|
|
def _build_compressed_history(trace: Trace, max_history: int) -> str:
|
|
"""Return hypothesis_and_feedback string with only `max_history` entries.
|
|
|
|
Older entries beyond the last 2 are compressed to one bullet line each.
|
|
"""
|
|
if len(trace.hist) == 0:
|
|
return "No previous hypothesis and feedback available since it's the first round."
|
|
|
|
FULL_DETAIL = 2
|
|
old_hist = trace.hist[:-FULL_DETAIL] if len(trace.hist) > FULL_DETAIL else []
|
|
recent_hist = trace.hist[-FULL_DETAIL:] if len(trace.hist) > FULL_DETAIL else trace.hist
|
|
|
|
parts = []
|
|
if old_hist:
|
|
lines = ["## Earlier experiments (summarized):"]
|
|
for exp, fb in old_hist:
|
|
names = []
|
|
for task in exp.sub_tasks:
|
|
if task is not None and hasattr(task, "factor_name"):
|
|
names.append(task.factor_name)
|
|
elif task is not None and hasattr(task, "model_type"):
|
|
names.append(getattr(task, "model_type", "model"))
|
|
ic_str = ""
|
|
try:
|
|
if exp.result is not None and "IC" in exp.result.index:
|
|
ic_str = f" IC={exp.result.loc['IC']:.4f}"
|
|
except Exception:
|
|
logging.debug("Exception caught", exc_info=True)
|
|
decision = "PASS" if fb.decision else "FAIL"
|
|
obs = (fb.observations or "")[:120].replace("\n", " ")
|
|
lines.append(f"- [{decision}]{ic_str} {', '.join(names) or 'unknown'}: {obs}")
|
|
parts.append("\n".join(lines))
|
|
|
|
if recent_hist:
|
|
rt = Trace(trace.scen)
|
|
rt.hist = recent_hist
|
|
parts.append(T("scenarios.qlib.prompts:hypothesis_and_feedback").r(trace=rt))
|
|
|
|
return "\n\n".join(parts)
|
|
|
|
QlibFactorHypothesis = Hypothesis
|
|
|
|
|
|
class QlibFactorHypothesisGen(FactorHypothesisGen):
|
|
def __init__(self, scen: Scenario) -> Tuple[dict, bool]:
|
|
super().__init__(scen)
|
|
|
|
def prepare_context(self, trace: Trace) -> Tuple[dict, bool]:
|
|
max_h = int(os.environ.get("QLIB_QUANT_MAX_FACTOR_HISTORY", "20"))
|
|
limited = Trace(trace.scen)
|
|
limited.hist = trace.hist[-max_h:] if len(trace.hist) > max_h else trace.hist
|
|
hypothesis_and_feedback = _build_compressed_history(limited, max_h)
|
|
last_hypothesis_and_feedback = (
|
|
T("scenarios.qlib.prompts:last_hypothesis_and_feedback").r(
|
|
experiment=trace.hist[-1][0], feedback=trace.hist[-1][1]
|
|
)
|
|
if len(trace.hist) > 0
|
|
else "No previous hypothesis and feedback available since it's the first round."
|
|
)
|
|
|
|
context_dict = {
|
|
"hypothesis_and_feedback": hypothesis_and_feedback,
|
|
"last_hypothesis_and_feedback": last_hypothesis_and_feedback,
|
|
"RAG": (
|
|
"Try the easiest and fastest factors to experiment with from various perspectives first."
|
|
if len(trace.hist) < 15
|
|
else "Now, you need to try factors that can achieve high IC (e.g., machine learning-based factors)."
|
|
),
|
|
"hypothesis_output_format": T("scenarios.qlib.prompts:factor_hypothesis_output_format").r(),
|
|
"hypothesis_specification": T("scenarios.qlib.prompts:factor_hypothesis_specification").r(),
|
|
}
|
|
return context_dict, True
|
|
|
|
def convert_response(self, response: str) -> Hypothesis:
|
|
response_dict = json.loads(response)
|
|
hypothesis = QlibFactorHypothesis(
|
|
hypothesis=response_dict.get("hypothesis"),
|
|
reason=response_dict.get("reason"),
|
|
concise_reason=response_dict.get("concise_reason"),
|
|
concise_observation=response_dict.get("concise_observation"),
|
|
concise_justification=response_dict.get("concise_justification"),
|
|
concise_knowledge=response_dict.get("concise_knowledge"),
|
|
)
|
|
return hypothesis
|
|
|
|
|
|
class QlibFactorHypothesis2Experiment(FactorHypothesis2Experiment):
|
|
def prepare_context(self, hypothesis: Hypothesis, trace: Trace) -> Tuple[dict | bool]:
|
|
if isinstance(trace.scen, QlibQuantScenario):
|
|
scenario = trace.scen.get_scenario_all_desc(action="factor")
|
|
else:
|
|
scenario = trace.scen.get_scenario_all_desc()
|
|
|
|
experiment_output_format = T("scenarios.qlib.prompts:factor_experiment_output_format").r()
|
|
|
|
if len(trace.hist) == 0:
|
|
hypothesis_and_feedback = "No previous hypothesis and feedback available since it's the first round."
|
|
else:
|
|
max_h = int(os.environ.get("QLIB_QUANT_MAX_FACTOR_HISTORY", "20"))
|
|
factor_hist = [
|
|
e for e in trace.hist
|
|
if not hasattr(e[0].hypothesis, "action") or e[0].hypothesis.action == "factor"
|
|
][-max_h:]
|
|
specific_trace = Trace(trace.scen)
|
|
specific_trace.hist = factor_hist
|
|
if specific_trace.hist:
|
|
hypothesis_and_feedback = _build_compressed_history(specific_trace, max_h)
|
|
else:
|
|
hypothesis_and_feedback = "No previous hypothesis and feedback available."
|
|
|
|
return {
|
|
"target_hypothesis": str(hypothesis),
|
|
"scenario": scenario,
|
|
"hypothesis_and_feedback": hypothesis_and_feedback,
|
|
"experiment_output_format": experiment_output_format,
|
|
"target_list": [],
|
|
"RAG": None,
|
|
}, True
|
|
|
|
def convert_response(self, response: str, hypothesis: Hypothesis, trace: Trace) -> FactorExperiment:
|
|
response_dict = json.loads(response)
|
|
tasks = []
|
|
|
|
for factor_name in response_dict:
|
|
description = response_dict[factor_name]["description"]
|
|
formulation = response_dict[factor_name]["formulation"]
|
|
variables = response_dict[factor_name]["variables"]
|
|
tasks.append(
|
|
FactorTask(
|
|
factor_name=factor_name,
|
|
factor_description=description,
|
|
factor_formulation=formulation,
|
|
variables=variables,
|
|
)
|
|
)
|
|
|
|
exp = QlibFactorExperiment(tasks, hypothesis=hypothesis)
|
|
exp.based_experiments = [QlibFactorExperiment(sub_tasks=[])] + [
|
|
t[0] for t in trace.hist if t[1] and isinstance(t[0], FactorExperiment)
|
|
]
|
|
|
|
unique_tasks = []
|
|
for task in tasks:
|
|
duplicate = False
|
|
for based_exp in exp.based_experiments:
|
|
if isinstance(based_exp, QlibModelExperiment):
|
|
continue
|
|
for sub_task in based_exp.sub_tasks:
|
|
if task.factor_name == sub_task.factor_name:
|
|
duplicate = True
|
|
break
|
|
if duplicate:
|
|
break
|
|
if not duplicate:
|
|
unique_tasks.append(task)
|
|
|
|
exp.tasks = unique_tasks
|
|
return exp
|