From d1019cb56825a51de7108272856a0f33ea061ec2 Mon Sep 17 00:00:00 2001 From: Yuante Li <104308117+WinstonLiyt@users.noreply.github.com> Date: Thu, 29 May 2025 16:16:51 +0800 Subject: [PATCH] feat: add RD-Agent-Quant scenario (#838) * fix model input shape bug and costeer_model bug * fix a bug * fix a bug in docker result extraction * a system-level optimization * add a filter of stdout * update * add stdout to model * model training_hyperparameters update * quant scenario * update some quant settings * llm choose action * Thompson Sampling Bandit for action choosing * refine both scens * add trace messages for quant scen * fix some bugs * fix some bugs * update * update * update * fix * fix * fix * update for merge * fix ci * fix some bugs * fix ci * fix ci * fix ci * fix ci * refactor * default qlib4rdagent local env downloading * fix ci * fix ci * fix a bug * fix ci * fix: align all prompts on template (#908) * use template to render all prompts * fix CI --------- Co-authored-by: Xu Yang * add fin_quant in cli * fix a bug * fix ci * fix some bugs * refactor * remove the columns in hypothesis if no value generated in this column * fix a bug * fix ci * fix conda env * add qlib gitignore * remove existed qlib folder & install torch in qlib conda * fix workspace ui in feedback * align model config in coder and runner in docker or conda * fix CI * fix CI --------- Co-authored-by: Xu Yang Co-authored-by: Xu Yang --- .gitignore | 1 + rdagent/app/cli.py | 2 + rdagent/app/qlib_rd_loop/conf.py | 44 +++ .../app/qlib_rd_loop/factor_from_report.py | 16 +- rdagent/app/qlib_rd_loop/prompts.yaml | 4 - rdagent/app/qlib_rd_loop/quant.py | 137 +++++++ .../coder/CoSTEER/evolving_strategy.py | 4 - .../coder/CoSTEER/knowledge_management.py | 14 +- .../coder/data_science/ensemble/__init__.py | 2 - .../components/coder/factor_coder/config.py | 2 +- .../coder/factor_coder/eva_utils.py | 98 ++--- .../coder/factor_coder/evolving_strategy.py | 61 +--- .../components/coder/factor_coder/factor.py | 6 + .../coder/factor_coder/prompts.yaml | 3 + rdagent/components/coder/model_coder/conf.py | 13 + .../components/coder/model_coder/eva_utils.py | 72 ++-- .../coder/model_coder/evolving_strategy.py | 37 +- rdagent/components/coder/model_coder/model.py | 26 +- .../coder/model_coder/one_shot/__init__.py | 12 +- .../coder/model_coder/task_loader.py | 9 +- rdagent/components/proposal/__init__.py | 87 ++--- rdagent/components/proposal/prompts.yaml | 60 ++-- rdagent/components/workflow/rd_loop.py | 4 +- rdagent/core/conf.py | 2 + rdagent/core/proposal.py | 17 +- rdagent/log/ui/app.py | 335 +++++++++++++++++- .../data_mining/developer/feedback.py | 34 +- .../data_mining/developer/model_runner.py | 7 +- .../experiment/model_experiment.py | 12 +- .../data_mining/proposal/model_proposal.py | 35 +- rdagent/scenarios/general_model/scenario.py | 15 +- rdagent/scenarios/kaggle/developer/coder.py | 18 +- .../scenarios/kaggle/developer/feedback.py | 26 +- rdagent/scenarios/kaggle/developer/runner.py | 5 - .../scenarios/kaggle/experiment/scenario.py | 76 ++-- rdagent/scenarios/kaggle/kaggle_crawler.py | 1 - .../knowledge_management/extract_knowledge.py | 33 +- .../kaggle/knowledge_management/graph.py | 17 +- .../knowledge_management/vector_base.py | 17 +- rdagent/scenarios/kaggle/proposal/proposal.py | 45 +-- .../scenarios/qlib/developer/factor_runner.py | 125 ++++--- rdagent/scenarios/qlib/developer/feedback.py | 123 ++++--- .../scenarios/qlib/developer/model_runner.py | 76 +++- rdagent/scenarios/qlib/developer/utils.py | 67 ++++ rdagent/scenarios/qlib/docker/Dockerfile | 2 +- .../qlib/experiment/factor_experiment.py | 19 +- .../factor_from_report_experiment.py | 14 +- .../qlib/experiment/factor_template/conf.yaml | 22 ++ .../factor_template/conf_combined.yaml | 12 + .../factor_template/conf_model_combined.yaml | 119 +++++++ .../qlib/experiment/model_experiment.py | 17 +- .../qlib/experiment/model_template/conf.yaml | 22 +- .../model_template/conf_model_combined.yaml | 119 +++++++ .../scenarios/qlib/experiment/prompts.yaml | 18 +- .../qlib/experiment/quant_experiment.py | 167 +++++++++ rdagent/scenarios/qlib/experiment/utils.py | 76 ++-- .../scenarios/qlib/experiment/workspace.py | 40 ++- .../factor_experiment_loader/pdf_loader.py | 33 +- rdagent/scenarios/qlib/prompts.yaml | 327 +++++++++-------- rdagent/scenarios/qlib/proposal/bandit.py | 109 ++++++ .../qlib/proposal/factor_proposal.py | 78 ++-- .../scenarios/qlib/proposal/model_proposal.py | 115 ++++-- .../scenarios/qlib/proposal/quant_proposal.py | 179 ++++++++++ rdagent/utils/env.py | 164 +++++++-- 64 files changed, 2427 insertions(+), 1025 deletions(-) create mode 100644 rdagent/app/qlib_rd_loop/quant.py create mode 100644 rdagent/components/coder/model_coder/conf.py create mode 100644 rdagent/scenarios/qlib/developer/utils.py create mode 100644 rdagent/scenarios/qlib/experiment/factor_template/conf_model_combined.yaml create mode 100644 rdagent/scenarios/qlib/experiment/model_template/conf_model_combined.yaml create mode 100644 rdagent/scenarios/qlib/experiment/quant_experiment.py create mode 100644 rdagent/scenarios/qlib/proposal/bandit.py create mode 100644 rdagent/scenarios/qlib/proposal/quant_proposal.py diff --git a/.gitignore b/.gitignore index 32d9bcb6..74500f0b 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ Pipfile public release-notes.md typescript* +qlib # Byte-compiled / optimized / DLL files __pycache__/ diff --git a/rdagent/app/cli.py b/rdagent/app/cli.py index 83bc24aa..ca58dfd1 100644 --- a/rdagent/app/cli.py +++ b/rdagent/app/cli.py @@ -25,6 +25,7 @@ from rdagent.app.general_model.general_model import ( from rdagent.app.qlib_rd_loop.factor import main as fin_factor from rdagent.app.qlib_rd_loop.factor_from_report import main as fin_factor_report from rdagent.app.qlib_rd_loop.model import main as fin_model +from rdagent.app.qlib_rd_loop.quant import main as fin_quant from rdagent.app.utils.health_check import health_check from rdagent.app.utils.info import collect_info @@ -50,6 +51,7 @@ def app(): "fin_factor": fin_factor, "fin_factor_report": fin_factor_report, "fin_model": fin_model, + "fin_quant": fin_quant, "med_model": med_model, "general_model": general_model, "ui": ui, diff --git a/rdagent/app/qlib_rd_loop/conf.py b/rdagent/app/qlib_rd_loop/conf.py index 63a30fcf..42debfa3 100644 --- a/rdagent/app/qlib_rd_loop/conf.py +++ b/rdagent/app/qlib_rd_loop/conf.py @@ -71,6 +71,50 @@ class FactorFromReportPropSetting(FactorBasePropSetting): """Limits report processing count if True; processes all if False""" +class QuantBasePropSetting(BasePropSetting): + model_config = SettingsConfigDict(env_prefix="QLIB_QUANT_", protected_namespaces=()) + + # 1) override base settings + scen: str = "rdagent.scenarios.qlib.experiment.quant_experiment.QlibQuantScenario" + """Scenario class for Qlib Model""" + + quant_hypothesis_gen: str = "rdagent.scenarios.qlib.proposal.quant_proposal.QlibQuantHypothesisGen" + """Hypothesis generation class""" + + model_hypothesis2experiment: str = "rdagent.scenarios.qlib.proposal.model_proposal.QlibModelHypothesis2Experiment" + """Hypothesis to experiment class""" + + model_coder: str = "rdagent.scenarios.qlib.developer.model_coder.QlibModelCoSTEER" + """Coder class""" + + model_runner: str = "rdagent.scenarios.qlib.developer.model_runner.QlibModelRunner" + """Runner class""" + + model_summarizer: str = "rdagent.scenarios.qlib.developer.feedback.QlibModelExperiment2Feedback" + """Summarizer class""" + + factor_hypothesis2experiment: str = ( + "rdagent.scenarios.qlib.proposal.factor_proposal.QlibFactorHypothesis2Experiment" + ) + """Hypothesis to experiment class""" + + factor_coder: str = "rdagent.scenarios.qlib.developer.factor_coder.QlibFactorCoSTEER" + """Coder class""" + + factor_runner: str = "rdagent.scenarios.qlib.developer.factor_runner.QlibFactorRunner" + """Runner class""" + + factor_summarizer: str = "rdagent.scenarios.qlib.developer.feedback.QlibFactorExperiment2Feedback" + """Summarizer class""" + + evolving_n: int = 10 + """Number of evolutions""" + + action_selection: str = "bandit" + """Action selection strategy: 'bandit' for bandit-based selection, 'llm' for LLM-based selection, 'random' for random selection""" + + FACTOR_PROP_SETTING = FactorBasePropSetting() FACTOR_FROM_REPORT_PROP_SETTING = FactorFromReportPropSetting() MODEL_PROP_SETTING = ModelBasePropSetting() +QUANT_PROP_SETTING = QuantBasePropSetting() diff --git a/rdagent/app/qlib_rd_loop/factor_from_report.py b/rdagent/app/qlib_rd_loop/factor_from_report.py index 4917509d..72e017f6 100644 --- a/rdagent/app/qlib_rd_loop/factor_from_report.py +++ b/rdagent/app/qlib_rd_loop/factor_from_report.py @@ -3,7 +3,6 @@ from pathlib import Path from typing import Any, Dict, Tuple import fire -from jinja2 import Environment, StrictUndefined from rdagent.app.qlib_rd_loop.conf import FACTOR_FROM_REPORT_PROP_SETTING from rdagent.app.qlib_rd_loop.factor import FactorRDLoop @@ -11,7 +10,6 @@ from rdagent.components.document_reader.document_reader import ( extract_first_page_screenshot_from_pdf, load_and_process_pdfs_by_langchain, ) -from rdagent.core.prompts import Prompts from rdagent.core.proposal import Hypothesis from rdagent.log import rdagent_logger as logger from rdagent.oai.llm_utils import APIBackend @@ -19,11 +17,9 @@ from rdagent.scenarios.qlib.experiment.factor_experiment import QlibFactorExperi from rdagent.scenarios.qlib.factor_experiment_loader.pdf_loader import ( FactorExperimentLoaderFromPDFfiles, ) +from rdagent.utils.agent.tpl import T from rdagent.utils.workflow import LoopMeta -prompts_path = Path(__file__).parent / "prompts.yaml" -prompts = Prompts(file_path=prompts_path) - def generate_hypothesis(factor_result: dict, report_content: str) -> str: """ @@ -36,13 +32,9 @@ def generate_hypothesis(factor_result: dict, report_content: str) -> str: Returns: str: The generated hypothesis. """ - system_prompt = ( - Environment(undefined=StrictUndefined).from_string(prompts["hypothesis_generation"]["system"]).render() - ) - user_prompt = ( - Environment(undefined=StrictUndefined) - .from_string(prompts["hypothesis_generation"]["user"]) - .render(factor_descriptions=json.dumps(factor_result), report_content=report_content) + system_prompt = T(".prompts:hypothesis_generation.system").r() + user_prompt = T(".prompts:hypothesis_generation.user").r( + factor_descriptions=json.dumps(factor_result), report_content=report_content ) response = APIBackend().build_messages_and_create_chat_completion( diff --git a/rdagent/app/qlib_rd_loop/prompts.yaml b/rdagent/app/qlib_rd_loop/prompts.yaml index a39c3174..129c2c09 100644 --- a/rdagent/app/qlib_rd_loop/prompts.yaml +++ b/rdagent/app/qlib_rd_loop/prompts.yaml @@ -5,10 +5,6 @@ hypothesis_generation: { "hypothesis": "A clear and concise hypothesis based on the provided information.", "reason": "A detailed explanation supporting the generated hypothesis.", - "concise_reason": "One line summary that focuses on the justification for the change that leads to the hypothesis (like a part of a knowledge that we are building)", - "concise_observation": "One line summary. It focuses on the observation of the given scenario, data characteristics, or previous experiences (failures & succeses).", - "concise_justification": "One line summary. It focuses on the justification for the change in new hypothesis and the route of exploration supporting the growth of the hypothesis, based on the observation. ", - "concise_knowledge": "One line summary. It focuses on a transferable knowledege that comes with the new hypothesis. Use conditional grammar. eg. "If...., ..; When..., .; and etc" } user: |- diff --git a/rdagent/app/qlib_rd_loop/quant.py b/rdagent/app/qlib_rd_loop/quant.py new file mode 100644 index 00000000..295446f6 --- /dev/null +++ b/rdagent/app/qlib_rd_loop/quant.py @@ -0,0 +1,137 @@ +""" +Quant (Factor & Model) workflow with session control +""" + +from typing import Any + +import fire + +from rdagent.app.qlib_rd_loop.conf import QUANT_PROP_SETTING +from rdagent.components.workflow.conf import BasePropSetting +from rdagent.components.workflow.rd_loop import RDLoop +from rdagent.core.developer import Developer +from rdagent.core.exception import FactorEmptyError, ModelEmptyError +from rdagent.core.proposal import ( + Experiment2Feedback, + Hypothesis2Experiment, + HypothesisFeedback, + HypothesisGen, +) +from rdagent.core.scenario import Scenario +from rdagent.core.utils import import_class +from rdagent.log import rdagent_logger as logger +from rdagent.scenarios.qlib.proposal.quant_proposal import QuantTrace + + +class QuantRDLoop(RDLoop): + skip_loop_error = ( + FactorEmptyError, + ModelEmptyError, + ) + + def __init__(self, PROP_SETTING: BasePropSetting): + with logger.tag("init"): + scen: Scenario = import_class(PROP_SETTING.scen)() + logger.log_object(scen, tag="scenario") + + self.hypothesis_gen: HypothesisGen = import_class(PROP_SETTING.quant_hypothesis_gen)(scen) + logger.log_object(self.hypothesis_gen, tag="quant hypothesis generator") + + self.factor_hypothesis2experiment: Hypothesis2Experiment = import_class( + PROP_SETTING.factor_hypothesis2experiment + )() + logger.log_object(self.factor_hypothesis2experiment, tag="factor hypothesis2experiment") + self.model_hypothesis2experiment: Hypothesis2Experiment = import_class( + PROP_SETTING.model_hypothesis2experiment + )() + logger.log_object(self.model_hypothesis2experiment, tag="model hypothesis2experiment") + + self.factor_coder: Developer = import_class(PROP_SETTING.factor_coder)(scen) + logger.log_object(self.factor_coder, tag="factor coder") + self.model_coder: Developer = import_class(PROP_SETTING.model_coder)(scen) + logger.log_object(self.model_coder, tag="model coder") + + self.factor_runner: Developer = import_class(PROP_SETTING.factor_runner)(scen) + logger.log_object(self.factor_runner, tag="factor runner") + self.model_runner: Developer = import_class(PROP_SETTING.model_runner)(scen) + logger.log_object(self.model_runner, tag="model runner") + + self.factor_summarizer: Experiment2Feedback = import_class(PROP_SETTING.factor_summarizer)(scen) + logger.log_object(self.factor_summarizer, tag="factor summarizer") + self.model_summarizer: Experiment2Feedback = import_class(PROP_SETTING.model_summarizer)(scen) + logger.log_object(self.model_summarizer, tag="model summarizer") + + self.trace = QuantTrace(scen=scen) + super(RDLoop, self).__init__() + + def direct_exp_gen(self, prev_out: dict[str, Any]): + with logger.tag("r"): # research + hypo = self._propose() + assert hypo.action in ["factor", "model"] + if hypo.action == "factor": + exp = self.factor_hypothesis2experiment.convert(hypo, self.trace) + else: + exp = self.model_hypothesis2experiment.convert(hypo, self.trace) + logger.log_object(exp.sub_tasks, tag="experiment generation") + return {"propose": hypo, "exp_gen": exp} + + def coding(self, prev_out: dict[str, Any]): + with logger.tag("d"): # development + if prev_out["direct_exp_gen"]["propose"].action == "factor": + exp = self.factor_coder.develop(prev_out["direct_exp_gen"]["exp_gen"]) + elif prev_out["direct_exp_gen"]["propose"].action == "model": + exp = self.model_coder.develop(prev_out["direct_exp_gen"]["exp_gen"]) + logger.log_object(exp, tag="coder result") + return exp + + def running(self, prev_out: dict[str, Any]): + with logger.tag("ef"): + if prev_out["direct_exp_gen"]["propose"].action == "factor": + exp = self.factor_runner.develop(prev_out["coding"]) + if exp is None: + logger.error(f"Factor extraction failed.") + raise FactorEmptyError("Factor extraction failed.") + elif prev_out["direct_exp_gen"]["propose"].action == "model": + exp = self.model_runner.develop(prev_out["coding"]) + logger.log_object(exp, tag="runner result") + return exp + + def feedback(self, prev_out: dict[str, Any]): + e = prev_out.get(self.EXCEPTION_KEY, None) + if e is not None: + feedback = HypothesisFeedback( + observations=e, + hypothesis_evaluation="", + new_hypothesis="", + reason="", + decision=False, + ) + with logger.tag("ef"): # evaluate and feedback + logger.log_object(feedback, tag="feedback") + self.trace.hist.append((prev_out["direct_exp_gen"]["exp_gen"], feedback)) + else: + if prev_out["direct_exp_gen"]["propose"].action == "factor": + feedback = self.factor_summarizer.generate_feedback(prev_out["running"], self.trace) + elif prev_out["direct_exp_gen"]["propose"].action == "model": + feedback = self.model_summarizer.generate_feedback(prev_out["running"], self.trace) + with logger.tag("ef"): + logger.log_object(feedback, tag="feedback") + self.trace.hist.append((prev_out["running"], feedback)) + + +def main(path=None, step_n=None): + """ + Auto R&D Evolving loop for fintech factors. + You can continue running session by + .. code-block:: python + dotenv run -- python rdagent/app/qlib_rd_loop/quant.py $LOG_PATH/__session__/1/0_propose --step_n 1 # `step_n` is a optional paramter + """ + if path is None: + quant_loop = QuantRDLoop(QUANT_PROP_SETTING) + else: + quant_loop = QuantRDLoop.load(path) + quant_loop.run(step_n=step_n) + + +if __name__ == "__main__": + fire.Fire(main) diff --git a/rdagent/components/coder/CoSTEER/evolving_strategy.py b/rdagent/components/coder/CoSTEER/evolving_strategy.py index ffedafb8..8d7292fb 100644 --- a/rdagent/components/coder/CoSTEER/evolving_strategy.py +++ b/rdagent/components/coder/CoSTEER/evolving_strategy.py @@ -1,7 +1,6 @@ from __future__ import annotations from abc import abstractmethod -from pathlib import Path from rdagent.components.coder.CoSTEER.config import CoSTEERSettings from rdagent.components.coder.CoSTEER.evaluators import ( @@ -15,12 +14,9 @@ from rdagent.components.coder.CoSTEER.knowledge_management import ( from rdagent.core.conf import RD_AGENT_SETTINGS from rdagent.core.evolving_framework import EvolvingStrategy, EvoStep, QueriedKnowledge from rdagent.core.experiment import FBWorkspace, Task -from rdagent.core.prompts import Prompts from rdagent.core.scenario import Scenario from rdagent.core.utils import multiprocessing_wrapper -implement_prompts = Prompts(file_path=Path(__file__).parent / "prompts.yaml") - class MultiProcessEvolvingStrategy(EvolvingStrategy): def __init__(self, scen: Scenario, settings: CoSTEERSettings): diff --git a/rdagent/components/coder/CoSTEER/knowledge_management.py b/rdagent/components/coder/CoSTEER/knowledge_management.py index eb675e7d..3d627daa 100644 --- a/rdagent/components/coder/CoSTEER/knowledge_management.py +++ b/rdagent/components/coder/CoSTEER/knowledge_management.py @@ -8,8 +8,6 @@ from itertools import combinations from pathlib import Path from typing import List, Union -from jinja2 import Environment, StrictUndefined - from rdagent.components.coder.CoSTEER.config import CoSTEERSettings from rdagent.components.coder.CoSTEER.evaluators import CoSTEERSingleFeedback from rdagent.components.knowledge_management.graph import ( @@ -26,12 +24,12 @@ from rdagent.core.evolving_framework import ( RAGStrategy, ) from rdagent.core.experiment import FBWorkspace, Task -from rdagent.core.prompts import Prompts from rdagent.log import rdagent_logger as logger from rdagent.oai.llm_utils import ( APIBackend, calculate_embedding_distance_between_str_list, ) +from rdagent.utils.agent.tpl import T class CoSTEERKnowledge(Knowledge): @@ -216,8 +214,6 @@ class CoSTEERQueriedKnowledgeV2(CoSTEERQueriedKnowledgeV1): class CoSTEERRAGStrategyV2(RAGStrategy): - prompt = Prompts(file_path=Path(__file__).parent / "prompts.yaml") - def __init__(self, knowledgebase: CoSTEERKnowledgeBaseV2, settings: CoSTEERSettings) -> None: super().__init__(knowledgebase) self.current_generated_trace_count = 0 @@ -324,12 +320,8 @@ class CoSTEERRAGStrategyV2(RAGStrategy): all_component_content = "" for _, component_node in enumerate(all_component_nodes): all_component_content += f"{component_node.content}, \n" - analyze_component_system_prompt = ( - Environment(undefined=StrictUndefined) - .from_string(self.prompt["analyze_component_prompt_v1_system"]) - .render( - all_component_content=all_component_content, - ) + analyze_component_system_prompt = T(".prompts:analyze_component_prompt_v1_system").r( + all_component_content=all_component_content, ) analyze_component_user_prompt = target_task_information diff --git a/rdagent/components/coder/data_science/ensemble/__init__.py b/rdagent/components/coder/data_science/ensemble/__init__.py index e1882fa5..fcb76120 100644 --- a/rdagent/components/coder/data_science/ensemble/__init__.py +++ b/rdagent/components/coder/data_science/ensemble/__init__.py @@ -11,9 +11,7 @@ File structure - Each coder could be tested. """ -import json from pathlib import Path -from typing import Dict from jinja2 import Environment, StrictUndefined diff --git a/rdagent/components/coder/factor_coder/config.py b/rdagent/components/coder/factor_coder/config.py index 36701262..21f89b7f 100644 --- a/rdagent/components/coder/factor_coder/config.py +++ b/rdagent/components/coder/factor_coder/config.py @@ -15,7 +15,7 @@ class FactorCoSTEERSettings(CoSTEERSettings): simple_background: bool = False """Whether to use simple background information for code feedback""" - file_based_execution_timeout: int = 120 + file_based_execution_timeout: int = 3600 """Timeout in seconds for each factor implementation execution""" select_method: str = "random" diff --git a/rdagent/components/coder/factor_coder/eva_utils.py b/rdagent/components/coder/factor_coder/eva_utils.py index 8c7705a4..30836a04 100644 --- a/rdagent/components/coder/factor_coder/eva_utils.py +++ b/rdagent/components/coder/factor_coder/eva_utils.py @@ -1,20 +1,16 @@ import io import json from abc import abstractmethod -from pathlib import Path from typing import Dict, Tuple import pandas as pd -from jinja2 import Environment, StrictUndefined from rdagent.components.coder.factor_coder.config import FACTOR_COSTEER_SETTINGS from rdagent.components.coder.factor_coder.factor import FactorTask from rdagent.core.experiment import Task, Workspace -from rdagent.core.prompts import Prompts from rdagent.oai.llm_conf import LLM_SETTINGS from rdagent.oai.llm_utils import APIBackend - -evaluate_prompts = Prompts(file_path=Path(__file__).parent / "prompts.yaml") +from rdagent.utils.agent.tpl import T class FactorEvaluator: @@ -81,36 +77,26 @@ class FactorCodeEvaluator(FactorEvaluator): factor_information = target_task.get_task_information() code = implementation.all_codes - system_prompt = ( - Environment(undefined=StrictUndefined) - .from_string(evaluate_prompts["evaluator_code_feedback_v1_system"]) - .render( - scenario=( - self.scen.get_scenario_all_desc( - target_task, - filtered_tag="feature", - simple_background=FACTOR_COSTEER_SETTINGS.simple_background, - ) - if self.scen is not None - else "No scenario description." + system_prompt = T(".prompts:evaluator_code_feedback_v1_system").r( + scenario=( + self.scen.get_scenario_all_desc( + target_task, + filtered_tag="feature", + simple_background=FACTOR_COSTEER_SETTINGS.simple_background, ) + if self.scen is not None + else "No scenario description." ) ) execution_feedback_to_render = execution_feedback for _ in range(10): # 10 times to split the content is enough - user_prompt = ( - Environment(undefined=StrictUndefined) - .from_string( - evaluate_prompts["evaluator_code_feedback_v1_user"], - ) - .render( - factor_information=factor_information, - code=code, - execution_feedback=execution_feedback_to_render, - value_feedback=value_feedback, - gt_code=gt_implementation.code if gt_implementation else None, - ) + user_prompt = T(".prompts:evaluator_code_feedback_v1_user").r( + factor_information=factor_information, + code=code, + execution_feedback=execution_feedback_to_render, + value_feedback=value_feedback, + gt_code=gt_implementation.code if gt_implementation else None, ) if ( APIBackend().build_messages_and_calculate_token( @@ -189,17 +175,11 @@ class FactorOutputFormatEvaluator(FactorEvaluator): buffer = io.StringIO() gen_df.info(buf=buffer) gen_df_info_str = f"The user is currently working on a feature related task.\nThe output dataframe info is:\n{buffer.getvalue()}" - system_prompt = ( - Environment(undefined=StrictUndefined) - .from_string( - evaluate_prompts["evaluator_output_format_system"], - ) - .render( - scenario=( - self.scen.get_scenario_all_desc(implementation.target_task, filtered_tag="feature") - if self.scen is not None - else "No scenario description." - ) + system_prompt = T(".prompts:evaluator_output_format_system").r( + scenario=( + self.scen.get_scenario_all_desc(implementation.target_task, filtered_tag="feature") + if self.scen is not None + else "No scenario description." ) ) @@ -504,35 +484,25 @@ class FactorFinalDecisionEvaluator(FactorEvaluator): code_feedback: str, **kwargs, ) -> Tuple: - system_prompt = ( - Environment(undefined=StrictUndefined) - .from_string(evaluate_prompts["evaluator_final_decision_v1_system"]) - .render( - scenario=( - self.scen.get_scenario_all_desc(target_task, filtered_tag="feature") - if self.scen is not None - else "No scenario description." - ) + system_prompt = T(".prompts:evaluator_final_decision_v1_system").r( + scenario=( + self.scen.get_scenario_all_desc(target_task, filtered_tag="feature") + if self.scen is not None + else "No scenario description." ) ) execution_feedback_to_render = execution_feedback for _ in range(10): # 10 times to split the content is enough - user_prompt = ( - Environment(undefined=StrictUndefined) - .from_string( - evaluate_prompts["evaluator_final_decision_v1_user"], - ) - .render( - factor_information=target_task.get_task_information(), - execution_feedback=execution_feedback_to_render, - code_feedback=code_feedback, - value_feedback=( - value_feedback - if value_feedback is not None - else "No Ground Truth Value provided, so no evaluation on value is performed." - ), - ) + user_prompt = T(".prompts:evaluator_final_decision_v1_user").r( + factor_information=target_task.get_task_information(), + execution_feedback=execution_feedback_to_render, + code_feedback=code_feedback, + value_feedback=( + value_feedback + if value_feedback is not None + else "No Ground Truth Value provided, so no evaluation on value is performed." + ), ) if ( APIBackend().build_messages_and_calculate_token( diff --git a/rdagent/components/coder/factor_coder/evolving_strategy.py b/rdagent/components/coder/factor_coder/evolving_strategy.py index 05804a84..e1d93c51 100644 --- a/rdagent/components/coder/factor_coder/evolving_strategy.py +++ b/rdagent/components/coder/factor_coder/evolving_strategy.py @@ -1,11 +1,8 @@ from __future__ import annotations import json -from pathlib import Path from typing import Dict -from jinja2 import Environment, StrictUndefined - from rdagent.components.coder.CoSTEER.evaluators import CoSTEERSingleFeedback from rdagent.components.coder.CoSTEER.evolving_strategy import ( MultiProcessEvolvingStrategy, @@ -17,11 +14,9 @@ from rdagent.components.coder.CoSTEER.knowledge_management import ( from rdagent.components.coder.factor_coder.config import FACTOR_COSTEER_SETTINGS from rdagent.components.coder.factor_coder.factor import FactorFBWorkspace, FactorTask from rdagent.core.experiment import FBWorkspace -from rdagent.core.prompts import Prompts from rdagent.oai.llm_conf import LLM_SETTINGS from rdagent.oai.llm_utils import APIBackend - -implement_prompts = Prompts(file_path=Path(__file__).parent / "prompts.yaml") +from rdagent.utils.agent.tpl import T class FactorMultiProcessEvolvingStrategy(MultiProcessEvolvingStrategy): @@ -36,24 +31,14 @@ class FactorMultiProcessEvolvingStrategy(MultiProcessEvolvingStrategy): queried_former_failed_knowledge_to_render: list, queried_similar_error_knowledge_to_render: list, ) -> str: - error_summary_system_prompt = ( - Environment(undefined=StrictUndefined) - .from_string(implement_prompts["evolving_strategy_error_summary_v2_system"]) - .render( - scenario=self.scen.get_scenario_all_desc(target_task), - factor_information_str=target_task.get_task_information(), - code_and_feedback=queried_former_failed_knowledge_to_render[-1].get_implementation_and_feedback_str(), - ) - .strip("\n") + error_summary_system_prompt = T(".prompts:evolving_strategy_error_summary_v2_system").r( + scenario=self.scen.get_scenario_all_desc(target_task), + factor_information_str=target_task.get_task_information(), + code_and_feedback=queried_former_failed_knowledge_to_render[-1].get_implementation_and_feedback_str(), ) for _ in range(10): # max attempt to reduce the length of error_summary_user_prompt - error_summary_user_prompt = ( - Environment(undefined=StrictUndefined) - .from_string(implement_prompts["evolving_strategy_error_summary_v2_user"]) - .render( - queried_similar_error_knowledge=queried_similar_error_knowledge_to_render, - ) - .strip("\n") + error_summary_user_prompt = T(".prompts:evolving_strategy_error_summary_v2_user").r( + queried_similar_error_knowledge=queried_similar_error_knowledge_to_render, ) if ( APIBackend().build_messages_and_calculate_token( @@ -106,16 +91,9 @@ class FactorMultiProcessEvolvingStrategy(MultiProcessEvolvingStrategy): latest_attempt_to_latest_successful_execution = queried_knowledge.task_to_former_failed_traces[ target_factor_task_information ][1] - - system_prompt = ( - Environment(undefined=StrictUndefined) - .from_string( - implement_prompts["evolving_strategy_factor_implementation_v1_system"], - ) - .render( - scenario=self.scen.get_scenario_all_desc(target_task, filtered_tag="feature"), - queried_former_failed_knowledge=queried_former_failed_knowledge_to_render, - ) + system_prompt = T(".prompts:evolving_strategy_factor_implementation_v1_system").r( + scenario=self.scen.get_scenario_all_desc(target_task, filtered_tag="feature"), + queried_former_failed_knowledge=queried_former_failed_knowledge_to_render, ) queried_similar_successful_knowledge_to_render = queried_similar_successful_knowledge queried_similar_error_knowledge_to_render = queried_similar_error_knowledge @@ -136,19 +114,12 @@ class FactorMultiProcessEvolvingStrategy(MultiProcessEvolvingStrategy): else: error_summary_critics = None # 构建user_prompt。开始写代码 - user_prompt = ( - Environment(undefined=StrictUndefined) - .from_string( - implement_prompts["evolving_strategy_factor_implementation_v2_user"], - ) - .render( - factor_information_str=target_factor_task_information, - queried_similar_successful_knowledge=queried_similar_successful_knowledge_to_render, - queried_similar_error_knowledge=queried_similar_error_knowledge_to_render, - error_summary_critics=error_summary_critics, - latest_attempt_to_latest_successful_execution=latest_attempt_to_latest_successful_execution, - ) - .strip("\n") + user_prompt = T(".prompts:evolving_strategy_factor_implementation_v2_user").r( + factor_information_str=target_factor_task_information, + queried_similar_successful_knowledge=queried_similar_successful_knowledge_to_render, + queried_similar_error_knowledge=queried_similar_error_knowledge_to_render, + error_summary_critics=error_summary_critics, + latest_attempt_to_latest_successful_execution=latest_attempt_to_latest_successful_execution, ) if ( APIBackend().build_messages_and_calculate_token(user_prompt=user_prompt, system_prompt=system_prompt) diff --git a/rdagent/components/coder/factor_coder/factor.py b/rdagent/components/coder/factor_coder/factor.py index 7902d429..398e966e 100644 --- a/rdagent/components/coder/factor_coder/factor.py +++ b/rdagent/components/coder/factor_coder/factor.py @@ -49,6 +49,12 @@ class FactorTask(CoSTEERTask): return f"""factor_name: {self.factor_name} factor_description: {self.factor_description} factor_formulation: {self.factor_formulation} +variables: {str(self.variables)}""" + + def get_task_brief_information(self): + return f"""factor_name: {self.factor_name} +factor_description: {self.factor_description} +factor_formulation: {self.factor_formulation} variables: {str(self.variables)}""" def get_task_information_and_implementation_result(self): diff --git a/rdagent/components/coder/factor_coder/prompts.yaml b/rdagent/components/coder/factor_coder/prompts.yaml index a9836cff..3d7d07f5 100644 --- a/rdagent/components/coder/factor_coder/prompts.yaml +++ b/rdagent/components/coder/factor_coder/prompts.yaml @@ -116,6 +116,9 @@ evolving_strategy_error_summary_v2_system: |- You suggestion should not include any code, just some clear and short suggestions. Please point out very critical issues in your response, ignore non-important issues to avoid confusion. If no big issue found in the code, you can response "No critics found". + [NOTE] + 1. When processing data, avoid time leakage. + Please response the critic in the following format. Here is an example structure for the output: critic 1: The critic message to critic 1 critic 2: The critic message to critic 2 diff --git a/rdagent/components/coder/model_coder/conf.py b/rdagent/components/coder/model_coder/conf.py new file mode 100644 index 00000000..b8a4efec --- /dev/null +++ b/rdagent/components/coder/model_coder/conf.py @@ -0,0 +1,13 @@ +from pydantic_settings import SettingsConfigDict + +from rdagent.components.coder.CoSTEER.config import CoSTEERSettings + + +class ModelCoSTEERSettings(CoSTEERSettings): + model_config = SettingsConfigDict(env_prefix="MODEL_CoSTEER_") + + env_type: str = "conda" # or "docker" + """Environment to run model code in coder and runner: 'conda' for local conda env, 'docker' for Docker container""" + + +MODEL_COSTEER_SETTINGS = ModelCoSTEERSettings() diff --git a/rdagent/components/coder/model_coder/eva_utils.py b/rdagent/components/coder/model_coder/eva_utils.py index 25ac7aa7..d434d112 100644 --- a/rdagent/components/coder/model_coder/eva_utils.py +++ b/rdagent/components/coder/model_coder/eva_utils.py @@ -1,18 +1,14 @@ import json -from pathlib import Path from typing import Dict, Tuple import numpy as np -from jinja2 import Environment, StrictUndefined from rdagent.components.coder.CoSTEER.evaluators import CoSTEEREvaluator from rdagent.components.coder.model_coder.model import ModelFBWorkspace, ModelTask from rdagent.core.experiment import Task, Workspace -from rdagent.core.prompts import Prompts from rdagent.oai.llm_conf import LLM_SETTINGS from rdagent.oai.llm_utils import APIBackend - -evaluate_prompts = Prompts(file_path=Path(__file__).parent / "prompts.yaml") +from rdagent.utils.agent.tpl import T # This shape evaluator is also used in data_science @@ -70,32 +66,21 @@ class ModelCodeEvaluator(CoSTEEREvaluator): model_task_information = target_task.get_task_information() code = implementation.all_codes - system_prompt = ( - Environment(undefined=StrictUndefined) - .from_string(evaluate_prompts["evaluator_code_feedback"]["system"]) - .render( - scenario=( - self.scen.get_scenario_all_desc(target_task, filtered_tag=target_task.model_type) - if self.scen is not None - else "No scenario description." - ) + system_prompt = T(".prompts:evaluator_code_feedback.system").r( + scenario=( + self.scen.get_scenario_all_desc(target_task, filtered_tag=target_task.model_type) + if self.scen is not None + else "No scenario description." ) ) - execution_feedback_to_render = model_execution_feedback for _ in range(10): # 10 times to split the content is enough - user_prompt = ( - Environment(undefined=StrictUndefined) - .from_string( - evaluate_prompts["evaluator_code_feedback"]["user"], - ) - .render( - model_information=model_task_information, - code=code, - model_execution_feedback=execution_feedback_to_render, - model_value_feedback=model_value_feedback, - gt_code=gt_implementation.all_codes if gt_implementation else None, - ) + user_prompt = T(".prompts:evaluator_code_feedback.user").r( + model_information=model_task_information, + code=code, + model_execution_feedback=execution_feedback_to_render, + model_value_feedback=model_value_feedback, + gt_code=gt_implementation.all_codes if gt_implementation else None, ) if ( APIBackend().build_messages_and_calculate_token( @@ -133,34 +118,25 @@ class ModelFinalEvaluator(CoSTEEREvaluator): if gt_implementation is not None: assert isinstance(gt_implementation, ModelFBWorkspace) - system_prompt = ( - Environment(undefined=StrictUndefined) - .from_string(evaluate_prompts["evaluator_final_feedback"]["system"]) - .render( - scenario=( - self.scen.get_scenario_all_desc(target_task, filtered_tag=target_task.model_type) - if self.scen is not None - else "No scenario description." - ) + system_prompt = T(".prompts:evaluator_final_feedback.system").r( + scenario=( + self.scen.get_scenario_all_desc(target_task, filtered_tag=target_task.model_type) + if self.scen is not None + else "No scenario description." ) ) execution_feedback_to_render = model_execution_feedback for _ in range(10): # 10 times to split the content is enough - user_prompt = ( - Environment(undefined=StrictUndefined) - .from_string( - evaluate_prompts["evaluator_final_feedback"]["user"], - ) - .render( - model_information=target_task.get_task_information(), - model_execution_feedback=execution_feedback_to_render, - model_shape_feedback=model_shape_feedback, - model_code_feedback=model_code_feedback, - model_value_feedback=model_value_feedback, - ) + user_prompt = T(".prompts:evaluator_final_feedback.user").r( + model_information=target_task.get_task_information(), + model_execution_feedback=execution_feedback_to_render, + model_shape_feedback=model_shape_feedback, + model_code_feedback=model_code_feedback, + model_value_feedback=model_value_feedback, ) + if ( APIBackend().build_messages_and_calculate_token( user_prompt=user_prompt, diff --git a/rdagent/components/coder/model_coder/evolving_strategy.py b/rdagent/components/coder/model_coder/evolving_strategy.py index 3d83c55e..24f84388 100644 --- a/rdagent/components/coder/model_coder/evolving_strategy.py +++ b/rdagent/components/coder/model_coder/evolving_strategy.py @@ -1,9 +1,6 @@ import json -from pathlib import Path from typing import Dict -from jinja2 import Environment, StrictUndefined - from rdagent.components.coder.CoSTEER.config import CoSTEER_SETTINGS from rdagent.components.coder.CoSTEER.evaluators import CoSTEERSingleFeedback from rdagent.components.coder.CoSTEER.evolving_strategy import ( @@ -14,16 +11,13 @@ from rdagent.components.coder.CoSTEER.knowledge_management import ( CoSTEERQueriedKnowledgeV2, ) from rdagent.components.coder.model_coder.model import ( - ModelExperiment, ModelFBWorkspace, ModelTask, ) from rdagent.core.experiment import FBWorkspace -from rdagent.core.prompts import Prompts from rdagent.oai.llm_conf import LLM_SETTINGS from rdagent.oai.llm_utils import APIBackend - -coder_prompts = Prompts(file_path=Path(__file__).parent / "prompts.yaml") +from rdagent.utils.agent.tpl import T class ModelMultiProcessEvolvingStrategy(MultiProcessEvolvingStrategy): @@ -52,31 +46,18 @@ class ModelMultiProcessEvolvingStrategy(MultiProcessEvolvingStrategy): if isinstance(queried_knowledge, CoSTEERQueriedKnowledgeV2) else queried_former_failed_knowledge ) - system_prompt = ( - Environment(undefined=StrictUndefined) - .from_string( - coder_prompts["evolving_strategy_model_coder"]["system"], - ) - .render( - scenario=self.scen.get_scenario_all_desc(filtered_tag=target_task.model_type), - queried_former_failed_knowledge=queried_former_failed_knowledge_to_render, - current_code=workspace.file_dict.get("model.py"), - ) + system_prompt = T(".prompts:evolving_strategy_model_coder.system").r( + scenario=self.scen.get_scenario_all_desc(filtered_tag="model"), + queried_former_failed_knowledge=queried_former_failed_knowledge_to_render, + current_code=workspace.file_dict.get("model.py"), ) queried_similar_successful_knowledge_to_render = queried_similar_successful_knowledge for _ in range(10): # max attempt to reduce the length of user_prompt - user_prompt = ( - Environment(undefined=StrictUndefined) - .from_string( - coder_prompts["evolving_strategy_model_coder"]["user"], - ) - .render( - model_information_str=model_information_str, - queried_similar_successful_knowledge=queried_similar_successful_knowledge_to_render, - queried_former_failed_knowledge=queried_former_failed_knowledge_to_render, - ) - .strip("\n") + user_prompt = T(".prompts:evolving_strategy_model_coder.user").r( + model_information_str=model_information_str, + queried_similar_successful_knowledge=queried_similar_successful_knowledge_to_render, + queried_former_failed_knowledge=queried_former_failed_knowledge_to_render, ) if ( APIBackend().build_messages_and_calculate_token( diff --git a/rdagent/components/coder/model_coder/model.py b/rdagent/components/coder/model_coder/model.py index 4e68d7de..9789cd5a 100644 --- a/rdagent/components/coder/model_coder/model.py +++ b/rdagent/components/coder/model_coder/model.py @@ -5,10 +5,11 @@ from pathlib import Path from typing import Dict, Optional from rdagent.components.coder.CoSTEER.task import CoSTEERTask +from rdagent.components.coder.model_coder.conf import MODEL_COSTEER_SETTINGS from rdagent.core.experiment import Experiment, FBWorkspace from rdagent.core.utils import cache_with_pickle from rdagent.oai.llm_utils import md5_hash -from rdagent.utils.env import KGDockerEnv, QTDockerEnv +from rdagent.utils.env import KGDockerEnv, QlibCondaConf, QlibCondaEnv, QTDockerEnv class ModelTask(CoSTEERTask): @@ -19,6 +20,7 @@ class ModelTask(CoSTEERTask): architecture: str, *args, hyperparameters: Dict[str, str], + training_hyperparameters: Dict[str, str], formulation: str = None, variables: Dict[str, str] = None, model_type: Optional[str] = None, @@ -28,6 +30,7 @@ class ModelTask(CoSTEERTask): self.architecture: str = architecture self.variables: str = variables self.hyperparameters: str = hyperparameters + self.training_hyperparameters: str = training_hyperparameters self.model_type: str = ( model_type # Tabular for tabular model, TimesSeries for time series model, Graph for graph model, XGBoost for XGBoost model ) @@ -41,6 +44,17 @@ description: {self.description} task_desc += f"architecture: {self.architecture}\n" task_desc += f"variables: {self.variables}\n" if self.variables else "" task_desc += f"hyperparameters: {self.hyperparameters}\n" + task_desc += f"training_hyperparameters: {self.training_hyperparameters}\n" + task_desc += f"model_type: {self.model_type}\n" + return task_desc + + def get_task_brief_information(self): + task_desc = f"""name: {self.name} +description: {self.description} +""" + task_desc += f"architecture: {self.architecture}\n" + task_desc += f"hyperparameters: {self.hyperparameters}\n" + task_desc += f"training_hyperparameters: {self.training_hyperparameters}\n" task_desc += f"model_type: {self.model_type}\n" return task_desc @@ -99,7 +113,15 @@ class ModelFBWorkspace(FBWorkspace): ): self.before_execute() try: - qtde = QTDockerEnv() if self.target_task.version == 1 else KGDockerEnv() + if self.target_task.version == 1: + if MODEL_COSTEER_SETTINGS.env_type == "docker": + qtde = QTDockerEnv() + elif MODEL_COSTEER_SETTINGS.env_type == "conda": + qtde = QlibCondaEnv(conf=QlibCondaConf()) + else: + raise ValueError(f"Unknown env_type: {MODEL_COSTEER_SETTINGS.env_type}") + else: + qtde = KGDockerEnv() qtde.prepare() if self.target_task.version == 1: diff --git a/rdagent/components/coder/model_coder/one_shot/__init__.py b/rdagent/components/coder/model_coder/one_shot/__init__.py index 29c9ff92..23345167 100644 --- a/rdagent/components/coder/model_coder/one_shot/__init__.py +++ b/rdagent/components/coder/model_coder/one_shot/__init__.py @@ -1,12 +1,10 @@ import re from pathlib import Path -from jinja2 import Environment, StrictUndefined - from rdagent.components.coder.model_coder.model import ModelExperiment, ModelFBWorkspace from rdagent.core.developer import Developer -from rdagent.core.prompts import Prompts from rdagent.oai.llm_utils import APIBackend +from rdagent.utils.agent.tpl import T DIRNAME = Path(__file__).absolute().resolve().parent @@ -17,18 +15,14 @@ class ModelCodeWriter(Developer[ModelExperiment]): for t in exp.sub_tasks: mti = ModelFBWorkspace(t) mti.prepare() - pr = Prompts(file_path=DIRNAME / "prompt.yaml") - user_prompt_tpl = Environment(undefined=StrictUndefined).from_string(pr["code_implement_user"]) - sys_prompt_tpl = Environment(undefined=StrictUndefined).from_string(pr["code_implement_sys"]) - - user_prompt = user_prompt_tpl.render( + user_prompt = T(".prompts:code_implement_user").r( name=t.name, description=t.description, formulation=t.formulation, variables=t.variables, ) - system_prompt = sys_prompt_tpl.render() + system_prompt = T(".prompts:code_implement_sys").r() resp = APIBackend().build_messages_and_create_chat_completion(user_prompt, system_prompt) diff --git a/rdagent/components/coder/model_coder/task_loader.py b/rdagent/components/coder/model_coder/task_loader.py index a0867745..a9922e28 100644 --- a/rdagent/components/coder/model_coder/task_loader.py +++ b/rdagent/components/coder/model_coder/task_loader.py @@ -2,19 +2,16 @@ from __future__ import annotations import json import re -from pathlib import Path -from rdagent.components.coder.model_coder.model import ModelExperiment, ModelTask +from rdagent.components.coder.model_coder.model import ModelTask from rdagent.components.document_reader.document_reader import ( load_and_process_pdfs_by_langchain, ) from rdagent.components.loader.task_loader import ModelTaskLoader -from rdagent.core.prompts import Prompts from rdagent.log import rdagent_logger as logger from rdagent.oai.llm_utils import APIBackend from rdagent.scenarios.qlib.experiment.model_experiment import QlibModelExperiment - -document_process_prompts = Prompts(file_path=Path(__file__).parent / "prompts.yaml") +from rdagent.utils.agent.tpl import T def extract_model_from_doc(doc_content: str) -> dict: @@ -32,7 +29,7 @@ def extract_model_from_doc(doc_content: str) -> dict: {model_name: dict{description, formulation, variables}} """ session = APIBackend().build_chat_session( - session_system_prompt=document_process_prompts["extract_model_formulation_system"], + session_system_prompt=T(".prompts:extract_model_formulation_system").r(), ) current_user_prompt = doc_content diff --git a/rdagent/components/proposal/__init__.py b/rdagent/components/proposal/__init__.py index 51980766..424233f0 100644 --- a/rdagent/components/proposal/__init__.py +++ b/rdagent/components/proposal/__init__.py @@ -1,11 +1,7 @@ from abc import abstractmethod -from pathlib import Path from typing import Tuple -from jinja2 import Environment, StrictUndefined - from rdagent.core.experiment import Experiment -from rdagent.core.prompts import Prompts from rdagent.core.proposal import ( Hypothesis, Hypothesis2Experiment, @@ -14,8 +10,7 @@ from rdagent.core.proposal import ( Trace, ) from rdagent.oai.llm_utils import APIBackend - -prompt_dict = Prompts(file_path=Path(__file__).parent / "prompts.yaml") +from rdagent.utils.agent.tpl import T class LLMHypothesisGen(HypothesisGen): @@ -32,27 +27,31 @@ class LLMHypothesisGen(HypothesisGen): def gen(self, trace: Trace) -> Hypothesis: context_dict, json_flag = self.prepare_context(trace) - system_prompt = ( - Environment(undefined=StrictUndefined) - .from_string(prompt_dict["hypothesis_gen"]["system_prompt"]) - .render( - targets=self.targets, - scenario=self.scen.get_scenario_all_desc(filtered_tag="hypothesis_and_experiment"), - hypothesis_output_format=context_dict["hypothesis_output_format"], - hypothesis_specification=context_dict["hypothesis_specification"], - ) + system_prompt = T(".prompts:hypothesis_gen.system_prompt").r( + targets=self.targets, + scenario=( + self.scen.get_scenario_all_desc(filtered_tag=self.targets) + if self.targets in ["factor", "model"] + else self.scen.get_scenario_all_desc(filtered_tag="hypothesis_and_experiment") + ), + hypothesis_output_format=context_dict["hypothesis_output_format"], + hypothesis_specification=context_dict["hypothesis_specification"], ) - user_prompt = ( - Environment(undefined=StrictUndefined) - .from_string(prompt_dict["hypothesis_gen"]["user_prompt"]) - .render( - targets=self.targets, - hypothesis_and_feedback=context_dict["hypothesis_and_feedback"], - RAG=context_dict["RAG"], - ) + user_prompt = T(".prompts:hypothesis_gen.user_prompt").r( + targets=self.targets, + hypothesis_and_feedback=context_dict["hypothesis_and_feedback"], + last_hypothesis_and_feedback=( + context_dict["last_hypothesis_and_feedback"] if "last_hypothesis_and_feedback" in context_dict else "" + ), + sota_hypothesis_and_feedback=( + context_dict["sota_hypothesis_and_feedback"] if "sota_hypothesis_and_feedback" in context_dict else "" + ), + RAG=context_dict["RAG"], ) - resp = APIBackend().build_messages_and_create_chat_completion(user_prompt, system_prompt, json_mode=json_flag) + resp = APIBackend().build_messages_and_create_chat_completion( + user_prompt, system_prompt, json_mode=json_flag, json_target_type=dict[str, str] + ) hypothesis = self.convert_response(resp) @@ -86,28 +85,30 @@ class LLMHypothesis2Experiment(Hypothesis2Experiment[Experiment]): def convert(self, hypothesis: Hypothesis, trace: Trace) -> Experiment: context, json_flag = self.prepare_context(hypothesis, trace) - system_prompt = ( - Environment(undefined=StrictUndefined) - .from_string(prompt_dict["hypothesis2experiment"]["system_prompt"]) - .render( - targets=self.targets, - scenario=trace.scen.get_scenario_all_desc(filtered_tag="hypothesis_and_experiment"), - experiment_output_format=context["experiment_output_format"], - ) + system_prompt = T(".prompts:hypothesis2experiment.system_prompt").r( + targets=self.targets, + scenario=trace.scen.get_scenario_all_desc(filtered_tag=self.targets), + experiment_output_format=context["experiment_output_format"], ) - user_prompt = ( - Environment(undefined=StrictUndefined) - .from_string(prompt_dict["hypothesis2experiment"]["user_prompt"]) - .render( - targets=self.targets, - target_hypothesis=context["target_hypothesis"], - hypothesis_and_feedback=context["hypothesis_and_feedback"], - target_list=context["target_list"], - RAG=context["RAG"], - ) + user_prompt = T(".prompts:hypothesis2experiment.user_prompt").r( + targets=self.targets, + target_hypothesis=context["target_hypothesis"], + hypothesis_and_feedback=( + context["hypothesis_and_feedback"] if "hypothesis_and_feedback" in context else "" + ), + last_hypothesis_and_feedback=( + context["last_hypothesis_and_feedback"] if "last_hypothesis_and_feedback" in context else "" + ), + sota_hypothesis_and_feedback=( + context["sota_hypothesis_and_feedback"] if "sota_hypothesis_and_feedback" in context else "" + ), + target_list=context["target_list"], + RAG=context["RAG"], ) - resp = APIBackend().build_messages_and_create_chat_completion(user_prompt, system_prompt, json_mode=json_flag) + resp = APIBackend().build_messages_and_create_chat_completion( + user_prompt, system_prompt, json_mode=json_flag, json_target_type=dict[str, dict[str, str | dict]] + ) return self.convert_response(resp, hypothesis, trace) diff --git a/rdagent/components/proposal/prompts.yaml b/rdagent/components/proposal/prompts.yaml index f98bde6b..bb9ca77d 100644 --- a/rdagent/components/proposal/prompts.yaml +++ b/rdagent/components/proposal/prompts.yaml @@ -1,48 +1,64 @@ hypothesis_gen: system_prompt: |- - The user is working on generating new hypotheses for the {{targets}} in a data-driven research and development process. - The {{targets}} are used in the following scenario: - {{scenario}} - The user has already proposed several hypotheses and conducted evaluations on them. This information will be provided to you. Your task is to check whether a similar hypothesis has already been generated. + The user is working on generating new hypotheses for the {{ targets }} in a data-driven research and development process. + The {{ targets }} are used in the following scenario: + {{ scenario }} + The user has already proposed several hypotheses and conducted evaluations on them. This information will be provided to you. Your task is to analyze previous experiments, reflect on the decision made in each experiment, and consider why experiments with a decision of true were successful while those with a decision of false failed. Then, think about how to improve further — either by refining the existing approach or by exploring an entirely new direction. If one exists and you agree with it, feel free to use it. If you disagree, please generate an improved version. {% if hypothesis_specification %} - To assist you in formulating new hypotheses, the user has provided some additional information: {{hypothesis_specification}}. + To assist you in formulating new hypotheses, the user has provided some additional information: {{ hypothesis_specification }}. **Important:** If the hypothesis_specification outlines the next steps you need to follow, ensure you adhere to those instructions. {% endif %} Please generate the output using the following format and specifications: {{ hypothesis_output_format }} user_prompt: |- - {% if hypothesis_and_feedback|length == 0 %}It is the first round of hypothesis generation. The user has no hypothesis on this scenario yet. - {% else %}It is not the first round, the user has made several hypothesis on this scenario and did several evaluation on them. - The former hypothesis and the corresponding feedbacks are as follows (focus on the last one & the new hypothesis that it provides and reasoning to see if you agree): + {% if hypothesis_and_feedback|length == 0 %} + It is the first round of hypothesis generation. The user has no hypothesis on this scenario yet. + {% else %} + The former hypothesis and the corresponding feedbacks are as follows: {{ hypothesis_and_feedback }} {% endif %} - {% if RAG %} - To assist you in generating new {{targets}}, we have provided the following information: {{RAG}}. - **Note:** The provided RAG is for reference only. - You must carefully assess whether the RAG aligns with the {{targets}}. - If it does not, it should not be used. Exercise caution and make your own judgment. + {% if last_hypothesis_and_feedback %} + Here is the last trial's hypothesis and the corresponding feedback (The main feedback contains a new hypothesis for your reference only. You need to evaluate the complete trace chain to decide whether to adopt it or propose a more appropriate hypothesis): + {{ last_hypothesis_and_feedback }} + {% endif %} + {% if sota_hypothesis_and_feedback != "" %} + Here is the SOTA trail's hypothesis and the corresponding feedback: + {{ sota_hypothesis_and_feedback }} + {% endif %} + {% if RAG %} + To assist you in generating new {{ targets }}, we have provided the following information: {{ RAG }}. {% endif %} - Also generate the relevant keys for the reasoning and the distilled knowledge that follows. For those keys, in particular for knowledge, explain in the context of the specific scenario to build up domain knowledge in the specific field rather than general knowledge. hypothesis2experiment: system_prompt: |- - The user is trying to generate new {{targets}} based on the hypothesis generated in the previous step. - The {{targets}} are used in certain scenario, the scenario is as follows: + The user is trying to generate new {{ targets }} based on the hypothesis generated in the previous step. + The {{ targets }} are used in certain scenario, the scenario is as follows: {{ scenario }} - The user will use the {{targets}} generated to do some experiments. The user will provide this information to you: - 1. The target hypothesis you are targeting to generate {{targets}} for. + The user will use the {{ targets }} generated to do some experiments. The user will provide this information to you: + 1. The target hypothesis you are targeting to generate {{ targets }} for. 2. The hypothesis generated in the previous steps and their corresponding feedbacks. - 3. Former proposed {{targets}} on similar hypothesis. - 4. Some additional information to help you generate new {{targets}}. + 3. Former proposed {{ targets }} on similar hypothesis. + 4. Some additional information to help you generate new {{ targets }}. Please generate the output following the format below: {{ experiment_output_format }} user_prompt: |- The user has made several hypothesis on this scenario and did several evaluation on them. - The target hypothesis you are targeting to generate {{targets}} for is as follows: + The target hypothesis you are targeting to generate {{ targets }} for is as follows: {{ target_hypothesis }} + {% if hypothesis_and_feedback %} The former hypothesis and the corresponding feedbacks are as follows: {{ hypothesis_and_feedback }} - Please generate the new {{targets}} based on the information above. + {% endif %} + {% if last_hypothesis_and_feedback %} + The latest hypothesis and the corresponding feedback are as follows: + {{ last_hypothesis_and_feedback }} + {% endif %} + {% if sota_hypothesis_and_feedback %} + The SOTA hypothesis and the corresponding feedback are as follows: + {{ sota_hypothesis_and_feedback }} + {% endif %} + + Please generate the new {{ targets }} based on the information above. diff --git a/rdagent/components/workflow/rd_loop.py b/rdagent/components/workflow/rd_loop.py index 11ef2ba4..a79a5a8b 100644 --- a/rdagent/components/workflow/rd_loop.py +++ b/rdagent/components/workflow/rd_loop.py @@ -78,12 +78,14 @@ class RDLoop(LoopBase, metaclass=LoopMeta): e = prev_out.get(self.EXCEPTION_KEY, None) if e is not None: feedback = HypothesisFeedback( - observations="Error occurred in loop, skip this loop", + observations=e, hypothesis_evaluation="", new_hypothesis="", reason="", decision=False, ) + with logger.tag("ef"): # evaluate and feedback + logger.log_object(feedback, tag="feedback") self.trace.hist.append((prev_out["direct_exp_gen"]["exp_gen"], feedback)) else: feedback = self.summarizer.generate_feedback(prev_out["running"], self.trace) diff --git a/rdagent/core/conf.py b/rdagent/core/conf.py index 0824c2fe..82e69cb7 100644 --- a/rdagent/core/conf.py +++ b/rdagent/core/conf.py @@ -82,5 +82,7 @@ class RDAgentSettings(ExtendedBaseSettings): enable_mlflow: bool = False + initial_fator_library_size: int = 20 + RD_AGENT_SETTINGS = RDAgentSettings() diff --git a/rdagent/core/proposal.py b/rdagent/core/proposal.py index a46759ca..ccf98991 100644 --- a/rdagent/core/proposal.py +++ b/rdagent/core/proposal.py @@ -3,16 +3,13 @@ from __future__ import annotations from abc import ABC, abstractmethod -from typing import TYPE_CHECKING, Generic, TypeVar +from typing import Generic, TypeVar from rdagent.core.evaluation import Feedback from rdagent.core.experiment import ASpecificExp, Experiment from rdagent.core.knowledge_base import KnowledgeBase from rdagent.core.scenario import Scenario -if TYPE_CHECKING: - from rdagent.core.prompts import Prompts - # class data_ana: XXX @@ -42,12 +39,7 @@ class Hypothesis: def __str__(self) -> str: return f"""Hypothesis: {self.hypothesis} - Reason: {self.reason} - Concise Reason & Knowledge: {self.concise_reason} - Concise Observation: {self.concise_observation} - Concise Justification: {self.concise_justification} - Concise Knowledge: {self.concise_knowledge} - """ +Reason: {self.reason}""" # source: data_ana | model_nan = None @@ -187,11 +179,6 @@ class ExpGen(ABC): class HypothesisGen(ABC): - # NOTE: the design is a little wierd - # - Sometimes we want accurate access the prompts in a specific level - # - It renders the prompt to a specific abstract level - # - Sometimes we want to access the most recent level prompts - prompts: Prompts # this is a class level prompt. def __init__(self, scen: Scenario) -> None: self.scen = scen diff --git a/rdagent/log/ui/app.py b/rdagent/log/ui/app.py index 3fa2eca9..e988aa43 100644 --- a/rdagent/log/ui/app.py +++ b/rdagent/log/ui/app.py @@ -35,6 +35,7 @@ from rdagent.scenarios.qlib.experiment.model_experiment import ( QlibModelExperiment, QlibModelScenario, ) +from rdagent.scenarios.qlib.experiment.quant_experiment import QlibQuantScenario st.set_page_config(layout="wide", page_title="RD-Agent", page_icon="🎓", initial_sidebar_state="expanded") @@ -55,12 +56,19 @@ else: QLIB_SELECTED_METRICS = [ "IC", - "1day.excess_return_without_cost.annualized_return", - "1day.excess_return_without_cost.information_ratio", - "1day.excess_return_without_cost.max_drawdown", + "1day.excess_return_with_cost.annualized_return", + "1day.excess_return_with_cost.information_ratio", + "1day.excess_return_with_cost.max_drawdown", ] -SIMILAR_SCENARIOS = (QlibModelScenario, DMModelScenario, QlibFactorScenario, QlibFactorFromReportScenario, KGScenario) +SIMILAR_SCENARIOS = ( + QlibModelScenario, + DMModelScenario, + QlibFactorScenario, + QlibFactorFromReportScenario, + QlibQuantScenario, + KGScenario, +) def filter_log_folders(main_log_path): @@ -123,6 +131,9 @@ if "h_decisions" not in state: if "metric_series" not in state: state.metric_series = [] +if "all_metric_series" not in state: + state.all_metric_series = [] + # Factor Task Baseline if "alpha158_metrics" not in state: state.alpha158_metrics = None @@ -164,7 +175,10 @@ def get_msgs_until(end_func: Callable[[Message], bool] = lambda _: True): # Update Summary Info if "model runner result" in tags or "factor runner result" in tags or "runner result" in tags: # factor baseline exp metrics - if isinstance(state.scenario, QlibFactorScenario) and state.alpha158_metrics is None: + if ( + isinstance(state.scenario, (QlibFactorScenario, QlibQuantScenario)) + and state.alpha158_metrics is None + ): sms = msg.content.based_experiments[0].result.loc[QLIB_SELECTED_METRICS] sms.name = "alpha158" state.alpha158_metrics = sms @@ -178,11 +192,19 @@ def get_msgs_until(end_func: Callable[[Message], bool] = lambda _: True): if isinstance(state.scenario, DMModelScenario): sms.index = ["AUROC"] elif isinstance( - state.scenario, (QlibModelScenario, QlibFactorFromReportScenario, QlibFactorScenario) + state.scenario, + ( + QlibModelScenario, + QlibFactorFromReportScenario, + QlibFactorScenario, + QlibQuantScenario, + ), ): + sms_all = sms sms = sms.loc[QLIB_SELECTED_METRICS] sms.name = f"Baseline" state.metric_series.append(sms) + state.all_metric_series.append(sms_all) # common metrics if msg.content.result is None: @@ -195,12 +217,21 @@ def get_msgs_until(end_func: Callable[[Message], bool] = lambda _: True): if isinstance(state.scenario, DMModelScenario): sms.index = ["AUROC"] elif isinstance( - state.scenario, (QlibModelScenario, QlibFactorFromReportScenario, QlibFactorScenario) + state.scenario, + ( + QlibModelScenario, + QlibFactorFromReportScenario, + QlibFactorScenario, + QlibQuantScenario, + ), ): + sms_all = sms sms = sms.loc[QLIB_SELECTED_METRICS] sms.name = f"Round {state.lround}" + sms_all.name = f"Round {state.lround}" state.metric_series.append(sms) + state.all_metric_series.append(sms_all) elif "hypothesis generation" in tags: state.hypotheses[state.lround] = msg.content elif "ef" in tags and "feedback" in tags: @@ -254,8 +285,9 @@ def refresh(same_trace: bool = False): # detect scenario if not same_trace: - get_msgs_until(lambda m: not isinstance(m.content, str)) + get_msgs_until(lambda m: "debug_" not in m.tag and not isinstance(m.content, str)) if state.last_msg is None or not isinstance(state.last_msg.content, Scenario): + st.write(state.msgs) st.toast(":red[**No Scenario Info detected**]", icon="❗") state.scenario = None else: @@ -269,6 +301,7 @@ def refresh(same_trace: bool = False): state.hypotheses = defaultdict(None) state.h_decisions = defaultdict(bool) state.metric_series = [] + state.all_metric_series = [] state.last_msg = None state.current_tags = [] state.alpha158_metrics = None @@ -335,6 +368,9 @@ def display_hypotheses(hypotheses: dict[int, Hypothesis], decisions: dict[int, b df.drop(["concise_reason"], axis=1, inplace=True) df.columns = df.columns.map(lambda x: name_dict.get(x, x)) + for col in list(df.columns): + if all([value is None for value in df[col]]): + df.drop([col], axis=1, inplace=True) def style_rows(row): if decisions[row.name]: @@ -396,6 +432,13 @@ def metrics_window(df: pd.DataFrame, R: int, C: int, *, height: int = 300, color ) st.plotly_chart(fig) + from io import BytesIO + + buffer = BytesIO() + df.to_csv(buffer) + buffer.seek(0) + st.download_button(label="download the metrics (csv)", data=buffer, file_name="metrics.csv", mime="text/csv") + def summary_window(): if isinstance(state.scenario, SIMILAR_SCENARIOS): @@ -422,6 +465,8 @@ def summary_window(): with chart_c: if isinstance(state.scenario, QlibFactorScenario) and state.alpha158_metrics is not None: df = pd.DataFrame([state.alpha158_metrics] + state.metric_series) + elif isinstance(state.scenario, QlibQuantScenario) and state.alpha158_metrics is not None: + df = pd.DataFrame([state.alpha158_metrics] + state.metric_series) else: df = pd.DataFrame(state.metric_series) if show_true_only and len(state.hypotheses) >= len(state.metric_series): @@ -516,6 +561,7 @@ def tasks_window(tasks: list[FactorTask | ModelTask]): for v, d in mt.variables.items(): mks += f"| ${v}$ | {d} |\n" st.markdown(mks) + st.markdown(f"**Train Para**: {mt.training_hyperparameters}") def research_window(): @@ -562,13 +608,54 @@ def research_window(): def feedback_window(): + # st.write(round) + # # Check if metric series exists and has the matching round + # if state.all_metric_series: + # for metric in state.all_metric_series: + # if metric.name == f"Round {round}": + # # Select specific metrics with cost + # selected_metrics_with_cost = { + # 'IC': float(f"{metric['IC']:.4f}"), + # 'ICIR': float(f"{metric['ICIR']:.4f}"), + # 'Rank IC': float(f"{metric['Rank IC']:.4f}"), + # 'Rank ICIR': float(f"{metric['Rank ICIR']:.4f}"), + # 'ARR': float(f"{metric['1day.excess_return_with_cost.annualized_return']:.4f}"), + # 'IR': float(f"{metric['1day.excess_return_with_cost.information_ratio']:.4f}"), + # 'MDD': float(f"{metric['1day.excess_return_with_cost.max_drawdown']:.4f}"), + # 'Sharpe': float(f"{metric['1day.excess_return_with_cost.annualized_return'] / abs(metric['1day.excess_return_with_cost.max_drawdown']):.4f}") + # } + # st.write("With Cost Metrics:") + # st.write(pd.Series(selected_metrics_with_cost)) + + # # Select specific metrics without cost + # selected_metrics_without_cost = { + # 'IC': float(f"{metric['IC']:.4f}"), + # 'ICIR': float(f"{metric['ICIR']:.4f}"), + # 'Rank IC': float(f"{metric['Rank IC']:.4f}"), + # 'Rank ICIR': float(f"{metric['Rank ICIR']:.4f}"), + # 'ARR': float(f"{metric['1day.excess_return_without_cost.annualized_return']:.4f}"), + # 'IR': float(f"{metric['1day.excess_return_without_cost.information_ratio']:.4f}"), + # 'MDD': float(f"{metric['1day.excess_return_without_cost.max_drawdown']:.4f}"), + # 'Sharpe': float(f"{metric['1day.excess_return_without_cost.annualized_return'] / abs(metric['1day.excess_return_without_cost.max_drawdown']):.4f}") + # } + # st.write("Without Cost Metrics:") + # st.write(pd.Series(selected_metrics_without_cost)) + # break if isinstance(state.scenario, SIMILAR_SCENARIOS): with st.container(border=True): st.subheader("Feedback📝", divider="orange", anchor="_feedback") if state.lround > 0 and isinstance( - state.scenario, (QlibModelScenario, QlibFactorScenario, QlibFactorFromReportScenario, KGScenario) + state.scenario, + (QlibModelScenario, QlibFactorScenario, QlibFactorFromReportScenario, QlibQuantScenario, KGScenario), ): + if fbr := state.msgs[round]["ef.runner result"]: + try: + st.write("workspace") + st.write(fbr[0].content.experiment_workspace.workspace_path) + st.write(fbr[0].content.stdout) + except Exception as e: + st.error(f"Error displaying workspace path: {str(e)}") with st.expander("**Config⚙️**", expanded=True): st.markdown(state.scenario.experiment_setting, unsafe_allow_html=True) @@ -812,8 +899,238 @@ def show_times(round: int): st.markdown(f"**:blue[{k}]**: :red[**{minutes}**] minutes :orange[**{seconds}**] seconds") +def analyze_task_completion(): + st.header("Task Completion Analysis", divider="orange") + + # Dictionary to store results for all loops + completion_stats = {} + + # Iterate through all loops + for loop_round in state.msgs.keys(): + if loop_round == 0: # Skip initialization round + continue + + max_evolving_round = state.erounds[loop_round] + if max_evolving_round == 0: + continue + + # Track tasks that pass in each evolving round + tasks_passed_by_round = {} + cumulative_passed = set() + + # For each evolving round in this loop + for e_round in range(1, max_evolving_round + 1): + if len(state.msgs[loop_round]["d.evolving feedback"]) >= e_round: + # Get feedback for this evolving round + feedback = state.msgs[loop_round]["d.evolving feedback"][e_round - 1].content + + # Count passed tasks and track their indices + passed_tasks = set() + for j, task_feedback in enumerate(feedback): + if task_feedback.final_decision: + passed_tasks.add(j) + cumulative_passed.add(j) + + # Store both individual round results and cumulative results + tasks_passed_by_round[e_round] = { + "count": len(passed_tasks), + "indices": passed_tasks, + "cumulative_count": len(cumulative_passed), + "cumulative_indices": cumulative_passed.copy(), + } + + completion_stats[loop_round] = { + "total_tasks": len(state.msgs[loop_round]["d.evolving feedback"][0].content), + "rounds": tasks_passed_by_round, + "max_round": max_evolving_round, + } + + # Display results + if completion_stats: + # Add an aggregate view at the top + st.subheader("🔄 Aggregate Completion Across All Loops") + + # Create summary data for comparison + summary_data = [] + total_tasks_across_loops = 0 + total_passed_r1 = 0 + total_passed_r3 = 0 + total_passed_r5 = 0 + total_passed_r10 = 0 + total_passed_final = 0 + + for loop_round, stats in completion_stats.items(): + total_tasks = stats["total_tasks"] + total_tasks_across_loops += total_tasks + + # Find data for specific rounds + r1_passed = stats["rounds"].get(1, {}).get("cumulative_count", 0) + total_passed_r1 += r1_passed + + # For round 3, use the closest round if exactly 3 doesn't exist + if 3 in stats["rounds"]: + r3_passed = stats["rounds"][3]["cumulative_count"] + elif stats["max_round"] >= 3: + max_r_below_3 = max([r for r in stats["rounds"].keys() if r <= 3]) + r3_passed = stats["rounds"][max_r_below_3]["cumulative_count"] + else: + r3_passed = stats["rounds"][stats["max_round"]]["cumulative_count"] if stats["rounds"] else 0 + total_passed_r3 += r3_passed + + # For round 5, use the closest round if exactly 5 doesn't exist + if 5 in stats["rounds"]: + r5_passed = stats["rounds"][5]["cumulative_count"] + elif stats["max_round"] >= 5: + max_r_below_5 = max([r for r in stats["rounds"].keys() if r <= 5]) + r5_passed = stats["rounds"][max_r_below_5]["cumulative_count"] + else: + r5_passed = stats["rounds"][stats["max_round"]]["cumulative_count"] if stats["rounds"] else 0 + total_passed_r5 += r5_passed + + # For round 10 + if 10 in stats["rounds"]: + r10_passed = stats["rounds"][10]["cumulative_count"] + else: + r10_passed = stats["rounds"][stats["max_round"]]["cumulative_count"] if stats["rounds"] else 0 + total_passed_r10 += r10_passed + + # Final round completion + final_passed = stats["rounds"][stats["max_round"]]["cumulative_count"] if stats["rounds"] else 0 + total_passed_final += final_passed + + # Add to summary table + summary_data.append( + { + "Loop": f"Loop {loop_round}", + "Total Tasks": total_tasks, + "Passed (Round 1)": ( + f"{r1_passed}/{total_tasks} ({r1_passed/total_tasks:.0%})" if total_tasks > 0 else "N/A" + ), + "Passed (Round 3)": ( + f"{r3_passed}/{total_tasks} ({r3_passed/total_tasks:.0%})" if total_tasks > 0 else "N/A" + ), + "Passed (Round 5)": ( + f"{r5_passed}/{total_tasks} ({r5_passed/total_tasks:.0%})" if total_tasks > 0 else "N/A" + ), + "Passed (Final)": ( + f"{final_passed}/{total_tasks} ({final_passed/total_tasks:.0%})" if total_tasks > 0 else "N/A" + ), + } + ) + + if total_tasks_across_loops > 0: + summary_data.append( + { + "Loop": "**TOTAL**", + "Total Tasks": total_tasks_across_loops, + "Passed (Round 1)": f"**{total_passed_r1}/{total_tasks_across_loops} ({total_passed_r1/total_tasks_across_loops:.0%})**", + "Passed (Round 3)": f"**{total_passed_r3}/{total_tasks_across_loops} ({total_passed_r3/total_tasks_across_loops:.0%})**", + "Passed (Round 5)": f"**{total_passed_r5}/{total_tasks_across_loops} ({total_passed_r5/total_tasks_across_loops:.0%})**", + "Passed (Final)": f"**{total_passed_final}/{total_tasks_across_loops} ({total_passed_final/total_tasks_across_loops:.0%})**", + } + ) + + st.table(pd.DataFrame(summary_data)) + + # Summary statistics + st.markdown("### 📊 Overall Completion Progress:") + col1, col2, col3, col4 = st.columns(4) + with col1: + st.metric( + label="After Round 1", + value=f"{total_passed_r1/total_tasks_across_loops:.0%}", + help=f"{total_passed_r1}/{total_tasks_across_loops} tasks", + ) + with col2: + st.metric( + label="After Round 3", + value=f"{total_passed_r3/total_tasks_across_loops:.0%}", + delta=f"{(total_passed_r3-total_passed_r1)/total_tasks_across_loops:.0%}", + help=f"{total_passed_r3}/{total_tasks_across_loops} tasks", + ) + with col3: + st.metric( + label="After Round 5", + value=f"{total_passed_r5/total_tasks_across_loops:.0%}", + delta=f"{(total_passed_r5-total_passed_r3)/total_tasks_across_loops:.0%}", + help=f"{total_passed_r5}/{total_tasks_across_loops} tasks", + ) + with col4: + st.metric( + label="Final Completion", + value=f"{total_passed_final/total_tasks_across_loops:.0%}", + delta=f"{(total_passed_final-total_passed_r5)/total_tasks_across_loops:.0%}", + help=f"{total_passed_final}/{total_tasks_across_loops} tasks", + ) + + # Show detailed results by loop + st.markdown("---") + st.subheader("Detailed Results by Loop") + + for loop_round, stats in completion_stats.items(): + with st.expander(f"Loop {loop_round} Details"): + total_tasks = stats["total_tasks"] + + # Create a results table + data = [] + for e_round in range(1, min(11, stats["max_round"] + 1)): + if e_round in stats["rounds"]: + round_data = stats["rounds"][e_round] + data.append( + { + "Evolving Round": e_round, + "Tasks Passed": f"{round_data['count']}/{total_tasks} ({round_data['count']/total_tasks:.0%})", + "Cumulative Passed": f"{round_data['cumulative_count']}/{total_tasks} ({round_data['cumulative_count']/total_tasks:.0%})", + } + ) + else: + data.append({"Evolving Round": e_round, "Tasks Passed": "N/A", "Cumulative Passed": "N/A"}) + + df = pd.DataFrame(data) + st.table(df) + + st.markdown("### Summary:") + if 1 in stats["rounds"]: + st.markdown( + f"- After round 1: **{stats['rounds'][1]['cumulative_count']}/{total_tasks}** tasks passed ({stats['rounds'][1]['cumulative_count']/total_tasks:.0%})" + ) + + if 3 in stats["rounds"]: + st.markdown( + f"- After round 3: **{stats['rounds'][3]['cumulative_count']}/{total_tasks}** tasks passed ({stats['rounds'][3]['cumulative_count']/total_tasks:.0%})" + ) + elif stats["max_round"] >= 3: + max_round_below_3 = max([r for r in stats["rounds"].keys() if r <= 3]) + st.markdown( + f"- After round 3: **{stats['rounds'][max_round_below_3]['cumulative_count']}/{total_tasks}** tasks passed ({stats['rounds'][max_round_below_3]['cumulative_count']/total_tasks:.0%})" + ) + + if 5 in stats["rounds"]: + st.markdown( + f"- After round 5: **{stats['rounds'][5]['cumulative_count']}/{total_tasks}** tasks passed ({stats['rounds'][5]['cumulative_count']/total_tasks:.0%})" + ) + elif stats["max_round"] >= 5: + max_round_below_5 = max([r for r in stats["rounds"].keys() if r <= 5]) + st.markdown( + f"- After round 5: **{stats['rounds'][max_round_below_5]['cumulative_count']}/{total_tasks}** tasks passed ({stats['rounds'][max_round_below_5]['cumulative_count']/total_tasks:.0%})" + ) + + if 10 in stats["rounds"]: + st.markdown( + f"- After round 10: **{stats['rounds'][10]['cumulative_count']}/{total_tasks}** tasks passed ({stats['rounds'][10]['cumulative_count']/total_tasks:.0%})" + ) + elif stats["max_round"] >= 1: + st.markdown( + f"- After final round ({stats['max_round']}): **{stats['rounds'][stats['max_round']]['cumulative_count']}/{total_tasks}** tasks passed ({stats['rounds'][stats['max_round']]['cumulative_count']/total_tasks:.0%})" + ) + else: + st.info("No task completion data available.") + + if state.scenario is not None: summary_window() + if st.toggle("show analyse_task_competition"): + analyze_task_completion() # R&D Loops Window if isinstance(state.scenario, SIMILAR_SCENARIOS): diff --git a/rdagent/scenarios/data_mining/developer/feedback.py b/rdagent/scenarios/data_mining/developer/feedback.py index e1ca2927..f8012a37 100644 --- a/rdagent/scenarios/data_mining/developer/feedback.py +++ b/rdagent/scenarios/data_mining/developer/feedback.py @@ -5,21 +5,13 @@ import json from pathlib import Path from typing import Dict -from jinja2 import Environment, StrictUndefined - from rdagent.core.experiment import Experiment -from rdagent.core.prompts import Prompts -from rdagent.core.proposal import ( - Experiment2Feedback, - Hypothesis, - HypothesisFeedback, - Trace, -) +from rdagent.core.proposal import Experiment2Feedback, HypothesisFeedback, Trace from rdagent.log import rdagent_logger as logger from rdagent.oai.llm_utils import APIBackend from rdagent.utils import convert2bool +from rdagent.utils.agent.tpl import T -feedback_prompts = Prompts(file_path=Path(__file__).parent.parent.parent / "qlib" / "prompts.yaml") DIRNAME = Path(__file__).absolute().resolve().parent @@ -35,24 +27,20 @@ class DMModelExperiment2Feedback(Experiment2Feedback): logger.info("Generating feedback...") # Define the system prompt for hypothesis feedback - system_prompt = feedback_prompts["model_feedback_generation"]["system"] + system_prompt = T("scenarios.qlib.prompts:model_feedback_generation.system").r() # Define the user prompt for hypothesis feedback context = trace.scen SOTA_hypothesis, SOTA_experiment = trace.get_sota_hypothesis_and_experiment() - user_prompt = ( - Environment(undefined=StrictUndefined) - .from_string(feedback_prompts["model_feedback_generation"]["user"]) - .render( - context=context, - last_hypothesis=SOTA_hypothesis, - last_task=SOTA_experiment.sub_tasks[0].get_task_information() if SOTA_hypothesis else None, - last_code=SOTA_experiment.sub_workspace_list[0].file_dict.get("model.py") if SOTA_hypothesis else None, - last_result=SOTA_experiment.result if SOTA_hypothesis else None, - hypothesis=hypothesis, - exp=exp, - ) + user_prompt = T("scenarios.qlib.prompts:model_feedback_generation.user").r( + context=context, + last_hypothesis=SOTA_hypothesis, + last_task=SOTA_experiment.sub_tasks[0].get_task_information() if SOTA_hypothesis else None, + last_code=SOTA_experiment.sub_workspace_list[0].file_dict.get("model.py") if SOTA_hypothesis else None, + last_result=SOTA_experiment.result if SOTA_hypothesis else None, + hypothesis=hypothesis, + exp=exp, ) # Call the APIBackend to generate the response for hypothesis feedback diff --git a/rdagent/scenarios/data_mining/developer/model_runner.py b/rdagent/scenarios/data_mining/developer/model_runner.py index 5beedcef..f9238323 100644 --- a/rdagent/scenarios/data_mining/developer/model_runner.py +++ b/rdagent/scenarios/data_mining/developer/model_runner.py @@ -1,6 +1,7 @@ from rdagent.components.runner import CachedRunner from rdagent.core.exception import ModelEmptyError from rdagent.core.utils import cache_with_pickle +from rdagent.log import rdagent_logger as logger from rdagent.scenarios.data_mining.experiment.model_experiment import DMModelExperiment @@ -14,7 +15,11 @@ class DMModelRunner(CachedRunner[DMModelExperiment]): env_to_use = {"PYTHONPATH": "./"} - result = exp.experiment_workspace.execute(run_env=env_to_use) + result, stdout = exp.experiment_workspace.execute(run_env=env_to_use) + + if result is None: + logger.error(f"Experiment failed to run, stdout: {stdout}") + raise ModelEmptyError(f"Failed to run this experiment, because {stdout}") exp.result = result diff --git a/rdagent/scenarios/data_mining/experiment/model_experiment.py b/rdagent/scenarios/data_mining/experiment/model_experiment.py index 1068d2a5..63ab3dfa 100644 --- a/rdagent/scenarios/data_mining/experiment/model_experiment.py +++ b/rdagent/scenarios/data_mining/experiment/model_experiment.py @@ -6,11 +6,9 @@ from rdagent.components.coder.model_coder.model import ( ModelTask, ) from rdagent.core.experiment import Task -from rdagent.core.prompts import Prompts from rdagent.core.scenario import Scenario from rdagent.scenarios.data_mining.experiment.workspace import DMFBWorkspace - -prompt_dict = Prompts(file_path=Path(__file__).parent / "prompts.yaml") +from rdagent.utils.agent.tpl import T class DMModelExperiment(ModelExperiment[ModelTask, DMFBWorkspace, ModelFBWorkspace]): @@ -22,7 +20,7 @@ class DMModelExperiment(ModelExperiment[ModelTask, DMFBWorkspace, ModelFBWorkspa class DMModelScenario(Scenario): @property def background(self) -> str: - return prompt_dict["dm_model_background"] + return T(".prompts:dm_model_background").r() @property def source_data(self) -> str: @@ -30,15 +28,15 @@ class DMModelScenario(Scenario): @property def output_format(self) -> str: - return prompt_dict["dm_model_output_format"] + return T(".prompts:dm_model_output_format").r() @property def interface(self) -> str: - return prompt_dict["dm_model_interface"] + return T(".prompts:dm_model_interface").r() @property def simulator(self) -> str: - return prompt_dict["dm_model_simulator"] + return T(".prompts:dm_model_simulator").r() @property def rich_style_description(self) -> str: diff --git a/rdagent/scenarios/data_mining/proposal/model_proposal.py b/rdagent/scenarios/data_mining/proposal/model_proposal.py index a2a6354d..2b3df085 100644 --- a/rdagent/scenarios/data_mining/proposal/model_proposal.py +++ b/rdagent/scenarios/data_mining/proposal/model_proposal.py @@ -1,20 +1,15 @@ import json -from pathlib import Path from typing import List, Tuple -from jinja2 import Environment, StrictUndefined - from rdagent.components.coder.model_coder.model import ModelExperiment, ModelTask from rdagent.components.proposal import ( Hypothesis, ModelHypothesis2Experiment, ModelHypothesisGen, ) -from rdagent.core.prompts import Prompts from rdagent.core.proposal import Hypothesis, Scenario, Trace from rdagent.scenarios.data_mining.experiment.model_experiment import DMModelExperiment - -prompt_dict = Prompts(file_path=Path(__file__).parent.parent.parent / "qlib" / "prompts.yaml") +from rdagent.utils.agent.tpl import T DMModelHypothesis = Hypothesis @@ -36,19 +31,27 @@ class DMModelHypothesisGen(ModelHypothesisGen): def prepare_context(self, trace: Trace) -> Tuple[dict, bool]: hypothesis_and_feedback = ( - ( - Environment(undefined=StrictUndefined) - .from_string(prompt_dict["hypothesis_and_feedback"]) - .render(trace=trace) + T("scenarios.qlib.prompts:hypothesis_and_feedback").r( + trace=trace, ) if len(trace.hist) > 0 else "No previous hypothesis and feedback available since it's the first round." ) + + 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": None, - "hypothesis_output_format": prompt_dict["hypothesis_output_format"], - "hypothesis_specification": prompt_dict["model_hypothesis_specification"], + "hypothesis_output_format": T("scenarios.qlib.prompts:hypothesis_output_format").r(), + "hypothesis_specification": T("scenarios.qlib.prompts:model_hypothesis_specification").r(), } return context_dict, True @@ -68,13 +71,11 @@ class DMModelHypothesisGen(ModelHypothesisGen): class DMModelHypothesis2Experiment(ModelHypothesis2Experiment): def prepare_context(self, hypothesis: Hypothesis, trace: Trace) -> Tuple[dict, bool]: scenario = trace.scen.get_scenario_all_desc() - experiment_output_format = prompt_dict["model_experiment_output_format"] + experiment_output_format = T("scenarios.qlib.prompts:model_experiment_output_format").r() hypothesis_and_feedback = ( - ( - Environment(undefined=StrictUndefined) - .from_string(prompt_dict["hypothesis_and_feedback"]) - .render(trace=trace) + T("scenarios.qlib.prompts:hypothesis_and_feedback").r( + trace=trace, ) if len(trace.hist) > 0 else "No previous hypothesis and feedback available since it's the first round." diff --git a/rdagent/scenarios/general_model/scenario.py b/rdagent/scenarios/general_model/scenario.py index 5455a64a..09fb4a59 100644 --- a/rdagent/scenarios/general_model/scenario.py +++ b/rdagent/scenarios/general_model/scenario.py @@ -1,21 +1,18 @@ from copy import deepcopy -from pathlib import Path from rdagent.core.experiment import Task -from rdagent.core.prompts import Prompts from rdagent.core.scenario import Scenario - -prompt_dict = Prompts(file_path=Path(__file__).parent / "prompts.yaml") +from rdagent.utils.agent.tpl import T class GeneralModelScenario(Scenario): def __init__(self) -> None: super().__init__() - self._background = deepcopy(prompt_dict["general_model_background"]) - self._output_format = deepcopy(prompt_dict["general_model_output_format"]) - self._interface = deepcopy(prompt_dict["general_model_interface"]) - self._simulator = deepcopy(prompt_dict["general_model_simulator"]) - self._rich_style_description = deepcopy(prompt_dict["general_model_rich_style_description"]) + self._background = deepcopy(T(".prompts:general_model_background").r()) + self._output_format = deepcopy(T(".prompts:general_model_output_format").r()) + self._interface = deepcopy(T(".prompts:general_model_interface").r()) + self._simulator = deepcopy(T(".prompts:general_model_simulator").r()) + self._rich_style_description = deepcopy(T(".prompts:general_model_rich_style_description").r()) @property def background(self) -> str: diff --git a/rdagent/scenarios/kaggle/developer/coder.py b/rdagent/scenarios/kaggle/developer/coder.py index dd1b4475..5547def7 100644 --- a/rdagent/scenarios/kaggle/developer/coder.py +++ b/rdagent/scenarios/kaggle/developer/coder.py @@ -1,5 +1,4 @@ import json -from pathlib import Path from typing import Dict, List from jinja2 import Environment, StrictUndefined @@ -7,7 +6,6 @@ from jinja2 import Environment, StrictUndefined from rdagent.components.coder.factor_coder import FactorCoSTEER from rdagent.components.coder.model_coder import ModelCoSTEER from rdagent.core.developer import Developer -from rdagent.core.prompts import Prompts from rdagent.oai.llm_utils import APIBackend from rdagent.scenarios.kaggle.experiment.kaggle_experiment import ( KG_SELECT_MAPPING, @@ -16,8 +14,7 @@ from rdagent.scenarios.kaggle.experiment.kaggle_experiment import ( KGModelCoSTEER = ModelCoSTEER KGFactorCoSTEER = FactorCoSTEER - -prompt_dict = Prompts(file_path=Path(__file__).parent.parent / "prompts.yaml") +from rdagent.utils.agent.tpl import T DEFAULT_SELECTION_CODE = """ import pandas as pd @@ -46,15 +43,12 @@ class KGModelFeatureSelectionCoder(Developer[KGModelExperiment]): .render(feature_index_list=None) ) else: - system_prompt = ( - Environment(undefined=StrictUndefined) - .from_string(prompt_dict["model_feature_selection"]["system"]) - .render(scenario=self.scen.get_scenario_all_desc(), model_type=exp.sub_tasks[0].model_type) + system_prompt = T("scenarios.kaggle.prompts:model_feature_selection.system").r( + scenario=exp.scen.get_scenario_all_desc(), + model_type=exp.sub_tasks[0].model_type, ) - user_prompt = ( - Environment(undefined=StrictUndefined) - .from_string(prompt_dict["model_feature_selection"]["user"]) - .render(feature_groups=[desc[0] for desc in exp.experiment_workspace.data_description]) + user_prompt = T("scenarios.kaggle.prompts:model_feature_selection.user").r( + feature_groups=[desc[0] for desc in exp.experiment_workspace.data_description] ) chosen_index = json.loads( diff --git a/rdagent/scenarios/kaggle/developer/feedback.py b/rdagent/scenarios/kaggle/developer/feedback.py index d023ef91..e6c2e39e 100644 --- a/rdagent/scenarios/kaggle/developer/feedback.py +++ b/rdagent/scenarios/kaggle/developer/feedback.py @@ -1,26 +1,16 @@ import json -from pathlib import Path from typing import Dict import pandas as pd -from jinja2 import Environment, StrictUndefined from rdagent.components.knowledge_management.graph import UndirectedNode from rdagent.core.experiment import Experiment -from rdagent.core.prompts import Prompts -from rdagent.core.proposal import ( - Experiment2Feedback, - Hypothesis, - HypothesisFeedback, - Trace, -) +from rdagent.core.proposal import Experiment2Feedback, HypothesisFeedback, Trace from rdagent.log import rdagent_logger as logger from rdagent.oai.llm_utils import APIBackend from rdagent.scenarios.kaggle.experiment.kaggle_experiment import KG_SELECT_MAPPING from rdagent.utils import convert2bool - -prompt_dict = Prompts(file_path=Path(__file__).parent.parent / "prompts.yaml") -DIRNAME = Path(__file__).absolute().resolve().parent +from rdagent.utils.agent.tpl import T class KGExperiment2Feedback(Experiment2Feedback): @@ -87,10 +77,8 @@ class KGExperiment2Feedback(Experiment2Feedback): prompt_key = "factor_feedback_generation" # Generate the system prompt - sys_prompt = ( - Environment(undefined=StrictUndefined) - .from_string(prompt_dict[prompt_key]["system"]) - .render(scenario=self.scen.get_scenario_all_desc(filtered_tag="feedback")) + sys_prompt = T(f"scenarios.kaggle.prompts:{prompt_key}.system").r( + scenario=self.scen.get_scenario_all_desc(filtered_tag="feedback") ) sota_exp = exp.based_experiments[-1] if exp.based_experiments else None @@ -139,11 +127,7 @@ class KGExperiment2Feedback(Experiment2Feedback): "last_hypothesis_and_feedback": last_hypothesis_and_feedback, } - usr_prompt = ( - Environment(undefined=StrictUndefined) - .from_string(prompt_dict["kg_feedback_generation_user"]) - .render(**render_dict) - ) + usr_prompt = T(f"scenarios.kaggle.prompts:kg_feedback_generation_user").r(**render_dict) response = APIBackend().build_messages_and_create_chat_completion( user_prompt=usr_prompt, diff --git a/rdagent/scenarios/kaggle/developer/runner.py b/rdagent/scenarios/kaggle/developer/runner.py index 9d407e02..494b66ff 100644 --- a/rdagent/scenarios/kaggle/developer/runner.py +++ b/rdagent/scenarios/kaggle/developer/runner.py @@ -1,5 +1,3 @@ -import json -import pickle import shutil from pathlib import Path @@ -8,7 +6,6 @@ import pandas as pd from rdagent.components.runner import CachedRunner from rdagent.core.exception import CoderError, FactorEmptyError, ModelEmptyError from rdagent.core.experiment import ASpecificExp, Experiment -from rdagent.core.prompts import Prompts from rdagent.core.utils import cache_with_pickle from rdagent.oai.llm_utils import md5_hash from rdagent.scenarios.kaggle.experiment.kaggle_experiment import ( @@ -16,8 +13,6 @@ from rdagent.scenarios.kaggle.experiment.kaggle_experiment import ( KGModelExperiment, ) -prompt_dict = Prompts(file_path=Path(__file__).parent.parent / "prompts.yaml") - class KGCachedRunner(CachedRunner[ASpecificExp]): def get_cache_key(self, exp: ASpecificExp) -> str: diff --git a/rdagent/scenarios/kaggle/experiment/scenario.py b/rdagent/scenarios/kaggle/experiment/scenario.py index 2c8ee1f5..acf0ad8b 100644 --- a/rdagent/scenarios/kaggle/experiment/scenario.py +++ b/rdagent/scenarios/kaggle/experiment/scenario.py @@ -6,11 +6,9 @@ from pathlib import Path from typing import Dict import pandas as pd -from jinja2 import Environment, StrictUndefined from rdagent.app.kaggle.conf import KAGGLE_IMPLEMENT_SETTING from rdagent.core.experiment import Task -from rdagent.core.prompts import Prompts from rdagent.core.scenario import Scenario from rdagent.oai.llm_utils import APIBackend from rdagent.scenarios.kaggle.experiment.kaggle_experiment import KGFactorExperiment @@ -21,8 +19,7 @@ from rdagent.scenarios.kaggle.kaggle_crawler import ( from rdagent.scenarios.kaggle.knowledge_management.vector_base import ( KaggleExperienceBase, ) - -prompt_dict = Prompts(file_path=Path(__file__).parent / "prompts.yaml") +from rdagent.utils.agent.tpl import T KG_ACTION_FEATURE_PROCESSING = "Feature processing" KG_ACTION_FEATURE_ENGINEERING = "Feature engineering" @@ -74,20 +71,11 @@ class KGScenario(Scenario): self.initial_performance = 0.0 def _analysis_competition_description(self): - sys_prompt = ( - Environment(undefined=StrictUndefined) - .from_string(prompt_dict["kg_description_template"]["system"]) - .render() - ) - - user_prompt = ( - Environment(undefined=StrictUndefined) - .from_string(prompt_dict["kg_description_template"]["user"]) - .render( - competition_descriptions=self.competition_descriptions, - raw_data_information=self.source_data, - evaluation_metric_direction=self.evaluation_metric_direction, - ) + sys_prompt = T(".prompts:kg_description_template.system").r() + user_prompt = T(".prompts:kg_description_template.user").r( + competition_descriptions=self.competition_descriptions, + raw_data_information=self.source_data, + evaluation_metric_direction=self.evaluation_metric_direction, ) response_analysis = APIBackend().build_messages_and_create_chat_completion( @@ -124,26 +112,22 @@ class KGScenario(Scenario): @property def background(self) -> str: - background_template = prompt_dict["kg_background"] train_script = ( Path(__file__).parent / "templates" / KAGGLE_IMPLEMENT_SETTING.competition / "train.py" ).read_text() - background_prompt = ( - Environment(undefined=StrictUndefined) - .from_string(background_template) - .render( - train_script=train_script, - competition_type=self.competition_type, - competition_description=self.competition_description, - target_description=self.target_description, - competition_features=self.competition_features, - submission_specifications=self.submission_specifications, - evaluation_desc=self.evaluation_desc, - evaluate_bool=self.evaluation_metric_direction, - ) + background_prompt = T(".prompts:kg_background").r( + train_script=train_script, + competition_type=self.competition_type, + competition_description=self.competition_description, + target_description=self.target_description, + competition_features=self.competition_features, + submission_specifications=self.submission_specifications, + evaluation_desc=self.evaluation_desc, + evaluate_bool=self.evaluation_metric_direction, ) + return background_prompt @property @@ -183,12 +167,11 @@ class KGScenario(Scenario): def output_format(self, tag=None) -> str: assert tag in [None, "feature", "model"] feature_output_format = f"""The feature code should output following the format: -{prompt_dict['kg_feature_output_format']}""" - model_output_format = f"""The model code should output following the format:\n""" + ( - Environment(undefined=StrictUndefined) - .from_string(prompt_dict["kg_model_output_format"]) - .render(channel=self.model_output_channel) - ) +{T(".prompts:kg_feature_output_format").r()}""" + model_output_format = f"""The model code should output following the format:\n""" + T( + ".prompts:kg_model_output_format" + ).r(channel=self.model_output_channel) + if tag is None: return feature_output_format + "\n" + model_output_format elif tag == "feature": @@ -199,12 +182,12 @@ class KGScenario(Scenario): def interface(self, tag=None) -> str: assert tag in [None, "feature", "XGBoost", "RandomForest", "LightGBM", "NN"] feature_interface = f"""The feature code should follow the interface: -{prompt_dict['kg_feature_interface']}""" +{T(".prompts:kg_feature_interface").r()}""" if tag == "feature": return feature_interface - model_interface = "The model code should follow the interface:\n" + ( - Environment(undefined=StrictUndefined).from_string(prompt_dict["kg_model_interface"]).render(tag=tag) + model_interface = "The model code should follow the interface:\n" + T(".prompts:kg_model_interface").r( + tag=tag, ) if tag is None: return feature_interface + "\n" + model_interface @@ -213,13 +196,14 @@ class KGScenario(Scenario): def simulator(self, tag=None) -> str: assert tag in [None, "feature", "model"] - kg_feature_simulator = "The feature code will be sent to the simulator:\n" + prompt_dict["kg_feature_simulator"] - kg_model_simulator = "The model code will be sent to the simulator:\n" + ( - Environment(undefined=StrictUndefined) - .from_string(prompt_dict["kg_model_simulator"]) - .render(submission_specifications=self.submission_specifications) + kg_feature_simulator = ( + "The feature code will be sent to the simulator:\n" + T(".prompts:kg_feature_simulator").r() ) + kg_model_simulator = "The model code will be sent to the simulator:\n" + T(".prompts:kg_model_simulator").r( + submission_specifications=self.submission_specifications, + ) + if tag is None: return kg_feature_simulator + "\n" + kg_model_simulator elif tag == "feature": diff --git a/rdagent/scenarios/kaggle/kaggle_crawler.py b/rdagent/scenarios/kaggle/kaggle_crawler.py index 277f7bce..7a7a5adb 100644 --- a/rdagent/scenarios/kaggle/kaggle_crawler.py +++ b/rdagent/scenarios/kaggle/kaggle_crawler.py @@ -9,7 +9,6 @@ from itertools import chain from pathlib import Path import nbformat -from jinja2 import Environment, StrictUndefined from rich import print from selenium import webdriver from selenium.webdriver.chrome.service import Service diff --git a/rdagent/scenarios/kaggle/knowledge_management/extract_knowledge.py b/rdagent/scenarios/kaggle/knowledge_management/extract_knowledge.py index d09d6d15..746839b7 100644 --- a/rdagent/scenarios/kaggle/knowledge_management/extract_knowledge.py +++ b/rdagent/scenarios/kaggle/knowledge_management/extract_knowledge.py @@ -1,27 +1,13 @@ import json -import os from pathlib import Path -from jinja2 import Environment, StrictUndefined - -from rdagent.core.prompts import Prompts from rdagent.oai.llm_utils import APIBackend - -prompt_dict = Prompts(file_path=Path(__file__).parent / "prompts.yaml") +from rdagent.utils.agent.tpl import T def extract_knowledge_from_high_score_answers(content: str): - sys_prompt = ( - Environment(undefined=StrictUndefined) - .from_string(prompt_dict["extract_kaggle_knowledge_prompts"]["system"]) - .render() - ) - - user_prompt = ( - Environment(undefined=StrictUndefined) - .from_string(prompt_dict["extract_kaggle_knowledge_prompts"]["user"]) - .render(file_content=content) - ) + sys_prompt = T(".prompts:extract_kaggle_knowledge_prompts.system").r() + user_prompt = T(".prompts:extract_kaggle_knowledge_prompts.user").r(file_content=content) response_analysis = APIBackend().build_messages_and_create_chat_completion( user_prompt=user_prompt, @@ -41,16 +27,9 @@ def extract_knowledge_from_feedback(feedback_response: dict) -> dict: """ Extracts knowledge from LLM-generated feedback and structures it. """ - sys_prompt = ( - Environment(undefined=StrictUndefined) - .from_string(prompt_dict["extract_kaggle_knowledge_from_feedback_prompts"]["system"]) - .render() - ) - - user_prompt = ( - Environment(undefined=StrictUndefined) - .from_string(prompt_dict["extract_kaggle_knowledge_from_feedback_prompts"]["user"]) - .render(experiment_strategy=feedback_response) + sys_prompt = T(".prompts:extract_kaggle_knowledge_from_feedback_prompts.system").r() + user_prompt = T(".prompts:extract_kaggle_knowledge_from_feedback_prompts.user").r( + experiment_strategy=feedback_response ) response_analysis = APIBackend().build_messages_and_create_chat_completion( diff --git a/rdagent/scenarios/kaggle/knowledge_management/graph.py b/rdagent/scenarios/kaggle/knowledge_management/graph.py index 9b002aca..483deea4 100644 --- a/rdagent/scenarios/kaggle/knowledge_management/graph.py +++ b/rdagent/scenarios/kaggle/knowledge_management/graph.py @@ -3,7 +3,6 @@ from datetime import datetime, timezone from pathlib import Path from typing import List -from jinja2 import Environment, StrictUndefined from tqdm import tqdm from rdagent.app.kaggle.conf import KAGGLE_IMPLEMENT_SETTING @@ -12,12 +11,10 @@ from rdagent.components.knowledge_management.graph import ( UndirectedNode, ) from rdagent.core.conf import RD_AGENT_SETTINGS -from rdagent.core.prompts import Prompts from rdagent.core.utils import multiprocessing_wrapper from rdagent.oai.llm_utils import APIBackend from rdagent.scenarios.kaggle.experiment.scenario import KGScenario - -PROMPT_DICT = Prompts(file_path=Path(__file__).parent / "prompts.yaml") +from rdagent.utils.agent.tpl import T class KGKnowledgeGraph(UndirectedGraph): @@ -42,19 +39,15 @@ class KGKnowledgeGraph(UndirectedGraph): self.dump() # Each valid experiment will overwrite this file once again. def analyze_one_document(self, document_content: str, scenario: KGScenario | None) -> list: - session_system_prompt = ( - Environment(undefined=StrictUndefined) - .from_string(PROMPT_DICT["extract_knowledge_graph_from_document"]["system"]) - .render(scenario=scenario.get_scenario_all_desc() if scenario is not None else "") + session_system_prompt = T(".prompts:extract_knowledge_graph_from_document.system").r( + scenario=scenario.get_scenario_all_desc() if scenario is not None else "" ) session = APIBackend().build_chat_session( session_system_prompt=session_system_prompt, ) - user_prompt = ( - Environment(undefined=StrictUndefined) - .from_string(PROMPT_DICT["extract_knowledge_graph_from_document"]["user"]) - .render(document_content=document_content) + user_prompt = T(".prompts:extract_knowledge_graph_from_document.user").r( + document_content=document_content, ) knowledge_list = [] for _ in range(10): diff --git a/rdagent/scenarios/kaggle/knowledge_management/vector_base.py b/rdagent/scenarios/kaggle/knowledge_management/vector_base.py index a07fd37a..e32bd4db 100644 --- a/rdagent/scenarios/kaggle/knowledge_management/vector_base.py +++ b/rdagent/scenarios/kaggle/knowledge_management/vector_base.py @@ -1,18 +1,16 @@ import json -from datetime import datetime, timezone from pathlib import Path from typing import List, Union import pandas as pd -from jinja2 import Environment, StrictUndefined from rdagent.components.knowledge_management.vector_base import Document, PDVectorBase -from rdagent.core.prompts import Prompts from rdagent.log import rdagent_logger as logger from rdagent.oai.llm_utils import APIBackend from rdagent.scenarios.kaggle.knowledge_management.extract_knowledge import ( extract_knowledge_from_feedback, ) +from rdagent.utils.agent.tpl import T class KGKnowledgeDocument(Document): @@ -270,17 +268,8 @@ class KaggleExperienceBase(PDVectorBase): return kaggle_docs, similarities def refine_with_LLM(self, target: str, text: str) -> str: - prompt_dict = Prompts(file_path=Path(__file__).parent / "prompts.yaml") - - sys_prompt = ( - Environment(undefined=StrictUndefined).from_string(prompt_dict["refine_with_LLM"]["system"]).render() - ) - - user_prompt = ( - Environment(undefined=StrictUndefined) - .from_string(prompt_dict["refine_with_LLM"]["user"]) - .render(target=target, text=text) - ) + sys_prompt = T(".prompts:refine_with_LLM.system").r() + user_prompt = T(".prompts:refine_with_LLM.user").r(target=target, text=text) response = APIBackend().build_messages_and_create_chat_completion( user_prompt=user_prompt, diff --git a/rdagent/scenarios/kaggle/proposal/proposal.py b/rdagent/scenarios/kaggle/proposal/proposal.py index 63636ee5..551acfa4 100644 --- a/rdagent/scenarios/kaggle/proposal/proposal.py +++ b/rdagent/scenarios/kaggle/proposal/proposal.py @@ -1,20 +1,14 @@ import json import math -from pathlib import Path from typing import List, Tuple -from jinja2 import Environment, StrictUndefined - -from rdagent.app.kaggle.conf import KAGGLE_IMPLEMENT_SETTING from rdagent.components.coder.factor_coder.factor import FactorTask from rdagent.components.coder.model_coder.model import ModelExperiment, ModelTask -from rdagent.components.knowledge_management.vector_base import VectorBase from rdagent.components.proposal import ( FactorAndModelHypothesis2Experiment, FactorAndModelHypothesisGen, ) from rdagent.core.exception import ModelEmptyError -from rdagent.core.prompts import Prompts from rdagent.core.proposal import Hypothesis, Scenario, Trace from rdagent.scenarios.kaggle.experiment.kaggle_experiment import ( KG_MODEL_MAPPING, @@ -22,22 +16,16 @@ from rdagent.scenarios.kaggle.experiment.kaggle_experiment import ( KGFactorExperiment, KGModelExperiment, ) -from rdagent.scenarios.kaggle.experiment.scenario import KGScenario -from rdagent.scenarios.kaggle.knowledge_management.graph import KGKnowledgeGraph -from rdagent.scenarios.kaggle.knowledge_management.vector_base import ( - KaggleExperienceBase, -) - -prompt_dict = Prompts(file_path=Path(__file__).parent.parent / "prompts.yaml") - - from rdagent.scenarios.kaggle.experiment.scenario import ( KG_ACTION_FEATURE_ENGINEERING, KG_ACTION_FEATURE_PROCESSING, KG_ACTION_LIST, KG_ACTION_MODEL_FEATURE_SELECTION, KG_ACTION_MODEL_TUNING, + KGScenario, ) +from rdagent.scenarios.kaggle.knowledge_management.graph import KGKnowledgeGraph +from rdagent.utils.agent.tpl import T class KGHypothesis(Hypothesis): @@ -187,10 +175,9 @@ def generate_RAG_content( insight["conclusion"] = "No conclusion information available." insights.append(insight) - RAG_content = ( - Environment(undefined=StrictUndefined) - .from_string(prompt_dict["KG_hypothesis_gen_RAG"]) - .render(insights=insights, experiences=experiences) + RAG_content = T("scenarios.kaggle.prompts:KG_hypothesis_gen_RAG").r( + insights=insights, + experiences=experiences, ) return RAG_content @@ -262,10 +249,8 @@ class KGHypothesisGen(FactorAndModelHypothesisGen): def prepare_context(self, trace: Trace) -> Tuple[dict, bool]: hypothesis_and_feedback = ( - ( - Environment(undefined=StrictUndefined) - .from_string(prompt_dict["hypothesis_and_feedback"]) - .render(trace=trace) + T("scenarios.kaggle.prompts:hypothesis_and_feedback").r( + trace=trace, ) if len(trace.hist) > 0 else "No previous hypothesis and feedback available since it's the first round." @@ -287,7 +272,7 @@ class KGHypothesisGen(FactorAndModelHypothesisGen): "\n\nNext experiment action is " + action + "\nspecification: " - + prompt_dict["hypothesis_specification"][action] + + T(f"scenarios.kaggle.prompts:hypothesis_specification.{action}").r() ) context_dict = { @@ -298,7 +283,7 @@ class KGHypothesisGen(FactorAndModelHypothesisGen): hypothesis_and_feedback=hypothesis_and_feedback, target=action if self.scen.if_action_choosing_based_on_UCB else None, ), - "hypothesis_output_format": prompt_dict["hypothesis_output_format"], + "hypothesis_output_format": T("scenarios.kaggle.prompts:hypothesis_output_format").r(), "hypothesis_specification": hypothesis_specification, } return context_dict, True @@ -324,17 +309,15 @@ class KGHypothesis2Experiment(FactorAndModelHypothesis2Experiment): scenario = trace.scen.get_scenario_all_desc(filtered_tag="hypothesis_and_experiment") assert isinstance(hypothesis, KGHypothesis) experiment_output_format = ( - prompt_dict["feature_experiment_output_format"] + T("scenarios.kaggle.prompts:feature_experiment_output_format").r() if hypothesis.action in [KG_ACTION_FEATURE_ENGINEERING, KG_ACTION_FEATURE_PROCESSING] - else prompt_dict["model_experiment_output_format"] + else T("scenarios.kaggle.prompts:model_experiment_output_format").r() ) self.current_action = hypothesis.action hypothesis_and_feedback = ( - ( - Environment(undefined=StrictUndefined) - .from_string(prompt_dict["hypothesis_and_feedback"]) - .render(trace=trace) + T("scenarios.kaggle.prompts:hypothesis_and_feedback").r( + trace=trace, ) if len(trace.hist) > 0 else "No previous hypothesis and feedback available since it's the first round." diff --git a/rdagent/scenarios/qlib/developer/factor_runner.py b/rdagent/scenarios/qlib/developer/factor_runner.py index 466bce24..10028c80 100644 --- a/rdagent/scenarios/qlib/developer/factor_runner.py +++ b/rdagent/scenarios/qlib/developer/factor_runner.py @@ -1,20 +1,19 @@ -import pickle from pathlib import Path -from typing import List import pandas as pd from pandarallel import pandarallel -from rdagent.components.coder.CoSTEER.evaluators import CoSTEERMultiFeedback from rdagent.core.conf import RD_AGENT_SETTINGS -from rdagent.core.utils import cache_with_pickle, multiprocessing_wrapper +from rdagent.core.utils import cache_with_pickle pandarallel.initialize(verbose=1) from rdagent.components.runner import CachedRunner from rdagent.core.exception import FactorEmptyError from rdagent.log import rdagent_logger as logger +from rdagent.scenarios.qlib.developer.utils import process_factor_data from rdagent.scenarios.qlib.experiment.factor_experiment import QlibFactorExperiment +from rdagent.scenarios.qlib.experiment.model_experiment import QlibModelExperiment DIRNAME = Path(__file__).absolute().resolve().parent DIRNAME_local = Path.cwd() @@ -78,24 +77,33 @@ class QlibFactorRunner(CachedRunner[QlibFactorExperiment]): then passing the combined data to Docker for backtest results. """ if exp.based_experiments and exp.based_experiments[-1].result is None: + logger.info(f"Baseline experiment execution ...") exp.based_experiments[-1] = self.develop(exp.based_experiments[-1]) if exp.based_experiments: SOTA_factor = None - if len(exp.based_experiments) > 1: - SOTA_factor = self.process_factor_data(exp.based_experiments) + # Filter and retain only QlibFactorExperiment instances + sota_factor_experiments_list = [ + base_exp for base_exp in exp.based_experiments if isinstance(base_exp, QlibFactorExperiment) + ] + if len(sota_factor_experiments_list) > 1: + logger.info(f"SOTA factor processing ...") + SOTA_factor = process_factor_data(sota_factor_experiments_list) + logger.info(f"New factor processing ...") # Process the new factors data - new_factors = self.process_factor_data(exp) + new_factors = process_factor_data(exp) if new_factors.empty: - raise FactorEmptyError("No valid factor data found to merge.") + raise FactorEmptyError("Factors failed to run on the full sample, this round of experiment failed.") # Combine the SOTA factor and new factors if SOTA factor exists if SOTA_factor is not None and not SOTA_factor.empty: new_factors = self.deduplicate_new_factors(SOTA_factor, new_factors) if new_factors.empty: - raise FactorEmptyError("No valid factor data found to merge.") + raise FactorEmptyError( + "The factors generated in this round are highly similar to the previous factors. Please change the direction for creating new factors." + ) combined_factors = pd.concat([SOTA_factor, new_factors], axis=1).dropna() else: combined_factors = new_factors @@ -105,6 +113,9 @@ class QlibFactorRunner(CachedRunner[QlibFactorExperiment]): combined_factors = combined_factors.loc[:, ~combined_factors.columns.duplicated(keep="last")] new_columns = pd.MultiIndex.from_product([["feature"], combined_factors.columns]) combined_factors.columns = new_columns + num_features = RD_AGENT_SETTINGS.initial_fator_library_size + len(combined_factors.columns) + logger.info(f"Factor data processing completed.") + # Due to the rdagent and qlib docker image in the numpy version of the difference, # the `combined_factors_df.pkl` file could not be loaded correctly in qlib dokcer, # so we changed the file type of `combined_factors_df` from pkl to parquet. @@ -113,52 +124,58 @@ class QlibFactorRunner(CachedRunner[QlibFactorExperiment]): # Save the combined factors to the workspace combined_factors.to_parquet(target_path, engine="pyarrow") - result = exp.experiment_workspace.execute( - qlib_config_name=f"conf.yaml" if len(exp.based_experiments) == 0 else "conf_combined.yaml" - ) + # If model exp exists in the previous experiment + exist_sota_model_exp = False + for base_exp in reversed(exp.based_experiments): + if isinstance(base_exp, QlibModelExperiment): + sota_model_exp = base_exp + exist_sota_model_exp = True + break + logger.info(f"Experiment execution ...") + if exist_sota_model_exp: + exp.experiment_workspace.inject_files( + **{"model.py": sota_model_exp.sub_workspace_list[0].file_dict["model.py"]} + ) + env_to_use = {"PYTHONPATH": "./"} + sota_training_hyperparameters = sota_model_exp.sub_tasks[0].training_hyperparameters + if sota_training_hyperparameters: + env_to_use.update( + { + "n_epochs": str(sota_training_hyperparameters.get("n_epochs", "100")), + "lr": str(sota_training_hyperparameters.get("lr", "2e-4")), + "early_stop": str(sota_training_hyperparameters.get("early_stop", 10)), + "batch_size": str(sota_training_hyperparameters.get("batch_size", 256)), + "weight_decay": str(sota_training_hyperparameters.get("weight_decay", 0.0)), + } + ) + sota_model_type = sota_model_exp.sub_tasks[0].model_type + if sota_model_type == "TimeSeries": + env_to_use.update( + {"dataset_cls": "TSDatasetH", "num_features": num_features, "step_len": 20, "num_timesteps": 20} + ) + elif sota_model_type == "Tabular": + env_to_use.update({"dataset_cls": "DatasetH", "num_features": num_features}) + + # model + combined factors + result, stdout = exp.experiment_workspace.execute( + qlib_config_name="conf_model_combined.yaml", run_env=env_to_use + ) + else: + # LGBM + combined factors + result, stdout = exp.experiment_workspace.execute( + qlib_config_name=f"conf.yaml" if len(exp.based_experiments) == 0 else "conf_combined.yaml" + ) + else: + logger.info(f"Experiment execution ...") + result, stdout = exp.experiment_workspace.execute( + qlib_config_name=f"conf.yaml" if len(exp.based_experiments) == 0 else "conf_combined.yaml" + ) + + if result is None: + logger.error(f"Failed to run this experiment, because {stdout}") + raise FactorEmptyError(f"Failed to run this experiment, because {stdout}") exp.result = result + exp.stdout = stdout return exp - - def process_factor_data(self, exp_or_list: List[QlibFactorExperiment] | QlibFactorExperiment) -> pd.DataFrame: - """ - Process and combine factor data from experiment implementations. - - Args: - exp (ASpecificExp): The experiment containing factor data. - - Returns: - pd.DataFrame: Combined factor data without NaN values. - """ - if isinstance(exp_or_list, QlibFactorExperiment): - exp_or_list = [exp_or_list] - factor_dfs = [] - - # Collect all exp's dataframes - for exp in exp_or_list: - if len(exp.sub_tasks) > 0: - # if it has no sub_tasks, the experiment is results from template project. - # otherwise, it is developed with designed task. So it should have feedback. - assert isinstance(exp.prop_dev_feedback, CoSTEERMultiFeedback) - # Iterate over sub-implementations and execute them to get each factor data - message_and_df_list = multiprocessing_wrapper( - [ - (implementation.execute, ("All",)) - for implementation, fb in zip(exp.sub_workspace_list, exp.prop_dev_feedback) - if implementation and fb - ], # only execute successfully feedback - n=RD_AGENT_SETTINGS.multi_proc_n, - ) - for message, df in message_and_df_list: - # Check if factor generation was successful - if df is not None and "datetime" in df.index.names: - time_diff = df.index.get_level_values("datetime").to_series().diff().dropna().unique() - if pd.Timedelta(minutes=1) not in time_diff: - factor_dfs.append(df) - - # Combine all successful factor data - if factor_dfs: - return pd.concat(factor_dfs, axis=1) - else: - raise FactorEmptyError("No valid factor data found to merge.") diff --git a/rdagent/scenarios/qlib/developer/feedback.py b/rdagent/scenarios/qlib/developer/feedback.py index 73c6f84c..5fa68f26 100644 --- a/rdagent/scenarios/qlib/developer/feedback.py +++ b/rdagent/scenarios/qlib/developer/feedback.py @@ -3,23 +3,27 @@ from pathlib import Path from typing import Dict import pandas as pd -from jinja2 import Environment, StrictUndefined from rdagent.core.experiment import Experiment -from rdagent.core.prompts import Prompts from rdagent.core.proposal import ( Experiment2Feedback, - Hypothesis, HypothesisFeedback, Trace, ) from rdagent.log import rdagent_logger as logger from rdagent.oai.llm_utils import APIBackend +from rdagent.scenarios.qlib.experiment.quant_experiment import QlibQuantScenario from rdagent.utils import convert2bool +from rdagent.utils.agent.tpl import T -feedback_prompts = Prompts(file_path=Path(__file__).parent.parent / "prompts.yaml") DIRNAME = Path(__file__).absolute().resolve().parent +IMPORTANT_METRICS = [ + "IC", + "1day.excess_return_with_cost.annualized_return", + "1day.excess_return_with_cost.max_drawdown", +] + def process_results(current_result, sota_result): # Convert the results to dataframes @@ -37,24 +41,18 @@ def process_results(current_result, sota_result): # Combine the dataframes on the Metric index combined_df = pd.concat([current_df, sota_df], axis=1) - # Select important metrics for comparison - important_metrics = [ - "1day.excess_return_without_cost.max_drawdown", - "1day.excess_return_without_cost.information_ratio", - "1day.excess_return_without_cost.annualized_return", - "IC", - ] - # Filter the combined DataFrame to retain only the important metrics - filtered_combined_df = combined_df.loc[important_metrics] + filtered_combined_df = combined_df.loc[IMPORTANT_METRICS] - filtered_combined_df[ - "Bigger columns name (Didn't consider the direction of the metric, you should judge it by yourself that bigger is better or smaller is better)" - ] = filtered_combined_df.apply( - lambda row: "Current Result" if row["Current Result"] > row["SOTA Result"] else "SOTA Result", axis=1 - ) + def format_filtered_combined_df(filtered_combined_df: pd.DataFrame) -> str: + results = [] + for metric, row in filtered_combined_df.iterrows(): + current = row["Current Result"] + sota = row["SOTA Result"] + results.append(f"{metric} of Current Result is {current:.6f}, of SOTA Result is {sota:.6f}") + return "; ".join(results) - return filtered_combined_df.to_string() + return format_filtered_combined_df(filtered_combined_df) class QlibFactorExperiment2Feedback(Experiment2Feedback): @@ -81,21 +79,20 @@ class QlibFactorExperiment2Feedback(Experiment2Feedback): combined_result = process_results(current_result, sota_result) # Generate the system prompt - sys_prompt = ( - Environment(undefined=StrictUndefined) - .from_string(feedback_prompts["factor_feedback_generation"]["system"]) - .render(scenario=self.scen.get_scenario_all_desc()) - ) + if isinstance(self.scen, QlibQuantScenario): + sys_prompt = T("scenarios.qlib.prompts:factor_feedback_generation.system").r( + scenario=self.scen.get_scenario_all_desc(action="factor") + ) + else: + sys_prompt = T("scenarios.qlib.prompts:factor_feedback_generation.system").r( + scenario=self.scen.get_scenario_all_desc() + ) # Generate the user prompt - usr_prompt = ( - Environment(undefined=StrictUndefined) - .from_string(feedback_prompts["factor_feedback_generation"]["user"]) - .render( - hypothesis_text=hypothesis_text, - task_details=tasks_factors, - combined_result=combined_result, - ) + usr_prompt = T("scenarios.qlib.prompts:factor_feedback_generation.user").r( + hypothesis_text=hypothesis_text, + task_details=tasks_factors, + combined_result=combined_result, ) # Call the APIBackend to generate the response for hypothesis feedback @@ -126,40 +123,58 @@ class QlibFactorExperiment2Feedback(Experiment2Feedback): class QlibModelExperiment2Feedback(Experiment2Feedback): - """Generated feedbacks on the hypothesis from **Executed** Implementations of different tasks & their comparisons with previous performances""" - def generate_feedback(self, exp: Experiment, trace: Trace) -> HypothesisFeedback: """ - The `ti` should be executed and the results should be included, as well as the comparison between previous results (done by LLM). - For example: `mlflow` of Qlib will be included. + Generate feedback for the given experiment and hypothesis. + + Args: + exp (QlibModelExperiment): The experiment to generate feedback for. + hypothesis (QlibModelHypothesis): The hypothesis to generate feedback for. + trace (Trace): The trace of the experiment. + + Returns: + HypothesisFeedback: The feedback generated for the given experiment and hypothesis. """ hypothesis = exp.hypothesis logger.info("Generating feedback...") - # Define the system prompt for hypothesis feedback - system_prompt = feedback_prompts["model_feedback_generation"]["system"] - # Define the user prompt for hypothesis feedback - context = trace.scen - SOTA_hypothesis, SOTA_experiment = trace.get_sota_hypothesis_and_experiment() - - user_prompt = ( - Environment(undefined=StrictUndefined) - .from_string(feedback_prompts["model_feedback_generation"]["user"]) - .render( - context=context, - last_hypothesis=SOTA_hypothesis, - last_task=SOTA_experiment.sub_tasks[0].get_task_information() if SOTA_hypothesis else None, - last_code=SOTA_experiment.sub_workspace_list[0].file_dict.get("model.py") if SOTA_hypothesis else None, - last_result=SOTA_experiment.result if SOTA_hypothesis else None, - hypothesis=hypothesis, - exp=exp, + # Generate the system prompt + if isinstance(self.scen, QlibQuantScenario): + sys_prompt = T("scenarios.qlib.prompts:model_feedback_generation.system").r( + scenario=self.scen.get_scenario_all_desc(action="model") ) + else: + sys_prompt = T("scenarios.qlib.prompts:factor_feedback_generation.system").r( + scenario=self.scen.get_scenario_all_desc() + ) + + # Generate the user prompt + SOTA_hypothesis, SOTA_experiment = trace.get_sota_hypothesis_and_experiment() + user_prompt = T("scenarios.qlib.prompts:model_feedback_generation.user").r( + sota_hypothesis=SOTA_hypothesis, + sota_task=SOTA_experiment.sub_tasks[0].get_task_information() if SOTA_hypothesis else None, + sota_code=SOTA_experiment.sub_workspace_list[0].file_dict.get("model.py") if SOTA_hypothesis else None, + sota_result=SOTA_experiment.result.loc[IMPORTANT_METRICS] if SOTA_hypothesis else None, + hypothesis=hypothesis, + exp=exp, + exp_result=exp.result.loc[IMPORTANT_METRICS] if exp.result is not None else "execution failed", ) + # Call the APIBackend to generate the response for hypothesis feedback + response = APIBackend().build_messages_and_create_chat_completion( + user_prompt=user_prompt, + system_prompt=sys_prompt, + json_mode=True, + json_target_type=Dict[str, str | bool | int], + ) + + # Parse the JSON response to extract the feedback + response_json_hypothesis = json.loads(response) + # Call the APIBackend to generate the response for hypothesis feedback response_hypothesis = APIBackend().build_messages_and_create_chat_completion( user_prompt=user_prompt, - system_prompt=system_prompt, + system_prompt=sys_prompt, json_mode=True, json_target_type=Dict[str, str | bool | int], ) diff --git a/rdagent/scenarios/qlib/developer/model_runner.py b/rdagent/scenarios/qlib/developer/model_runner.py index 4a7196bf..4b83f449 100644 --- a/rdagent/scenarios/qlib/developer/model_runner.py +++ b/rdagent/scenarios/qlib/developer/model_runner.py @@ -1,6 +1,12 @@ +import pandas as pd + from rdagent.components.runner import CachedRunner +from rdagent.core.conf import RD_AGENT_SETTINGS from rdagent.core.exception import ModelEmptyError from rdagent.core.utils import cache_with_pickle +from rdagent.log import rdagent_logger as logger +from rdagent.scenarios.qlib.developer.utils import process_factor_data +from rdagent.scenarios.qlib.experiment.factor_experiment import QlibFactorExperiment from rdagent.scenarios.qlib.experiment.model_experiment import QlibModelExperiment @@ -19,6 +25,34 @@ class QlibModelRunner(CachedRunner[QlibModelExperiment]): @cache_with_pickle(CachedRunner.get_cache_key, CachedRunner.assign_cached_result) def develop(self, exp: QlibModelExperiment) -> QlibModelExperiment: + if exp.based_experiments and exp.based_experiments[-1].result is None: + exp.based_experiments[-1] = self.develop(exp.based_experiments[-1]) + + exist_sota_factor_exp = False + if exp.based_experiments: + SOTA_factor = None + # Filter and retain only QlibFactorExperiment instances + sota_factor_experiments_list = [ + base_exp for base_exp in exp.based_experiments if isinstance(base_exp, QlibFactorExperiment) + ] + if len(sota_factor_experiments_list) > 1: + logger.info(f"SOTA factor processing ...") + SOTA_factor = process_factor_data(sota_factor_experiments_list) + + if SOTA_factor is not None and not SOTA_factor.empty: + exist_sota_factor_exp = True + combined_factors = SOTA_factor + combined_factors = combined_factors.sort_index() + combined_factors = combined_factors.loc[:, ~combined_factors.columns.duplicated(keep="last")] + new_columns = pd.MultiIndex.from_product([["feature"], combined_factors.columns]) + combined_factors.columns = new_columns + num_features = str(RD_AGENT_SETTINGS.initial_fator_library_size + len(combined_factors.columns)) + + target_path = exp.experiment_workspace.workspace_path / "combined_factors_df.parquet" + + # Save the combined factors to the workspace + combined_factors.to_parquet(target_path, engine="pyarrow") + if exp.sub_workspace_list[0].file_dict.get("model.py") is None: raise ModelEmptyError("model.py is empty") # to replace & inject code @@ -26,13 +60,45 @@ class QlibModelRunner(CachedRunner[QlibModelExperiment]): env_to_use = {"PYTHONPATH": "./"} - if exp.sub_tasks[0].model_type == "TimeSeries": - env_to_use.update({"dataset_cls": "TSDatasetH", "step_len": 20, "num_timesteps": 20}) - elif exp.sub_tasks[0].model_type == "Tabular": - env_to_use.update({"dataset_cls": "DatasetH"}) + training_hyperparameters = exp.sub_tasks[0].training_hyperparameters + if training_hyperparameters: + env_to_use.update( + { + "n_epochs": str(training_hyperparameters.get("n_epochs", "100")), + "lr": str(training_hyperparameters.get("lr", "1e-3")), + "early_stop": str(training_hyperparameters.get("early_stop", 10)), + "batch_size": str(training_hyperparameters.get("batch_size", 256)), + "weight_decay": str(training_hyperparameters.get("weight_decay", 0.0001)), + } + ) - result = exp.experiment_workspace.execute(qlib_config_name="conf.yaml", run_env=env_to_use) + logger.info(f"start to run {exp.sub_tasks[0].name} model") + if exp.sub_tasks[0].model_type == "TimeSeries": + if exist_sota_factor_exp: + env_to_use.update( + {"dataset_cls": "TSDatasetH", "num_features": num_features, "step_len": 20, "num_timesteps": 20} + ) + result, stdout = exp.experiment_workspace.execute( + qlib_config_name="conf_model_combined.yaml", run_env=env_to_use + ) + else: + env_to_use.update({"dataset_cls": "TSDatasetH", "step_len": 20, "num_timesteps": 20}) + result, stdout = exp.experiment_workspace.execute(qlib_config_name="conf.yaml", run_env=env_to_use) + elif exp.sub_tasks[0].model_type == "Tabular": + if exist_sota_factor_exp: + env_to_use.update({"dataset_cls": "DatasetH", "num_features": num_features}) + result, stdout = exp.experiment_workspace.execute( + qlib_config_name="conf_model_combined.yaml", run_env=env_to_use + ) + else: + env_to_use.update({"dataset_cls": "DatasetH"}) + result, stdout = exp.experiment_workspace.execute(qlib_config_name="conf.yaml", run_env=env_to_use) exp.result = result + exp.stdout = stdout + + if result is None: + logger.error(f"Failed to run {exp.sub_tasks[0].name}, because {stdout}") + raise ModelEmptyError(f"Failed to run {exp.sub_tasks[0].name} model, because {stdout}") return exp diff --git a/rdagent/scenarios/qlib/developer/utils.py b/rdagent/scenarios/qlib/developer/utils.py new file mode 100644 index 00000000..ea541890 --- /dev/null +++ b/rdagent/scenarios/qlib/developer/utils.py @@ -0,0 +1,67 @@ +from typing import List + +import pandas as pd + +from rdagent.components.coder.CoSTEER.evaluators import CoSTEERMultiFeedback +from rdagent.core.conf import RD_AGENT_SETTINGS +from rdagent.core.exception import FactorEmptyError +from rdagent.core.utils import multiprocessing_wrapper +from rdagent.log import rdagent_logger as logger +from rdagent.scenarios.qlib.experiment.factor_experiment import QlibFactorExperiment + + +def process_factor_data(exp_or_list: List[QlibFactorExperiment] | QlibFactorExperiment) -> pd.DataFrame: + """ + Process and combine factor data from experiment implementations. + + Args: + exp (ASpecificExp): The experiment containing factor data. + + Returns: + pd.DataFrame: Combined factor data without NaN values. + """ + if isinstance(exp_or_list, QlibFactorExperiment): + exp_or_list = [exp_or_list] + factor_dfs = [] + + # Collect all exp's dataframes + for exp in exp_or_list: + if isinstance(exp, QlibFactorExperiment): + if len(exp.sub_tasks) > 0: + # if it has no sub_tasks, the experiment is results from template project. + # otherwise, it is developed with designed task. So it should have feedback. + assert isinstance(exp.prop_dev_feedback, CoSTEERMultiFeedback) + # Iterate over sub-implementations and execute them to get each factor data + message_and_df_list = multiprocessing_wrapper( + [ + (implementation.execute, ("All",)) + for implementation, fb in zip(exp.sub_workspace_list, exp.prop_dev_feedback) + if implementation and fb + ], # only execute successfully feedback + n=RD_AGENT_SETTINGS.multi_proc_n, + ) + error_message = "" + for message, df in message_and_df_list: + # Check if factor generation was successful + if df is not None and "datetime" in df.index.names: + time_diff = df.index.get_level_values("datetime").to_series().diff().dropna().unique() + if pd.Timedelta(minutes=1) not in time_diff: + factor_dfs.append(df) + logger.info( + f"Factor data from {exp.hypothesis.concise_justification} is successfully generated." + ) + else: + logger.warning(f"Factor data from {exp.hypothesis.concise_justification} is not generated.") + else: + error_message += f"Factor data from {exp.hypothesis.concise_justification} is not generated because of {message}" + logger.warning( + f"Factor data from {exp.hypothesis.concise_justification} is not generated because of {message}" + ) + + # Combine all successful factor data + if factor_dfs: + return pd.concat(factor_dfs, axis=1) + else: + raise FactorEmptyError( + f"No valid factor data found to merge (in process_factor_data) because of {error_message}." + ) diff --git a/rdagent/scenarios/qlib/docker/Dockerfile b/rdagent/scenarios/qlib/docker/Dockerfile index dcedfe10..932a22ee 100644 --- a/rdagent/scenarios/qlib/docker/Dockerfile +++ b/rdagent/scenarios/qlib/docker/Dockerfile @@ -13,7 +13,7 @@ RUN git clone https://github.com/microsoft/qlib.git WORKDIR /workspace/qlib -RUN git fetch && git reset dbde1a109fb53e742a4dd210552ebf943576add9 --hard +RUN git fetch && git reset 3e72593b8c985f01979bebcf646658002ac43b00 --hard RUN python -m pip install --upgrade cython RUN python -m pip install -e . diff --git a/rdagent/scenarios/qlib/experiment/factor_experiment.py b/rdagent/scenarios/qlib/experiment/factor_experiment.py index c9031213..8972922d 100644 --- a/rdagent/scenarios/qlib/experiment/factor_experiment.py +++ b/rdagent/scenarios/qlib/experiment/factor_experiment.py @@ -7,31 +7,30 @@ from rdagent.components.coder.factor_coder.factor import ( FactorTask, ) from rdagent.core.experiment import Task -from rdagent.core.prompts import Prompts from rdagent.core.scenario import Scenario from rdagent.scenarios.qlib.experiment.utils import get_data_folder_intro from rdagent.scenarios.qlib.experiment.workspace import QlibFBWorkspace - -prompt_dict = Prompts(file_path=Path(__file__).parent / "prompts.yaml") +from rdagent.utils.agent.tpl import T class QlibFactorExperiment(FactorExperiment[FactorTask, QlibFBWorkspace, FactorFBWorkspace]): def __init__(self, *args, **kwargs) -> None: super().__init__(*args, **kwargs) self.experiment_workspace = QlibFBWorkspace(template_folder_path=Path(__file__).parent / "factor_template") + self.stdout = "" class QlibFactorScenario(Scenario): def __init__(self) -> None: super().__init__() - self._background = deepcopy(prompt_dict["qlib_factor_background"]) + self._background = deepcopy(T(".prompts:qlib_factor_background").r()) self._source_data = deepcopy(get_data_folder_intro()) - self._output_format = deepcopy(prompt_dict["qlib_factor_output_format"]) - self._interface = deepcopy(prompt_dict["qlib_factor_interface"]) - self._strategy = deepcopy(prompt_dict["qlib_factor_strategy"]) - self._simulator = deepcopy(prompt_dict["qlib_factor_simulator"]) - self._rich_style_description = deepcopy(prompt_dict["qlib_factor_rich_style_description"]) - self._experiment_setting = deepcopy(prompt_dict["qlib_factor_experiment_setting"]) + self._output_format = deepcopy(T(".prompts:qlib_factor_output_format").r()) + self._interface = deepcopy(T(".prompts:qlib_factor_interface").r()) + self._strategy = deepcopy(T(".prompts:qlib_factor_strategy").r()) + self._simulator = deepcopy(T(".prompts:qlib_factor_simulator").r()) + self._rich_style_description = deepcopy(T(".prompts:qlib_factor_rich_style_description").r()) + self._experiment_setting = deepcopy(T(".prompts:qlib_factor_experiment_setting").r()) @property def background(self) -> str: diff --git a/rdagent/scenarios/qlib/experiment/factor_from_report_experiment.py b/rdagent/scenarios/qlib/experiment/factor_from_report_experiment.py index 5cb2838d..64dd2b0a 100644 --- a/rdagent/scenarios/qlib/experiment/factor_from_report_experiment.py +++ b/rdagent/scenarios/qlib/experiment/factor_from_report_experiment.py @@ -1,23 +1,13 @@ from copy import deepcopy -from pathlib import Path -from rdagent.components.coder.factor_coder.factor import ( - FactorExperiment, - FactorFBWorkspace, - FactorTask, -) -from rdagent.core.prompts import Prompts -from rdagent.core.scenario import Scenario from rdagent.scenarios.qlib.experiment.factor_experiment import QlibFactorScenario -from rdagent.scenarios.qlib.experiment.workspace import QlibFBWorkspace - -prompt_dict = Prompts(file_path=Path(__file__).parent / "prompts.yaml") +from rdagent.utils.agent.tpl import T class QlibFactorFromReportScenario(QlibFactorScenario): def __init__(self) -> None: super().__init__() - self._rich_style_description = deepcopy(prompt_dict["qlib_factor_from_report_rich_style_description"]) + self._rich_style_description = deepcopy(T(".prompts:qlib_factor_from_report_rich_style_description").r()) @property def rich_style_description(self) -> str: diff --git a/rdagent/scenarios/qlib/experiment/factor_template/conf.yaml b/rdagent/scenarios/qlib/experiment/factor_template/conf.yaml index b8483b02..59825dea 100644 --- a/rdagent/scenarios/qlib/experiment/factor_template/conf.yaml +++ b/rdagent/scenarios/qlib/experiment/factor_template/conf.yaml @@ -11,6 +11,28 @@ data_handler_config: &data_handler_config fit_start_time: 2008-01-01 fit_end_time: 2014-12-31 instruments: *market + infer_processors: + - class: FilterCol + kwargs: + fields_group: feature + col_list: ["RESI5", "WVMA5", "RSQR5", "KLEN", "RSQR10", "CORR5", "CORD5", "CORR10", + "ROC60", "RESI10", "VSTD5", "RSQR60", "CORR60", "WVMA60", "STD5", + "RSQR20", "CORD60", "CORD10", "CORR20", "KLOW" + ] + - class: RobustZScoreNorm + kwargs: + fields_group: feature + clip_outlier: true + - class: Fillna + kwargs: + fields_group: feature + learn_processors: + - class: DropnaLabel + - class: CSRankNorm + kwargs: + fields_group: label + label: ["Ref($close, -2) / Ref($close, -1) - 1"] + port_analysis_config: &port_analysis_config strategy: class: TopkDropoutStrategy diff --git a/rdagent/scenarios/qlib/experiment/factor_template/conf_combined.yaml b/rdagent/scenarios/qlib/experiment/factor_template/conf_combined.yaml index e98305e3..8b37a33b 100644 --- a/rdagent/scenarios/qlib/experiment/factor_template/conf_combined.yaml +++ b/rdagent/scenarios/qlib/experiment/factor_template/conf_combined.yaml @@ -19,6 +19,18 @@ data_handler_config: &data_handler_config label: - ["Ref($close, -2)/Ref($close, -1) - 1"] - ["LABEL0"] + feature: + - ["Resi($close, 5)/$close", "Std(Abs($close/Ref($close, 1)-1)*$volume, 5)/(Mean(Abs($close/Ref($close, 1)-1)*$volume, 5)+1e-12)", + "Rsquare($close, 5)", "($high-$low)/$open", "Rsquare($close, 10)", "Corr($close, Log($volume+1), 5)", + "Corr($close/Ref($close,1), Log($volume/Ref($volume, 1)+1), 5)", "Corr($close, Log($volume+1), 10)", + "Ref($close, 60)/$close", "Resi($close, 10)/$close", "Std($volume, 5)/($volume+1e-12)", + "Rsquare($close, 60)", "Corr($close, Log($volume+1), 60)", "Std(Abs($close/Ref($close, 1)-1)*$volume, 60)/(Mean(Abs($close/Ref($close, 1)-1)*$volume, 60)+1e-12)", + "Std($close, 5)/$close", "Rsquare($close, 20)", "Corr($close/Ref($close,1), Log($volume/Ref($volume, 1)+1), 60)", + "Corr($close/Ref($close,1), Log($volume/Ref($volume, 1)+1), 10)", "Corr($close, Log($volume+1), 20)", + "(Less($open, $close)-$low)/$open"] + - ["RESI5", "WVMA5", "RSQR5", "KLEN", "RSQR10", "CORR5", "CORD5", "CORR10", + "ROC60", "RESI10", "VSTD5", "RSQR60", "CORR60", "WVMA60", "STD5", + "RSQR20", "CORD60", "CORD10", "CORR20", "KLOW"] - class: qlib.data.dataset.loader.StaticDataLoader kwargs: config: "combined_factors_df.parquet" diff --git a/rdagent/scenarios/qlib/experiment/factor_template/conf_model_combined.yaml b/rdagent/scenarios/qlib/experiment/factor_template/conf_model_combined.yaml new file mode 100644 index 00000000..0d4e07da --- /dev/null +++ b/rdagent/scenarios/qlib/experiment/factor_template/conf_model_combined.yaml @@ -0,0 +1,119 @@ +qlib_init: + provider_uri: "~/.qlib/qlib_data/cn_data" + region: cn + +market: &market csi300 +benchmark: &benchmark SH000300 + +data_handler_config: &data_handler_config + start_time: 2008-01-01 + end_time: 2022-08-01 + instruments: *market + data_loader: + class: NestedDataLoader + kwargs: + dataloader_l: + - class: qlib.contrib.data.loader.Alpha158DL + kwargs: + config: + label: + - ["Ref($close, -2)/Ref($close, -1) - 1"] + - ["LABEL0"] + feature: + - ["Resi($close, 5)/$close", "Std(Abs($close/Ref($close, 1)-1)*$volume, 5)/(Mean(Abs($close/Ref($close, 1)-1)*$volume, 5)+1e-12)", + "Rsquare($close, 5)", "($high-$low)/$open", "Rsquare($close, 10)", "Corr($close, Log($volume+1), 5)", + "Corr($close/Ref($close,1), Log($volume/Ref($volume, 1)+1), 5)", "Corr($close, Log($volume+1), 10)", + "Ref($close, 60)/$close", "Resi($close, 10)/$close", "Std($volume, 5)/($volume+1e-12)", + "Rsquare($close, 60)", "Corr($close, Log($volume+1), 60)", "Std(Abs($close/Ref($close, 1)-1)*$volume, 60)/(Mean(Abs($close/Ref($close, 1)-1)*$volume, 60)+1e-12)", + "Std($close, 5)/$close", "Rsquare($close, 20)", "Corr($close/Ref($close,1), Log($volume/Ref($volume, 1)+1), 60)", + "Corr($close/Ref($close,1), Log($volume/Ref($volume, 1)+1), 10)", "Corr($close, Log($volume+1), 20)", + "(Less($open, $close)-$low)/$open"] + - ["RESI5", "WVMA5", "RSQR5", "KLEN", "RSQR10", "CORR5", "CORD5", "CORR10", + "ROC60", "RESI10", "VSTD5", "RSQR60", "CORR60", "WVMA60", "STD5", + "RSQR20", "CORD60", "CORD10", "CORR20", "KLOW"] + - class: qlib.data.dataset.loader.StaticDataLoader + kwargs: + config: "combined_factors_df.parquet" + + infer_processors: + - class: RobustZScoreNorm + kwargs: + fields_group: feature + clip_outlier: true + fit_start_time: 2008-01-01 + fit_end_time: 2014-12-31 + - class: Fillna + kwargs: + fields_group: feature + learn_processors: + - class: DropnaLabel + - class: CSZScoreNorm + kwargs: + fields_group: label + +port_analysis_config: &port_analysis_config + strategy: + class: TopkDropoutStrategy + module_path: qlib.contrib.strategy + kwargs: + signal: + topk: 50 + n_drop: 5 + backtest: + start_time: 2017-01-01 + end_time: 2020-08-01 + account: 100000000 + benchmark: *benchmark + exchange_kwargs: + limit_threshold: 0.095 + deal_price: close + open_cost: 0.0005 + close_cost: 0.0015 + min_cost: 5 + +task: + model: + class: GeneralPTNN + module_path: qlib.contrib.model.pytorch_general_nn + kwargs: + n_epochs: {{ n_epochs }} + lr: {{ lr }} + early_stop: {{ early_stop }} + batch_size: {{ batch_size }} + weight_decay: {{ weight_decay }} + metric: loss + loss: mse + n_jobs: 20 + GPU: 0 + pt_model_uri: "model.model_cls" + pt_model_kwargs: { + "num_features": {{ num_features }}{% if num_timesteps %}, "num_timesteps": {{ num_timesteps }}{% endif %} + } + dataset: + class: {{ dataset_cls | default("DatasetH") }} + module_path: qlib.data.dataset + kwargs: + handler: + class: DataHandlerLP + module_path: qlib.contrib.data.handler + kwargs: *data_handler_config + segments: + train: [2008-01-01, 2014-12-31] + valid: [2015-01-01, 2016-12-31] + test: [2017-01-01, 2020-08-01] + {% if step_len %}step_len: {{ step_len }}{% endif %} + record: + - class: SignalRecord + module_path: qlib.workflow.record_temp + kwargs: + model: + dataset: + - class: SigAnaRecord + module_path: qlib.workflow.record_temp + kwargs: + ana_long_short: False + ann_scaler: 252 + - class: PortAnaRecord + module_path: qlib.workflow.record_temp + kwargs: + config: *port_analysis_config diff --git a/rdagent/scenarios/qlib/experiment/model_experiment.py b/rdagent/scenarios/qlib/experiment/model_experiment.py index ea971315..0e2bbdf4 100644 --- a/rdagent/scenarios/qlib/experiment/model_experiment.py +++ b/rdagent/scenarios/qlib/experiment/model_experiment.py @@ -7,28 +7,27 @@ from rdagent.components.coder.model_coder.model import ( ModelTask, ) from rdagent.core.experiment import Task -from rdagent.core.prompts import Prompts from rdagent.core.scenario import Scenario from rdagent.scenarios.qlib.experiment.workspace import QlibFBWorkspace - -prompt_dict = Prompts(file_path=Path(__file__).parent / "prompts.yaml") +from rdagent.utils.agent.tpl import T class QlibModelExperiment(ModelExperiment[ModelTask, QlibFBWorkspace, ModelFBWorkspace]): def __init__(self, *args, **kwargs) -> None: super().__init__(*args, **kwargs) self.experiment_workspace = QlibFBWorkspace(template_folder_path=Path(__file__).parent / "model_template") + self.stdout = "" class QlibModelScenario(Scenario): def __init__(self) -> None: super().__init__() - self._background = deepcopy(prompt_dict["qlib_model_background"]) - self._output_format = deepcopy(prompt_dict["qlib_model_output_format"]) - self._interface = deepcopy(prompt_dict["qlib_model_interface"]) - self._simulator = deepcopy(prompt_dict["qlib_model_simulator"]) - self._rich_style_description = deepcopy(prompt_dict["qlib_model_rich_style_description"]) - self._experiment_setting = deepcopy(prompt_dict["qlib_model_experiment_setting"]) + self._background = deepcopy(T(".prompts:qlib_model_background").r()) + self._output_format = deepcopy(T(".prompts:qlib_model_output_format").r()) + self._interface = deepcopy(T(".prompts:qlib_model_interface").r()) + self._simulator = deepcopy(T(".prompts:qlib_model_simulator").r()) + self._rich_style_description = deepcopy(T(".prompts:qlib_model_rich_style_description").r()) + self._experiment_setting = deepcopy(T(".prompts:qlib_model_experiment_setting").r()) @property def background(self) -> str: diff --git a/rdagent/scenarios/qlib/experiment/model_template/conf.yaml b/rdagent/scenarios/qlib/experiment/model_template/conf.yaml index bbed0975..3810c887 100644 --- a/rdagent/scenarios/qlib/experiment/model_template/conf.yaml +++ b/rdagent/scenarios/qlib/experiment/model_template/conf.yaml @@ -55,30 +55,20 @@ task: class: GeneralPTNN module_path: qlib.contrib.model.pytorch_general_nn kwargs: - n_epochs: 100 - lr: 1e-3 - early_stop: 10 - batch_size: 2000 + n_epochs: {{ n_epochs }} + lr: {{ lr }} + early_stop: {{ early_stop }} + batch_size: {{ batch_size }} + weight_decay: {{ weight_decay }} metric: loss loss: mse n_jobs: 20 GPU: 0 - # loss: mse - # lr: 0.002 - # optimizer: adam - # batch_size: 8192 - # GPU: 0 - weight_decay: 0.0001 - # pt_model_uri: "qlib.contrib.model.pytorch_nn.Net" - # pt_model_uri: "env_tpl.model.Net" - # pt_model_uri: "./model.py:Net" pt_model_uri: "model.model_cls" pt_model_kwargs: { "num_features": 20, {% if num_timesteps %}num_timesteps: {{ num_timesteps }}{% endif %} - } - # input_dim: 20 - # How should I use jinja to put step len here conditionally + } dataset: class: {{ dataset_cls | default("DatasetH") }} module_path: qlib.data.dataset diff --git a/rdagent/scenarios/qlib/experiment/model_template/conf_model_combined.yaml b/rdagent/scenarios/qlib/experiment/model_template/conf_model_combined.yaml new file mode 100644 index 00000000..0d4e07da --- /dev/null +++ b/rdagent/scenarios/qlib/experiment/model_template/conf_model_combined.yaml @@ -0,0 +1,119 @@ +qlib_init: + provider_uri: "~/.qlib/qlib_data/cn_data" + region: cn + +market: &market csi300 +benchmark: &benchmark SH000300 + +data_handler_config: &data_handler_config + start_time: 2008-01-01 + end_time: 2022-08-01 + instruments: *market + data_loader: + class: NestedDataLoader + kwargs: + dataloader_l: + - class: qlib.contrib.data.loader.Alpha158DL + kwargs: + config: + label: + - ["Ref($close, -2)/Ref($close, -1) - 1"] + - ["LABEL0"] + feature: + - ["Resi($close, 5)/$close", "Std(Abs($close/Ref($close, 1)-1)*$volume, 5)/(Mean(Abs($close/Ref($close, 1)-1)*$volume, 5)+1e-12)", + "Rsquare($close, 5)", "($high-$low)/$open", "Rsquare($close, 10)", "Corr($close, Log($volume+1), 5)", + "Corr($close/Ref($close,1), Log($volume/Ref($volume, 1)+1), 5)", "Corr($close, Log($volume+1), 10)", + "Ref($close, 60)/$close", "Resi($close, 10)/$close", "Std($volume, 5)/($volume+1e-12)", + "Rsquare($close, 60)", "Corr($close, Log($volume+1), 60)", "Std(Abs($close/Ref($close, 1)-1)*$volume, 60)/(Mean(Abs($close/Ref($close, 1)-1)*$volume, 60)+1e-12)", + "Std($close, 5)/$close", "Rsquare($close, 20)", "Corr($close/Ref($close,1), Log($volume/Ref($volume, 1)+1), 60)", + "Corr($close/Ref($close,1), Log($volume/Ref($volume, 1)+1), 10)", "Corr($close, Log($volume+1), 20)", + "(Less($open, $close)-$low)/$open"] + - ["RESI5", "WVMA5", "RSQR5", "KLEN", "RSQR10", "CORR5", "CORD5", "CORR10", + "ROC60", "RESI10", "VSTD5", "RSQR60", "CORR60", "WVMA60", "STD5", + "RSQR20", "CORD60", "CORD10", "CORR20", "KLOW"] + - class: qlib.data.dataset.loader.StaticDataLoader + kwargs: + config: "combined_factors_df.parquet" + + infer_processors: + - class: RobustZScoreNorm + kwargs: + fields_group: feature + clip_outlier: true + fit_start_time: 2008-01-01 + fit_end_time: 2014-12-31 + - class: Fillna + kwargs: + fields_group: feature + learn_processors: + - class: DropnaLabel + - class: CSZScoreNorm + kwargs: + fields_group: label + +port_analysis_config: &port_analysis_config + strategy: + class: TopkDropoutStrategy + module_path: qlib.contrib.strategy + kwargs: + signal: + topk: 50 + n_drop: 5 + backtest: + start_time: 2017-01-01 + end_time: 2020-08-01 + account: 100000000 + benchmark: *benchmark + exchange_kwargs: + limit_threshold: 0.095 + deal_price: close + open_cost: 0.0005 + close_cost: 0.0015 + min_cost: 5 + +task: + model: + class: GeneralPTNN + module_path: qlib.contrib.model.pytorch_general_nn + kwargs: + n_epochs: {{ n_epochs }} + lr: {{ lr }} + early_stop: {{ early_stop }} + batch_size: {{ batch_size }} + weight_decay: {{ weight_decay }} + metric: loss + loss: mse + n_jobs: 20 + GPU: 0 + pt_model_uri: "model.model_cls" + pt_model_kwargs: { + "num_features": {{ num_features }}{% if num_timesteps %}, "num_timesteps": {{ num_timesteps }}{% endif %} + } + dataset: + class: {{ dataset_cls | default("DatasetH") }} + module_path: qlib.data.dataset + kwargs: + handler: + class: DataHandlerLP + module_path: qlib.contrib.data.handler + kwargs: *data_handler_config + segments: + train: [2008-01-01, 2014-12-31] + valid: [2015-01-01, 2016-12-31] + test: [2017-01-01, 2020-08-01] + {% if step_len %}step_len: {{ step_len }}{% endif %} + record: + - class: SignalRecord + module_path: qlib.workflow.record_temp + kwargs: + model: + dataset: + - class: SigAnaRecord + module_path: qlib.workflow.record_temp + kwargs: + ana_long_short: False + ann_scaler: 252 + - class: PortAnaRecord + module_path: qlib.workflow.record_temp + kwargs: + config: *port_analysis_config diff --git a/rdagent/scenarios/qlib/experiment/prompts.yaml b/rdagent/scenarios/qlib/experiment/prompts.yaml index a83fa60f..85e2a0d0 100644 --- a/rdagent/scenarios/qlib/experiment/prompts.yaml +++ b/rdagent/scenarios/qlib/experiment/prompts.yaml @@ -1,3 +1,9 @@ +qlib_quant_background: |- + Quantitative investment is a data-driven approach to asset management that relies on mathematical models, statistical techniques, and computational methods to analyze financial markets and make investment decisions. Two essential components of this approach are factors and models. + + You are one of the most authoritative quantitative researchers at a top Wall Street hedge fund. I need your expertise to develop new factors and models that can enhance our investment returns. Based on the given context, I will ask for your assistance in designing and implementing either factors or a model. + + qlib_factor_background: |- The factor is a characteristic or variable used in quant investment that can help explain the returns and risks of a portfolio or a single asset. Factors are used by investors to identify and exploit sources of excess returns, and they are central to many quantitative investment strategies. Each number in the factor represents a physics value to an instrument on a day. @@ -154,8 +160,9 @@ qlib_model_background: |- 1. Name: The name of the model. 2. Description: The description of the model. 3. Architecture: The detailed architecture of the model, such as neural network layers or tree structures. - 4. Hyperparameters: The hyperparameters used in the model, such as learning rate, number of epochs, etc. - 5. ModelType: The type of the model, "Tabular" for tabular model and "TimeSeries" for time series model. + 4. Hyperparameters: The hyperparameters used in the model. + 5. Training_hyperparameters: The hyperparameters used during the training process. + 6. ModelType: The type of the model, "Tabular" for tabular model and "TimeSeries" for time series model. The model should provide clear and detailed documentation of its architecture and hyperparameters. One model should statically define one output with a fixed architecture and hyperparameters. qlib_model_interface: |- @@ -176,9 +183,8 @@ qlib_model_interface: |- model_cls = XXXModel ``` - The model has two types, "Tabular" for tabular model and "TimeSeries" for time series model. The input shape to a tabular model is (batch_size, num_features) and the input shape to a time series model is (batch_size, num_timesteps, num_features). The output shape of the model should be (batch_size, 1). - The "batch_size" is a dynamic value which is determined by the input of forward function. - The "num_features" and "num_timesteps" are static which will be provided to the model through init function. + The model can be configured as either "Tabular" for tabular models or "TimeSeries" for time series models. For a tabular model, the input shape is (batch_size, num_features), while for a time series model, the input shape is (batch_size, num_timesteps, num_features). In both cases, the output shape of the model should be (batch_size, 1). + `num_features` will be directly set for the model based on the input data shape. User will initialize the tabular model with the following code: ```python model = model_cls(num_features=num_features) @@ -206,7 +212,7 @@ qlib_model_simulator: |- 1. Generate a baseline factor table. 2. Train the model defined in your class Net to predict the next several days' returns based on the factor values. 3. Build a portfolio based on the predicted returns using a specific strategy. - 4. Evaluate the portfolio's performance, including metrics such as return, Sharpe ratio, max drawdown, and others. + 4. Evaluate the portfolio's performance, including metrics such as return, IC, max drawdown, and others. 5. Iterate on growing the hypothesis to enable model improvements based on performance evaluations and feedback. qlib_model_rich_style_description: |- diff --git a/rdagent/scenarios/qlib/experiment/quant_experiment.py b/rdagent/scenarios/qlib/experiment/quant_experiment.py new file mode 100644 index 00000000..e601e031 --- /dev/null +++ b/rdagent/scenarios/qlib/experiment/quant_experiment.py @@ -0,0 +1,167 @@ +from copy import deepcopy +from pathlib import Path + +# Factor +from rdagent.components.coder.factor_coder.factor import ( + FactorExperiment, + FactorFBWorkspace, + FactorTask, +) + +# Model +from rdagent.components.coder.model_coder.model import ( + ModelExperiment, + ModelFBWorkspace, + ModelTask, +) +from rdagent.core.experiment import Task +from rdagent.core.scenario import Scenario +from rdagent.scenarios.qlib.experiment.utils import get_data_folder_intro +from rdagent.scenarios.qlib.experiment.workspace import QlibFBWorkspace +from rdagent.utils.agent.tpl import T + + +class QlibFactorExperiment(FactorExperiment[FactorTask, QlibFBWorkspace, FactorFBWorkspace]): + def __init__(self, *args, **kwargs) -> None: + super().__init__(*args, **kwargs) + self.experiment_workspace = QlibFBWorkspace(template_folder_path=Path(__file__).parent / "factor_template") + + +class QlibModelExperiment(ModelExperiment[ModelTask, QlibFBWorkspace, ModelFBWorkspace]): + def __init__(self, *args, **kwargs) -> None: + super().__init__(*args, **kwargs) + self.experiment_workspace = QlibFBWorkspace(template_folder_path=Path(__file__).parent / "model_template") + + +class QlibQuantScenario(Scenario): + def __init__(self) -> None: + super().__init__() + self._source_data = deepcopy(get_data_folder_intro()) + + self._rich_style_description = deepcopy(T(".prompts:qlib_factor_rich_style_description").r()) + self._experiment_setting = deepcopy(T(".prompts:qlib_factor_experiment_setting").r()) + + def background(self, tag=None) -> str: + assert tag in [None, "factor", "model"] + quant_background = "The background of the scenario is as follows:\n" + T(".prompts:qlib_quant_background").r() + factor_background = ( + "This time, I need your help with the research and development of the factor. The background of the factor scenario is as follows:\n" + + T(".prompts:qlib_factor_background").r() + ) + model_background = ( + "This time, I need your help with the research and development of the model. The background of the model scenario is as follows:\n" + + T(".prompts:qlib_model_background").r() + ) + + # TODO: There are some issues here + if tag is None: + return quant_background + "\n" + factor_background + "\n" + model_background + elif tag == "factor": + return factor_background + else: + return model_background + + def get_source_data_desc(self) -> str: + return self._source_data + + def output_format(self, tag=None) -> str: + assert tag in [None, "factor", "model"] + factor_output_format = ( + "The factor code should output the following format:\n" + T(".prompts:qlib_factor_output_format").r() + ) + model_output_format = ( + "The model code should output the following format:\n" + T(".prompts:qlib_model_output_format").r() + ) + + if tag is None: + return factor_output_format + "\n" + model_output_format + elif tag == "factor": + return factor_output_format + else: + return model_output_format + + def interface(self, tag=None) -> str: + assert tag in [None, "factor", "model"] + factor_interface = ( + "The factor code should be written in the following interface:\n" + T(".prompts:qlib_factor_interface").r() + ) + model_interface = ( + "The model code should be written in the following interface:\n" + T(".prompts:qlib_model_interface").r() + ) + + if tag is None: + return factor_interface + "\n" + model_interface + elif tag == "factor": + return factor_interface + else: + return model_interface + + def simulator(self, tag=None) -> str: + assert tag in [None, "factor", "model"] + factor_simulator = "The factor code will be sent to the simulator:\n" + T(".prompts:qlib_factor_simulator").r() + model_simulator = "The model code will be sent to the simulator:\n" + T(".prompts:qlib_model_simulator").r() + + if tag is None: + return factor_simulator + "\n" + model_simulator + elif tag == "factor": + return factor_simulator + else: + return model_simulator + + @property + def rich_style_description(self) -> str: + return self._rich_style_description + + @property + def experiment_setting(self) -> str: + return self._experiment_setting + + def get_scenario_all_desc( + self, + task: Task | None = None, + filtered_tag: str | None = None, + simple_background: bool | None = None, + action: str | None = None, + ) -> str: + def common_description(action: str | None = None) -> str: + return f"""\n------Background of the scenario------ +{self.background(action)} +------The source dataset you can use------ +{self.get_source_data_desc()} +""" + + # TODO: There are still some issues with handling source_data here + def source_data() -> str: + return f""" +------The source data you can use------ +{self.get_source_data_desc()} +""" + + def interface(tag: str | None) -> str: + return f""" +------The interface you should follow to write the runnable code------ +{self.interface(tag)} +""" + + def output(tag: str | None) -> str: + return f""" +------The output of your code should be in the format------ +{self.output_format(tag)} +""" + + def simulator(tag: str | None) -> str: + return f""" +------The simulator user can use to test your solution------ +{self.simulator(tag)} +""" + + if simple_background: + return common_description() + elif filtered_tag == "hypothesis_and_experiment" or filtered_tag == "feedback": + return common_description() + simulator(None) + elif filtered_tag == "factor" or filtered_tag == "feature" or filtered_tag == "factors": + return common_description("factor") + interface("factor") + output("factor") + simulator("factor") + elif filtered_tag == "model" or filtered_tag == "model tuning": + return common_description("model") + interface("model") + output("model") + simulator("model") + elif action == "factor" or action == "model": + return common_description(action) + interface(action) + output(action) + simulator(action) diff --git a/rdagent/scenarios/qlib/experiment/utils.py b/rdagent/scenarios/qlib/experiment/utils.py index 0a4cac2d..0edde395 100644 --- a/rdagent/scenarios/qlib/experiment/utils.py +++ b/rdagent/scenarios/qlib/experiment/utils.py @@ -1,11 +1,9 @@ -import io +import random import re import shutil from pathlib import Path import pandas as pd - -# render it with jinja from jinja2 import Environment, StrictUndefined from rdagent.components.coder.factor_coder.config import FACTOR_COSTEER_SETTINGS @@ -69,53 +67,77 @@ def get_file_desc(p: Path, variable_list=[]) -> str: JJ_TPL = Environment(undefined=StrictUndefined).from_string( """ -{{file_name}} -```{{type_desc}} +# {{file_name}} + +## File Type +{{type_desc}} + +## Content Overview {{content}} -``` """ ) if p.name.endswith(".h5"): df = pd.read_hdf(p) - # get df.head() as string with full width - pd.set_option("display.max_columns", None) # or 1000 - pd.set_option("display.max_rows", None) # or 1000 - pd.set_option("display.max_colwidth", None) # or 199 + pd.set_option("display.max_columns", None) + pd.set_option("display.max_rows", None) + pd.set_option("display.max_colwidth", None) - if isinstance(df.index, pd.MultiIndex): - df_info = f"MultiIndex names:, {df.index.names})\n" - else: - df_info = f"Index name: {df.index.name}\n" + df_info = "### Data Structure\n" + df_info += ( + f"- Index: MultiIndex with levels {df.index.names}\n" + if isinstance(df.index, pd.MultiIndex) + else f"- Index: {df.index.name}\n" + ) + + df_info += "\n### Columns\n" columns = df.dtypes.to_dict() - filtered_columns = [f"{i, j}" for i, j in columns.items() if i in variable_list] - if filtered_columns: - df_info += "Related Data columns: \n" - df_info += ",".join(filtered_columns) + grouped_columns = {} + + for col in columns: + if col.startswith("$"): + prefix = col.split("_")[0] if "_" in col else col + grouped_columns.setdefault(prefix, []).append(col) + else: + grouped_columns.setdefault("other", []).append(col) + + if variable_list: + df_info += "#### Relevant Columns:\n" + relevant_line = ", ".join(f"{col}: {columns[col]}" for col in variable_list if col in columns) + df_info += relevant_line + "\n" else: - df_info += "Data columns: \n" - df_info += ",".join(columns) - df_info += "\n" + df_info += "#### All Columns:\n" + grouped_items = list(grouped_columns.items()) + random.shuffle(grouped_items) + for prefix, cols in grouped_items: + header = "Other Columns" if prefix == "other" else f"{prefix} Related Columns" + df_info += f"\n#### {header}:\n" + random.shuffle(cols) + line = ", ".join(f"{col}: {columns[col]}" for col in cols) + df_info += line + "\n" + if "REPORT_PERIOD" in df.columns: one_instrument = df.index.get_level_values("instrument")[0] df_on_one_instrument = df.loc[pd.IndexSlice[:, one_instrument], ["REPORT_PERIOD"]] - df_info += f""" -A snapshot of one instrument, from which you can tell the distribution of the data: -{df_on_one_instrument.head(5)} -""" + df_info += "\n### Sample Data\n" + df_info += f"Showing data for instrument {one_instrument}:\n" + df_info += str(df_on_one_instrument.head(5)) + return JJ_TPL.render( file_name=p.name, - type_desc="h5 info", + type_desc="HDF5 Data File", content=df_info, ) + elif p.name.endswith(".md"): with open(p) as f: content = f.read() return JJ_TPL.render( file_name=p.name, - type_desc="markdown", + type_desc="Markdown Documentation", content=content, ) + else: raise NotImplementedError( f"file type {p.name} is not supported. Please implement its description function.", diff --git a/rdagent/scenarios/qlib/experiment/workspace.py b/rdagent/scenarios/qlib/experiment/workspace.py index f3e16e11..ec245014 100644 --- a/rdagent/scenarios/qlib/experiment/workspace.py +++ b/rdagent/scenarios/qlib/experiment/workspace.py @@ -1,11 +1,13 @@ +import re from pathlib import Path from typing import Any import pandas as pd +from rdagent.components.coder.model_coder.conf import MODEL_COSTEER_SETTINGS from rdagent.core.experiment import FBWorkspace from rdagent.log import rdagent_logger as logger -from rdagent.utils.env import QTDockerEnv +from rdagent.utils.env import QlibCondaConf, QlibCondaEnv, QTDockerEnv class QlibFBWorkspace(FBWorkspace): @@ -14,29 +16,45 @@ class QlibFBWorkspace(FBWorkspace): self.inject_code_from_folder(template_folder_path) def execute(self, qlib_config_name: str = "conf.yaml", run_env: dict = {}, *args, **kwargs) -> str: - qtde = QTDockerEnv() + if MODEL_COSTEER_SETTINGS.env_type == "docker": + qtde = QTDockerEnv() + elif MODEL_COSTEER_SETTINGS.env_type == "conda": + qtde = QlibCondaEnv(conf=QlibCondaConf()) + else: + logger.error(f"Unknown env_type: {MODEL_COSTEER_SETTINGS.env_type}") + return None, "Unknown environment type" qtde.prepare() # Run the Qlib backtest - execute_log = qtde.run( + execute_qlib_log = qtde.run( local_path=str(self.workspace_path), entry=f"qrun {qlib_config_name}", env=run_env, ) + logger.log_object(execute_qlib_log, tag="Qlib_execute_log") + # TODO: We should handle the case when Docker times out. execute_log = qtde.run( local_path=str(self.workspace_path), entry="python read_exp_res.py", env=run_env, ) - ret_df = pd.read_pickle(self.workspace_path / "ret.pkl") - logger.log_object(ret_df, tag="Quantitative Backtesting Chart") + pattern = r"(Epoch\d+: train -[0-9\.]+, valid -[0-9\.]+|best score: -[0-9\.]+ @ \d+ epoch)" + matches = re.findall(pattern, execute_qlib_log) + execute_qlib_log = "\n".join(matches) - csv_path = self.workspace_path / "qlib_res.csv" + quantitative_backtesting_chart_path = self.workspace_path / "ret.pkl" + if quantitative_backtesting_chart_path.exists(): + ret_df = pd.read_pickle(quantitative_backtesting_chart_path) + logger.log_object(ret_df, tag="Quantitative Backtesting Chart") + else: + logger.error("No result file found.") + return None, execute_qlib_log - if not csv_path.exists(): - logger.error(f"File {csv_path} does not exist.") - return None - - return pd.read_csv(csv_path, index_col=0).iloc[:, 0] + qlib_res_path = self.workspace_path / "qlib_res.csv" + if qlib_res_path.exists(): + return pd.read_csv(qlib_res_path, index_col=0).iloc[:, 0], execute_qlib_log + else: + logger.error(f"File {qlib_res_path} does not exist.") + return None, execute_qlib_log diff --git a/rdagent/scenarios/qlib/factor_experiment_loader/pdf_loader.py b/rdagent/scenarios/qlib/factor_experiment_loader/pdf_loader.py index a9e61c92..822bf749 100644 --- a/rdagent/scenarios/qlib/factor_experiment_loader/pdf_loader.py +++ b/rdagent/scenarios/qlib/factor_experiment_loader/pdf_loader.py @@ -1,14 +1,10 @@ from __future__ import annotations import json -import multiprocessing as mp -import re -from pathlib import Path from typing import Mapping import numpy as np import pandas as pd -from jinja2 import Environment, StrictUndefined from sklearn.cluster import KMeans from sklearn.metrics.pairwise import cosine_similarity from sklearn.preprocessing import normalize @@ -19,7 +15,6 @@ from rdagent.components.document_reader.document_reader import ( ) from rdagent.components.loader.experiment_loader import FactorExperimentLoader from rdagent.core.conf import RD_AGENT_SETTINGS -from rdagent.core.prompts import Prompts from rdagent.core.utils import multiprocessing_wrapper from rdagent.log import rdagent_logger as logger from rdagent.oai.llm_conf import LLM_SETTINGS @@ -27,8 +22,7 @@ from rdagent.oai.llm_utils import APIBackend from rdagent.scenarios.qlib.factor_experiment_loader.json_loader import ( FactorExperimentLoaderFromDict, ) - -document_process_prompts = Prompts(file_path=Path(__file__).parent / "prompts.yaml") +from rdagent.utils.agent.tpl import T def classify_report_from_dict( @@ -62,7 +56,7 @@ def classify_report_from_dict( # ) res_dict = {} - classify_prompt = document_process_prompts["classify_system"] + classify_prompt = T(".prompts:classify_system").r() for key, value in tqdm(report_dict.items()): if not key.endswith(".pdf"): @@ -121,7 +115,7 @@ def __extract_factors_name_and_desc_from_content( content: str, ) -> dict[str, dict[str, str]]: session = APIBackend().build_chat_session( - session_system_prompt=document_process_prompts["extract_factors_system"], + session_system_prompt=T(".prompts:extract_factors_system").r(), ) extracted_factor_dict = {} @@ -138,7 +132,7 @@ def __extract_factors_name_and_desc_from_content( break for factor_name, factor_description in factors.items(): extracted_factor_dict[factor_name] = factor_description - current_user_prompt = document_process_prompts["extract_factors_follow_user"] + current_user_prompt = T(".prompts:extract_factors_follow_user").r() return extracted_factor_dict @@ -152,13 +146,10 @@ def __extract_factors_formulation_from_content( columns=["factor_name", "factor_description"], ) - system_prompt = document_process_prompts["extract_factor_formulation_system"] - current_user_prompt = ( - Environment(undefined=StrictUndefined) - .from_string( - document_process_prompts["extract_factor_formulation_user"], - ) - .render(report_content=content, factor_dict=factor_dict_df.to_string()) + system_prompt = T(".prompts:extract_factor_formulation_system").r() + current_user_prompt = T(".prompts:extract_factor_formulation_user").r( + report_content=content, + factor_dict=factor_dict_df.to_string(), ) session = APIBackend().build_chat_session(session_system_prompt=system_prompt) @@ -279,7 +270,7 @@ def __check_factor_dict_relevance( factor_df_string: str, ) -> dict[str, dict[str, str]]: extract_result_resp = APIBackend().build_messages_and_create_chat_completion( - system_prompt=document_process_prompts["factor_relevance_system"], + system_prompt=T(".prompts:factor_relevance_system").r(), user_prompt=factor_df_string, json_mode=True, ) @@ -322,7 +313,7 @@ def __check_factor_dict_viability_simulate_json_mode( factor_df_string: str, ) -> dict[str, dict[str, str]]: extract_result_resp = APIBackend().build_messages_and_create_chat_completion( - system_prompt=document_process_prompts["factor_viability_system"], + system_prompt=T(".prompts:factor_viability_system").r(), user_prompt=factor_df_string, json_mode=True, ) @@ -373,7 +364,7 @@ def __check_factor_duplication_simulate_json_mode( current_df = working_list.pop(0) if ( APIBackend().build_messages_and_calculate_token( - user_prompt=current_df.to_string(), system_prompt=document_process_prompts["factor_duplicate_system"] + user_prompt=current_df.to_string(), system_prompt=T(".prompts:factor_duplicate_system").r() ) > LLM_SETTINGS.chat_token_limit ): @@ -386,7 +377,7 @@ def __check_factor_duplication_simulate_json_mode( for current_df in final_list: current_factor_to_string = current_df.to_string() session = APIBackend().build_chat_session( - session_system_prompt=document_process_prompts["factor_duplicate_system"], + session_system_prompt=T(".prompts:factor_duplicate_system").r(), ) for _ in range(10): extract_result_resp = session.build_chat_completion( diff --git a/rdagent/scenarios/qlib/prompts.yaml b/rdagent/scenarios/qlib/prompts.yaml index c6b9baff..3838899d 100644 --- a/rdagent/scenarios/qlib/prompts.yaml +++ b/rdagent/scenarios/qlib/prompts.yaml @@ -1,91 +1,120 @@ hypothesis_and_feedback: |- - {% for experiment, feedback in trace.hist[-10:] %} - Hypothesis {{ loop.index }}: {{ experiment.hypothesis }} - - Corresponding Code (that leads to the difference in performance): - {% for workspace in experiment.sub_workspace_list %} - {% if workspace is not none %} - Workspace {{loop.index}}: - {{workspace.all_codes}}{% endif %}{% endfor %} - - Observation on the result with the hypothesis: {{ feedback.observations }} - Feedback on the original hypothesis: {{ feedback.hypothesis_evaluation }} - New Feedback for Context (For you to agree or improve upon): {{ feedback.new_hypothesis }} - Reasoning for new hypothesis: {{ feedback.reason }} - Did changing to this hypothesis work? (focus on the change): {{ feedback.decision }} + ========================================================= + {% for experiment, feedback in trace.hist %} + # Trial {{ loop.index }}: + ## Hypothesis + {{ experiment.hypothesis }} + ## Specific task: + {% for task in experiment.sub_tasks %} + {% if task is not none and task.get_task_brief_information is defined %} + {{ task.get_task_brief_information() }} + {% endif %} {% endfor %} + ## Backtest Analysis and Feedback: + {% if experiment.result is not none %} + Backtest Result: {{ experiment.result.loc[["IC", "1day.excess_return_without_cost.annualized_return", "1day.excess_return_without_cost.max_drawdown"]] }} + {% endif %} + Observation: {{ feedback.observations }} + Hypothesis Evaluation: {{ feedback.hypothesis_evaluation }} + Decision (Whether the hypothesis was successful): {{ feedback.decision }} + ========================================================= + {% endfor %} + +last_hypothesis_and_feedback: |- + ## Hypothesis + {{ experiment.hypothesis }} + ## Specific task: + {% for task in experiment.sub_tasks %} + {% if task is not none and task.get_task_brief_information is defined %} + {{ task.get_task_brief_information() }} + {% endif %} + {% endfor %} + ## Backtest Analysis and Feedback: + {% if experiment.result is not none %} + Backtest Result: {{ experiment.result.loc[["IC", "1day.excess_return_without_cost.annualized_return", "1day.excess_return_without_cost.max_drawdown"]] }} + {% endif %} + Training Log: + Here, you need to focus on analyzing whether there are any issues with the training. If any problems are identified, you must correct them in the next iteration and clearly describe how the changes will be made in the hypothesis. + {{ experiment.stdout }} + Observation: {{ feedback.observations }} + Evaluation: {{ feedback.hypothesis_evaluation }} + Decision (Whether this experiment is SOTA): {{ feedback.decision }} + New Hypothesis (Given in feedback stage, just for reference, and can be accepted or rejected in the next round): {{ feedback.new_hypothesis }} + Reasoning (Justification for the new hypothesis): {{ feedback.reason }} + +sota_hypothesis_and_feedback: |- + ## Hypothesis + {{ experiment.hypothesis }} + ## Specific task: + {% for task in experiment.sub_tasks %} + {% if task is not none and task.get_task_brief_information is defined %} + {{ task.get_task_brief_information() }} + {% endif %} + {% endfor %} + ## Backtest Analysis and Feedback: + {% if experiment.result is not none %} + Backtest Result: {{ experiment.result.loc[["IC", "1day.excess_return_without_cost.annualized_return", "1day.excess_return_without_cost.max_drawdown"]] }} + {% endif %} + Training Log: {{ experiment.stdout }} + Observation: {{ feedback.observations }} + Evaluation: {{ feedback.hypothesis_evaluation }} + Decision (Whether this experiment is SOTA): {{ feedback.decision }} hypothesis_output_format: |- The output should follow JSON format. The schema is as follows: { - "hypothesis": "The new hypothesis generated based on the information provided.", - "reason": "The reason why you generate this hypothesis. It should be comprehensive and logical. It should cover the other keys below and extend them.", - "concise_reason": "Two-line summary. First line focuses on a concise justification for the change. Second line generalizes a knowledge statement.", - "concise_observation": "One line summary. It focuses on the observation of the given scenario, data characteristics, or previous experiences (failures & succeses).", - "concise_justification": "One line summary. Justify the hypothesis based on theoretical principles or initial assumptions.", - "concise_knowledge": "One line summary. Transferable knowledge based on theoretical principles. Use conditional grammar. eg. "If...., ..; When..., .; and etc" Make sure that you state things clearly without ambiguity. Eg. avoid saying "previous hypothesis", because one wouldn't know what that is." + "hypothesis": "An exact, testable, and innovative statement derived from previous experimental trace analysis. Avoid overly general ideas and ensure precision. The hypothesis should clearly specify the exact approach and expected improvement in performance in two or three sentences.", + "reason": "Provide a clear, logical explanation for why this hypothesis was proposed, grounded in evidence (e.g., trace history, domain principles). Reason should be short with no more than two sentences.", } +factor_hypothesis_output_format: |- + The output should follow JSON format. The schema is as follows: + { + "hypothesis": "The new hypothesis generated based on the information provided. Limit in two or three sentences.", + "reason": "The reason why you generate this hypothesis. It should be comprehensive and logical. It should cover the other keys below and extend them. Limit in two or three sentences.", + } +hypothesis_output_format_with_action: |- + The output should follow JSON format. The schema is as follows: + { + "action": "If `hypothesis_specification` provides the action you need to take, please follow "hypothesis_specification" to choose the action. Otherwise, based on previous experimental results, suggest the action you believe is most appropriate at the moment. It should be one of [`factor`, `model`].", + "hypothesis": "The new hypothesis generated based on the information provided.", + "reason": "The reason why you generate this hypothesis. It should be comprehensive and logical. It should cover the other keys below and extend them. Limit in two or three sentences.", + } model_hypothesis_specification: |- - Additional Specifications: - - Hypotheses should grow and evolve based on the previous hypothesis. If there is no previous hypothesis, start with something simple. Gradually Build Up Upon previous hypothesis & feedbacks. In each round, hypothesis is different. Pay attention to your previous hypothesis. - - Ensure that the hypothesis focuses on the architecture of a PyTorch model. Each hypothesis should address specific architectural choices such as the type of layers, activation functions, regularization techniques, and the overall structure of the model. Avoid hypotheses related to input features or optimization processes. - - Remember: if there is no hypothesis, start with something simple like MLP. - - Usually, a larger model works better than a smaller one. - - Logic for generating a new hypothesis: If the previous hypothesis works, try to inherit from it and grow deeper. If the previous hypotheis doesn't work, try to make changes in the current level. - - Sample hypothesis evolution loop: (This is the entire loop, see what stage you are at. We want hypothesis to continue growing.) Levels include **Model Type**, **Layer Configuration**, **Activation Functions**, **Regularization Techniques** - - 1st Round Hypothesis: The model should be a CNN. - - 2nd Round Hypothesis (If first round worked: CNN is the model type level, which means that we should extend to the next level, like layer configuration): The model should be a CNN. The CNN should have 5 convolutional layers. (Reasoning: As CNN worked, we now specify the layers specification to grow the hypothesis deeper.) - - 3rd Round Hypothesis (If second round didn't work): The model should be a CNN. The CNN should have 3 convolutional layers. (Reasoning: As 5-layer structure didn't work in the 2nd round hypothesis, try something else within the layer configuration level.) - - 4th Round Hypothesis (If third round worked): The model should be a CNN. The CNN should have 3 convolutional layers. Use Leaky ReLU activation for all layers. (As last round worked, now proceed to the next level: activation functions) - - 5th Round Hypothesis (If fourth round worked): The model should be a CNN. The CNN should have 3 convolutional layers. Use Leaky ReLU activation for all layers. Use dropout regularization with a rate of 0.5. (Similar Reasoning & Continuing to Grow to the dropout setup) - - 6th Round Hypothesis (If fourth round didn't work): The model should be a CNN. The CNN should have 5 convolutional layers. Use Leaky ReLU activation for all layers. Use dropout regularization with a rate of 0.3. (Reasoning: As regularisation rate of 0.5 didn't work, we only change a new regularisation and keep the other elements that worked. This means making changes in the current level.) + 1. First, observe and analyze the overall experimental progression in `hypothesis_and_feedback`. Analyze where the previous model designs were inadequate — whether it was due to parameter settings, architectural flaws, or a lack of novelty (proposing entirely new concepts is highly encouraged as long as they demonstrate effectiveness). + 2. Second, `last_hypothesis_and_feedback` and `sota_hypothesis_and_feedback` are key references you should pay close attention to. You can choose to optimize based on either of them or generate new ideas to form hypotheses and experiments. + 3. If there is no prior experiment or result available at the beginning, you can start by implementing a simple and small architecture. + 4. If a series of attempts fail to achieve SOTA, consider exploring entirely new directions; at this point, it is acceptable to return to simple architectures. + 5. Focus exclusively on the architecture of PyTorch models. Each hypothesis should specifically address architectural decisions, such as layer configurations, activation functions, regularization methods, and overall model structure. DO NOT do any feature-specific processing. Instead, you can propose innovative transformations on the input time-series data to enhance model training effectiveness. + 6. Avoid including aspects unrelated to architecture, such as input features or optimization strategies. + 7. Sometimes, when training performance is poor, adjusting hyperparameters can also be an effective strategy for improvement. + 8. Use standard libraries for baseline models, but also explore custom architecture designs to investigate novel structures. After sufficient trials with traditional models, aim for innovation comparable to top-tier AI conferences (NeurIPS, ICLR, ICML, SIGKDD, etc.) in time series modeling. factor_hypothesis_specification: |- - 1. **Type of Factor and Financial Trends:** - - Define the type of factor introduced. - - Explain the financial trends or market behaviors indicated by this factor. - - Omit unnecessary or redundant details. - + 1. **1-5 Factors per Generation:** + - Ensure each generation produces 1-5 factors. + - Balance simplicity and complexity to build a robust factor library. + - Make full use of the financial data provided to you instead of focusing solely on a specific field. 2. **Simple and Effective Factors First:** - - Start with factors that are simple and likely effective. + - Start with factors that are simple, easy to achieve and likely effective. - Concisely explain why these factors are expected to work. - Avoid complex or combined factors initially. - 3. **Gradual Complexity Increase:** - - Introduce more complex factors as more experimental results are gathered. - - Discuss potential advantages and complexities. + - Introduce more complex factors (e.g. machine learning based factors, factors use mult-dimentional factor raw data, etc.) as more experimental results are gathered. - Combine factors only after simpler ones are tested and validated. - 4. **New Directions and Optimizations:** - - If a new direction is needed, explain why based on financial principles, economic theories, or market behaviors. - - Suggest only one new direction at a time for clarity. - - If a previous hypothesis did not surpass SOTA but seems optimizable, you may continue in the same direction. + - If multiple consecutive iterations fail to produce factors surpassing SOTA, consider switching to a new direction and can starting with simple factors again. + - If optimizing a specific type of factor, proceed from simple to complex. + 5. Note - Highlight that factors surpassing SOTA are included in the library to avoid re-implementation. - 5. **1-3 Factors per Generation:** - - Ensure each generation produces 1-3 factors. - - Balance simplicity and complexity to build a robust factor library. - factor_experiment_output_format: |- The output should follow JSON format. The schema is as follows: { "factor name 1": { - "description": "description of factor 1", + "description": "description of factor 1, start with its type, e.g. [Momentum Factor]", "formulation": "latex formulation of factor 1", "variables": { "variable or function name 1": "description of variable or function 1", @@ -93,7 +122,7 @@ factor_experiment_output_format: |- } }, "factor name 2": { - "description": "description of factor 1", + "description": "description of factor 2, start with its type, e.g. [Machine Learning based Factor]", "formulation": "latex formulation of factor 2", "variables": { "variable or function name 1": "description of variable or function 1", @@ -105,9 +134,9 @@ factor_experiment_output_format: |- model_experiment_output_format: |- So far please only design one model to test the hypothesis! - The output should follow JSON format. The schema is as follows: + The output should follow JSON format. The schema is as follows (value in training_hyperparameters is a basic setting for reference, you CAN CHANGE depends on the previous training log): { - "model_name 1 (The name of the model)": { + "model_name (The name of the model)": { "description": "A detailed description of the model", "formulation": "A LaTeX formula representing the model's formulation", "architecture": "A detailed description of the model's architecture, e.g., neural network layers or tree structures", @@ -121,13 +150,16 @@ model_experiment_output_format: |- "hyperparameter_name_2": "value of hyperparameter 2", "hyperparameter_name_3": "value of hyperparameter 3" }, + "training_hyperparameters" { # All values are for reference; you can set them yourself + "n_epochs": "100", + "lr": "1e-3", + "early_stop": 10, + "batch_size": 256, + "weight_decay": 1e-4, + } "model_type": "Tabular or TimeSeries" # Should be one of "Tabular" or "TimeSeries" }, - "model_name 2 (The name of the model)": { - ... - } } - Usually a larger model works better than a smaller one. Hence, the parameters should be larger. factor_feedback_generation: system: |- @@ -141,28 +173,23 @@ factor_feedback_generation: Please understand the following operation logic and then make your feedback that is suitable for the scenario: 1. Logic Explanation: - - If the previous hypothesis factor surpasses the SOTA, include this factor in the SOTA factor library. - - New experiments will generate new factors, which will be combined with the factors in the SOTA library. - - These combined factors will be backtested and compared against the current SOTA to continuously iterate. + a) All factors that have surpassed SOTA in previous attempts will be included in the SOTA factor library. + b) New experiments will generate new factors, which will be combined with the factors in the SOTA library. + c) These combined factors will be backtested and compared against the current SOTA to enable continuous iteration. 2. Development Directions: - - New Direction: - - Propose a new factor direction for exploration and development. - - Optimization of Existing Direction: - - If the previous experiment's factor replaced the SOTA, suggest further improvements to that factor. - - Clearly specify the differences in name and improvements compared to the previous factor. - - Continued Research: - - If the previous experiment's factor did not replace the SOTA, suggest ways to optimize and develop factors in this direction. - 3. Final Goal: - - The ultimate goal is to continuously accumulate factors that surpass each iteration to maintain the best SOTA. + a) New Direction: Propose a new factor direction for exploration and development. + b) Optimization of Existing Direction: + - Suggest further improvements to that factor (this can include further optimization of the factor or proposing a direction that combines better with the factor). + - Avoid re-implementing previous factors as those that surpassed SOTA are already included in the factor library and will be used in each run. + 3. Final Goal: To continuously accumulate factors that surpass each iteration to maintain the best SOTA. - When judging the results: - 1. **Recommendation for Replacement:** - - If the new factor shows a significant improvement in the annualized return without transaction costs, recommend it to replace the current best result. - - If the annualized return and any other single metric are better than SOTA, recommend the replacement. - - Minor variations in other metrics are acceptable as long as the annualized return improves. + When judging the results: + 1. Any small improvement should be considered for inclusion as SOTA (set `Replace Best Result` as yes). + 2. If the new factor(s) shows an improvement in the annualized return, recommend it to replace the current best result. + 3. Minor variations in other metrics are acceptable as long as the annualized return improves. Consider Changing Direction for Significant Gaps with SOTA: - - If the new results significantly differ from the SOTA, consider exploring a new direction. + - If the new results significantly differ from the SOTA, consider exploring a new direction (write new type factors). - Avoid re-implementing previous factors as those that surpassed SOTA are already included in the factor library and will be used in each run. Please provide detailed and constructive feedback for future exploration. @@ -193,80 +220,80 @@ factor_feedback_generation: Analyze the combined result in the context of its ability to: 1. Support or refute the hypothesis. 2. Show improvement or deterioration compared to the SOTA experiment. - - Evaluation Metrics Explanations: - Below are the financial meanings of each metric, which should be used to judge the results: - - - 1day.excess_return_without_cost.max_drawdown: Measures the maximum loss from a peak to a trough without considering transaction costs. (the smaller the better) - - 1day.excess_return_without_cost.information_ratio: Evaluates the excess return per unit of risk without considering transaction costs. (the bigger the better) - - 1day.excess_return_without_cost.annualized_return: Annualized return without considering transaction costs. (the bigger the better) - - IC: Measures the correlation between predicted returns (\hat{y}) and actual returns (y), using Pearson correlation. (the bigger the better) - - When judging the results: - 1. **Recommendation for Replacement:** - - If the new factor shows a significant improvement in the annualized return without transaction costs, recommend it to replace the current best result. - - If the annualized return and any other single metric are better than SOTA, recommend the replacement. - - Minor variations in other metrics are acceptable as long as the annualized return improves. - - Consider Changing Direction for Significant Gaps with SOTA: - - If the new results significantly differ from the SOTA, consider exploring a new direction. - - Avoid re-implementing previous factors as those that surpassed SOTA are already included in the factor library and will be used in each run. - + Note: Only factors with 'Factor Implementation' as True are implemented and tested in this experiment. If 'Factor Implementation' is False, the hypothesis for that factor cannot be verified in this run. model_feedback_generation: system: |- - You are a professional result analysis assistant. You will receive a result and a hypothesis. - Your task is to provide feedback on how well the result supports or refutes the hypothesis by judging from the observation of performance increase or decrease. - Please provide detailed and constructive feedback. Note that as hypothesis evolve, a general trend should be that the model grows larger. + You are a professional quantitative analysis assistant in top-tier hedge fund. + + The task is described in the following scenario: + {{ scenario }} + + You will receive a quantitative model hypothesis, its specific task description, and it market backtest result. + Your feedback should specify whether the current result supports or refutes the hypothesis, compare it with previous SOTA results, examine the model's training logs to analyze whether there are issues with hyperparameter settings, and suggest improvements or new directions. + + Please provide detailed and constructive feedback. Example JSON Structure for Result Analysis: { - "Observations": "Your overall observations here", - "Feedback for Hypothesis": "Observations related to the hypothesis", - "New Hypothesis": "Put your new hypothesis here.", - "Reasoning": "Provide reasoning for the hypothesis here.", + "Observations": "First analyze the model's training logs to determine whether there are any issues with its parameter settings. Then clearly summarize the current results and the SOTA results with exact scores and any notable patterns. Limit your summary to no more than three concise, data-focused sentences.", + "Feedback for Hypothesis": "Explicitly confirm or refute the hypothesis based on specific data points or performance trends. Limit to two sentences.", + "New Hypothesis": "Propose a revised hypothesis, considering observed patterns and limitations in the current one. Limit to no more than two sentences.", + "Reasoning": "Explain the rationale for the new hypothesis using specific trends or performance shifts. Be concise but technically complete. Limit to two sentences.", "Decision": , } - Focus on the changes in hypothesis and justify why do hypothesis evolve like this. Also, increase complexity as the hypothesis evolves (give more layers, more neurons, and etc) - - Logic for generating a new hypothesis: If the previous hypothesis works, try to inherit from it and grow deeper. If the previous hypotheis doesn't work, try to make changes in the current level. - - Sample hypothesis evolution loop: (This is the entire loop, see what stage you are at. We want hypothesis to continue growing.) Levels include **Model Type**, **Layer Configuration**, **Activation Functions**, **Regularization Techniques** - - 1st Round Hypothesis: The model should be a CNN. - - 2nd Round Hypothesis (If first round worked: CNN is the model type level, which means that we should extend to the next level, like layer configuration): The model should be a CNN. The CNN should have 5 convolutional layers. (Reasoning: As CNN worked, we now specify the layers specification to grow the hypothesis deeper.) - - 3rd Round Hypothesis (If second round didn't work): The model should be a CNN. The CNN should have 3 convolutional layers. (Reasoning: As 5-layer structure didn't work in the 2nd round hypothesis, try something else within the layer configuration level.) - - 4th Round Hypothesis (If third round worked): The model should be a CNN. The CNN should have 3 convolutional layers. Use Leaky ReLU activation for all layers. (As last round worked, now proceed to the next level: activation functions) - - 5th Round Hypothesis (If fourth round worked): The model should be a CNN. The CNN should have 3 convolutional layers. Use Leaky ReLU activation for all layers. Use dropout regularization with a rate of 0.5. (Similar Reasoning & Continuing to Grow to the dropout setup) - - 6th Round Hypothesis (If fourth round didn't work): The model should be a CNN. The CNN should have 5 convolutional layers. Use Leaky ReLU activation for all layers. Use dropout regularization with a rate of 0.3. (Reasoning: As regularisation rate of 0.5 didn't work, we only change a new regularisation and keep the other elements that worked. This means making changes in the current level.) - user: |- - We are in an experiment of finding hypothesis and validating or rejecting them so that in the end we have a powerful model generated. - Here are the context: {{context}}. - - {% if last_hypothesis %} - Last Round Information: - Hypothesis: {{last_hypothesis.hypothesis}} - Task: {{last_task}} - Code Implemented: {{last_code}} - Result: {{last_result}} + {% if sota_hypothesis %} + # SOTA Round Information: + Hypothesis: {{ sota_hypothesis.hypothesis }} + Specific Task: {{ sota_task }} + Code Implementation: {{ sota_code }} + Result: {{ sota_result }} {% else %} - This is the first round. No previous information available. As long as the performance is not too negative (eg.ICIR is greater than 0), treat it as successful. Do not set the threshold too high. + # This is the first round. No previous information available. As long as the performance is not too negative (eg.ICIR is greater than 0), treat it as successful. Do not set the threshold too high. {% endif %} - Now let's come to this round. You will receive the result and you will evaluate if the performance increases or decreases. - Hypothesis: {{hypothesis.hypothesis}} - Experiment Setup: {{exp.sub_tasks[0]}} - Code Implemented: {{exp.sub_workspace_list[0].file_dict.get("model.py")}} - Relevant Reasoning: {{hypothesis.reason}} - Result: {{exp.result}} + # Current Round Information: + Hypothesis: {{ hypothesis.hypothesis }} + Why propose this hypothesis: {{ hypothesis.reason }} + Specific Task: {{ exp.sub_tasks[0].get_task_information() }} + Code Implementation: {{ exp.sub_workspace_list[0].file_dict.get("model.py") }} + Training Log: {{ exp.stdout }} + Result: {{ exp_result }} - Compare and observe. Which result has a better return and lower risk? If the performance increases, the hypothesis should be considered positive (working). - Hence, with the hypotheses, relevant reasoning, and results in mind (comparison), provide detailed and constructive feedback and suggest a new hypothesis. + # When judging the results: + 1. **Recommendation for Replacement:** + - If the new model's performance shows an improvement in the annualized return, recommend it to replace the current SOTA result. + - Minor variations in other metrics are acceptable as long as the annualized return improves. + 2. Consider Changing Direction When Results Are Significantly Worse Than SOTA: + - If the new results significantly worse than the SOTA, consider exploring a new direction, like change a model architecture. + +action_gen: + system: |- + Quantitative investment is a data-driven approach to asset management that relies on mathematical models, statistical techniques, and computational methods to analyze financial markets and make investment decisions. Two essential components of this approach are factors and models. + + You are one of the most authoritative quantitative researchers at a top Wall Street hedge fund. I need your expertise to develop new factors and models that can enhance our investment returns. Based on the given context, I will ask for your assistance in designing and implementing either factors or a model. + + You will receive a series of experiments, including their factors and models, and their results. + Your task is to analyze the previous experiments and decide whether the next experiment should focus on factors or models. + + Example JSON Structure for your return: + { + "action": "factor" or "model", # You must choose one of the two + } + + user: |- + {% if hypothesis_and_feedback|length == 0 %} + It is the first round of hypothesis generation. The user has no hypothesis on this scenario yet. + {% else %} + The former hypothesis and the corresponding feedbacks are as follows: + {{ hypothesis_and_feedback }} + {% endif %} + + + {% if last_hypothesis_and_feedback != "" %} + Here is the last trial's hypothesis and the corresponding feedback. The main feedback includes a new hypothesis for your reference only. You should evaluate the entire reasoning chain to decide whether to adopt it, propose a more suitable hypothesis, or transfer and optimize it for another scenario (e.g., factor/model), since transfers are generally encouraged: + {{ last_hypothesis_and_feedback }} + {% endif %} \ No newline at end of file diff --git a/rdagent/scenarios/qlib/proposal/bandit.py b/rdagent/scenarios/qlib/proposal/bandit.py new file mode 100644 index 00000000..f5584974 --- /dev/null +++ b/rdagent/scenarios/qlib/proposal/bandit.py @@ -0,0 +1,109 @@ +import json +import math +from dataclasses import dataclass +from pathlib import Path +from typing import List, Literal, Tuple + +import numpy as np + + +@dataclass +class Metrics: + ic: float = 0.0 + icir: float = 0.0 + rank_ic: float = 0.0 + rank_icir: float = 0.0 + arr: float = 0.0 + ir: float = 0.0 + mdd: float = 0.0 + sharpe: float = 0.0 + + def as_vector(self) -> np.ndarray: + return np.array( + [ + self.ic, + self.icir, + self.rank_ic, + self.rank_icir, + self.arr, + self.ir, + -self.mdd, + self.sharpe, + ] + ) + + +def extract_metrics_from_experiment(experiment) -> Metrics: + """Extract metrics from experiment feedback""" + try: + result = experiment.result + ic = result.get("IC", 0.0) + icir = result.get("ICIR", 0.0) + rank_ic = result.get("Rank IC", 0.0) + rank_icir = result.get("Rank ICIR", 0.0) + arr = result.get("1day.excess_return_with_cost.annualized_return ", 0.0) + ir = result.get("1day.excess_return_with_cost.information_ratio", 0.0) + mdd = result.get("1day.excess_return_with_cost.max_drawdown", 1.0) # Avoid division by zero + sharpe = arr / -mdd if mdd != 0 else 0.0 + + return Metrics(ic=ic, icir=icir, rank_ic=rank_ic, rank_icir=rank_icir, arr=arr, ir=ir, mdd=mdd, sharpe=sharpe) + except Exception as e: + print(f"Error extracting metrics: {e}") + return Metrics() + + +class LinearThompsonTwoArm: + def __init__(self, dim: int, prior_var: float = 1.0, noise_var: float = 1.0): + self.dim = dim + self.noise_var = noise_var + # Each arm has its own posterior: mean & inverse of covariance (precision matrix) + self.mean = { + "factor": np.zeros(dim), + "model": np.zeros(dim), + } + self.precision = { + "factor": np.eye(dim) / prior_var, + "model": np.eye(dim) / prior_var, + } + + def sample_reward(self, arm: str, x: np.ndarray) -> float: + P = self.precision[arm] + P = 0.5 * (P + P.T) + + eps = 1e-6 + try: + cov = np.linalg.inv(P + eps * np.eye(self.dim)) + L = np.linalg.cholesky(cov) + z = np.random.randn(self.dim) + w_sample = self.mean[arm] + L @ z + except np.linalg.LinAlgError: + w_sample = self.mean[arm] + + return float(np.dot(w_sample, x)) + + def update(self, arm: str, x: np.ndarray, r: float) -> None: + P = self.precision[arm] + P += np.outer(x, x) / self.noise_var + self.precision[arm] = P + self.mean[arm] = np.linalg.solve(P, P @ self.mean[arm] + (r / self.noise_var) * x) + + def next_arm(self, x: np.ndarray) -> str: + scores = {arm: self.sample_reward(arm, x) for arm in ("factor", "model")} + return max(scores, key=scores.get) + + +class EnvController: + def __init__(self, weights: Tuple[float, ...] = None) -> None: + self.weights = np.asarray(weights or (0.1, 0.1, 0.05, 0.05, 0.25, 0.15, 0.1, 0.2)) + self.bandit = LinearThompsonTwoArm(dim=8, prior_var=10.0, noise_var=0.5) + + def reward(self, m: Metrics) -> float: + return float(np.dot(self.weights, m.as_vector())) + + def decide(self, m: Metrics) -> str: + x = m.as_vector() + return self.bandit.next_arm(x) + + def record(self, m: Metrics, arm: str) -> None: + r = self.reward(m) + self.bandit.update(arm, m.as_vector(), r) diff --git a/rdagent/scenarios/qlib/proposal/factor_proposal.py b/rdagent/scenarios/qlib/proposal/factor_proposal.py index 80df3eba..0ff088dd 100644 --- a/rdagent/scenarios/qlib/proposal/factor_proposal.py +++ b/rdagent/scenarios/qlib/proposal/factor_proposal.py @@ -1,16 +1,13 @@ import json -from pathlib import Path from typing import List, Tuple -from jinja2 import Environment, StrictUndefined - from rdagent.components.coder.factor_coder.factor import FactorExperiment, FactorTask from rdagent.components.proposal import FactorHypothesis2Experiment, FactorHypothesisGen -from rdagent.core.prompts import Prompts from rdagent.core.proposal import Hypothesis, Scenario, Trace from rdagent.scenarios.qlib.experiment.factor_experiment import QlibFactorExperiment - -prompt_dict = Prompts(file_path=Path(__file__).parent.parent / "prompts.yaml") +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 QlibFactorHypothesis = Hypothesis @@ -21,49 +18,68 @@ class QlibFactorHypothesisGen(FactorHypothesisGen): def prepare_context(self, trace: Trace) -> Tuple[dict, bool]: hypothesis_and_feedback = ( - ( - Environment(undefined=StrictUndefined) - .from_string(prompt_dict["hypothesis_and_feedback"]) - .render(trace=trace) + T("scenarios.qlib.prompts:hypothesis_and_feedback").r( + trace=trace, ) if len(trace.hist) > 0 else "No previous hypothesis and feedback available since it's the first round." ) + 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, - "RAG": None, - "hypothesis_output_format": prompt_dict["hypothesis_output_format"], - "hypothesis_specification": prompt_dict["factor_hypothesis_specification"], + "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["hypothesis"], - reason=response_dict["reason"], - concise_reason=response_dict["concise_reason"], - concise_observation=response_dict["concise_observation"], - concise_justification=response_dict["concise_justification"], - concise_knowledge=response_dict["concise_knowledge"], + 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]: - scenario = trace.scen.get_scenario_all_desc() - experiment_output_format = prompt_dict["factor_experiment_output_format"] + 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() - hypothesis_and_feedback = ( - ( - Environment(undefined=StrictUndefined) - .from_string(prompt_dict["hypothesis_and_feedback"]) - .render(trace=trace) - ) - if len(trace.hist) > 0 - else "No previous hypothesis and feedback available since it's the first round." - ) + if len(trace.hist) == 0: + hypothesis_and_feedback = "No previous hypothesis and feedback available since it's the first round." + else: + specific_trace = Trace(trace.scen) + for i in range(len(trace.hist) - 1, -1, -1): # Reverse iteration + if not hasattr(trace.hist[i][0].hypothesis, "action") or trace.hist[i][0].hypothesis.action == "factor": + specific_trace.hist.insert(0, trace.hist[i]) + if len(specific_trace.hist) > 0: + specific_trace.hist.reverse() + hypothesis_and_feedback = T("scenarios.qlib.prompts:hypothesis_and_feedback").r( + trace=specific_trace, + ) + else: + hypothesis_and_feedback = "No previous hypothesis and feedback available." experiment_list: List[FactorExperiment] = [t[0] for t in trace.hist] @@ -105,6 +121,8 @@ class QlibFactorHypothesis2Experiment(FactorHypothesis2Experiment): 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 diff --git a/rdagent/scenarios/qlib/proposal/model_proposal.py b/rdagent/scenarios/qlib/proposal/model_proposal.py index 81ee91ad..133017d5 100644 --- a/rdagent/scenarios/qlib/proposal/model_proposal.py +++ b/rdagent/scenarios/qlib/proposal/model_proposal.py @@ -1,16 +1,12 @@ import json -from pathlib import Path from typing import List, Tuple -from jinja2 import Environment, StrictUndefined - from rdagent.components.coder.model_coder.model import ModelExperiment, ModelTask from rdagent.components.proposal import ModelHypothesis2Experiment, ModelHypothesisGen -from rdagent.core.prompts import Prompts from rdagent.core.proposal import Hypothesis, Scenario, Trace from rdagent.scenarios.qlib.experiment.model_experiment import QlibModelExperiment - -prompt_dict = Prompts(file_path=Path(__file__).parent.parent / "prompts.yaml") +from rdagent.scenarios.qlib.experiment.quant_experiment import QlibQuantScenario +from rdagent.utils.agent.tpl import T QlibModelHypothesis = Hypothesis @@ -21,50 +17,109 @@ class QlibModelHypothesisGen(ModelHypothesisGen): def prepare_context(self, trace: Trace) -> Tuple[dict, bool]: hypothesis_and_feedback = ( - ( - Environment(undefined=StrictUndefined) - .from_string(prompt_dict["hypothesis_and_feedback"]) - .render(trace=trace) + T("scenarios.qlib.prompts:hypothesis_and_feedback").r( + trace=trace, ) if len(trace.hist) > 0 else "No previous hypothesis and feedback available since it's the first round." ) + + 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." + ) + + sota_hypothesis_and_feedback = "" + if len(trace.hist) == 0: + sota_hypothesis_and_feedback = "No SOTA hypothesis and feedback available since it is the first round." + else: + for i in range(len(trace.hist) - 1, -1, -1): + if trace.hist[i][1].decision: + sota_hypothesis_and_feedback = T("scenarios.qlib.prompts:sota_hypothesis_and_feedback").r( + experiment=trace.hist[i][0], feedback=trace.hist[i][1] + ) + break + else: + sota_hypothesis_and_feedback = ( + "No SOTA hypothesis and feedback available since previous experiments were not accepted." + ) + context_dict = { "hypothesis_and_feedback": hypothesis_and_feedback, - "RAG": "In Quantitative Finance, market data could be time-series, and GRU model/LSTM model are suitable for them. Do not generate GNN model as for now.", - "hypothesis_output_format": prompt_dict["hypothesis_output_format"], - "hypothesis_specification": prompt_dict["model_hypothesis_specification"], + "last_hypothesis_and_feedback": last_hypothesis_and_feedback, + "SOTA_hypothesis_and_feedback": sota_hypothesis_and_feedback, + "RAG": "1. In Quantitative Finance, market data could be time-series, and GRU model/LSTM model are suitable for them. Do not generate GNN model as for now.\n2. The training data consists of less than 1 million samples for the training set and approximately 250,000 samples for the validation set. Please design the hyperparameters accordingly and control the model size. This has a significant impact on the training results. If you believe that the previous model itself is good but the training hyperparameters or model hyperparameters are not optimal, you can return the same model and adjust these parameters instead.", + "hypothesis_output_format": T("scenarios.qlib.prompts:hypothesis_output_format").r(), + "hypothesis_specification": T("scenarios.qlib.prompts:model_hypothesis_specification").r(), } return context_dict, True def convert_response(self, response: str) -> Hypothesis: response_dict = json.loads(response) hypothesis = QlibModelHypothesis( - hypothesis=response_dict["hypothesis"], - reason=response_dict["reason"], - concise_reason=response_dict["concise_reason"], - concise_observation=response_dict["concise_observation"], - concise_justification=response_dict["concise_justification"], - concise_knowledge=response_dict["concise_knowledge"], + 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 QlibModelHypothesis2Experiment(ModelHypothesis2Experiment): def prepare_context(self, hypothesis: Hypothesis, trace: Trace) -> Tuple[dict, bool]: - scenario = trace.scen.get_scenario_all_desc() - experiment_output_format = prompt_dict["model_experiment_output_format"] + if isinstance(trace.scen, QlibQuantScenario): + scenario = trace.scen.get_scenario_all_desc(action="model") + else: + scenario = trace.scen.get_scenario_all_desc() + experiment_output_format = T("scenarios.qlib.prompts:model_experiment_output_format").r() - hypothesis_and_feedback = ( - ( - Environment(undefined=StrictUndefined) - .from_string(prompt_dict["hypothesis_and_feedback"]) - .render(trace=trace) + last_experiment = None + last_feedback = None + sota_experiment = None + sota_feedback = None + + if len(trace.hist) == 0: + hypothesis_and_feedback = "No previous hypothesis and feedback available since it's the first round." + else: + specific_trace = Trace(trace.scen) + for i in range(len(trace.hist) - 1, -1, -1): # Reverse iteration + if not hasattr(trace.hist[i][0].hypothesis, "action") or trace.hist[i][0].hypothesis.action == "model": + if last_experiment is None: + last_experiment = trace.hist[i][0] + last_feedback = trace.hist[i][1] + if trace.hist[i][1].decision is True and sota_experiment is None: + sota_experiment = trace.hist[i][0] + sota_feedback = trace.hist[i][1] + specific_trace.hist.insert(0, trace.hist[i]) + if len(specific_trace.hist) > 0: + specific_trace.hist.reverse() + hypothesis_and_feedback = T("scenarios.qlib.prompts:hypothesis_and_feedback").r( + trace=specific_trace, + ) + else: + hypothesis_and_feedback = "No previous hypothesis and feedback available." + + last_hypothesis_and_feedback = ( + T("scenarios.qlib.prompts:last_hypothesis_and_feedback").r( + experiment=last_experiment, feedback=last_feedback ) - if len(trace.hist) > 0 + if last_experiment is not None else "No previous hypothesis and feedback available since it's the first round." ) + sota_hypothesis_and_feedback = ( + T("scenarios.qlib.prompts:sota_hypothesis_and_feedback").r( + experiment=sota_experiment, feedback=sota_feedback + ) + if sota_experiment is not None + else "No SOTA hypothesis and feedback available since previous experiments were not accepted." + ) + experiment_list: List[ModelExperiment] = [t[0] for t in trace.hist] model_list = [] @@ -75,9 +130,11 @@ class QlibModelHypothesis2Experiment(ModelHypothesis2Experiment): "target_hypothesis": str(hypothesis), "scenario": scenario, "hypothesis_and_feedback": hypothesis_and_feedback, + "last_hypothesis_and_feedback": last_hypothesis_and_feedback, + "SOTA_hypothesis_and_feedback": sota_hypothesis_and_feedback, "experiment_output_format": experiment_output_format, "target_list": model_list, - "RAG": None, + "RAG": "Note, the training data consists of less than 1 million samples for the training set and approximately 250,000 samples for the validation set. Please design the hyperparameters accordingly and control the model size. This has a significant impact on the training results. If you believe that the previous model itself is good but the training hyperparameters or model hyperparameters are not optimal, you can return the same model and adjust these parameters instead.", }, True def convert_response(self, response: str, hypothesis: Hypothesis, trace: Trace) -> ModelExperiment: @@ -89,6 +146,7 @@ class QlibModelHypothesis2Experiment(ModelHypothesis2Experiment): architecture = response_dict[model_name]["architecture"] variables = response_dict[model_name]["variables"] hyperparameters = response_dict[model_name]["hyperparameters"] + training_hyperparameters = response_dict[model_name]["training_hyperparameters"] model_type = response_dict[model_name]["model_type"] tasks.append( ModelTask( @@ -98,6 +156,7 @@ class QlibModelHypothesis2Experiment(ModelHypothesis2Experiment): architecture=architecture, variables=variables, hyperparameters=hyperparameters, + training_hyperparameters=training_hyperparameters, model_type=model_type, ) ) diff --git a/rdagent/scenarios/qlib/proposal/quant_proposal.py b/rdagent/scenarios/qlib/proposal/quant_proposal.py new file mode 100644 index 00000000..a31bc051 --- /dev/null +++ b/rdagent/scenarios/qlib/proposal/quant_proposal.py @@ -0,0 +1,179 @@ +import json +import random +from typing import Tuple + +from rdagent.app.qlib_rd_loop.conf import QUANT_PROP_SETTING +from rdagent.components.proposal import FactorAndModelHypothesisGen +from rdagent.core.proposal import Hypothesis, Scenario, Trace +from rdagent.oai.llm_utils import APIBackend +from rdagent.scenarios.qlib.proposal.bandit import ( + EnvController, + extract_metrics_from_experiment, +) +from rdagent.utils.agent.tpl import T + + +class QuantTrace(Trace): + def __init__(self, scen: Scenario) -> None: + super().__init__(scen) + # Initialize the controller with default weights + self.controller = EnvController() + + +class QlibQuantHypothesis(Hypothesis): + def __init__( + self, + hypothesis: str, + reason: str, + concise_reason: str, + concise_observation: str, + concise_justification: str, + concise_knowledge: str, + action: str, + ) -> None: + super().__init__( + hypothesis, reason, concise_reason, concise_observation, concise_justification, concise_knowledge + ) + self.action = action + + def __str__(self) -> str: + return f"""Chosen Action: {self.action} +Hypothesis: {self.hypothesis} +Reason: {self.reason} +""" + + +class QlibQuantHypothesisGen(FactorAndModelHypothesisGen): + def __init__(self, scen: Scenario) -> Tuple[dict, bool]: + super().__init__(scen) + + def prepare_context(self, trace: Trace) -> Tuple[dict, bool]: + + # ========= Bandit ========== + if QUANT_PROP_SETTING.action_selection == "bandit": + if len(trace.hist) > 0: + metric = extract_metrics_from_experiment(trace.hist[-1][0]) + prev_action = trace.hist[-1][0].hypothesis.action + trace.controller.record(metric, prev_action) + action = trace.controller.decide(metric) + else: + action = "factor" + # ========= LLM ========== + elif QUANT_PROP_SETTING.action_selection == "llm": + hypothesis_and_feedback = ( + T("scenarios.qlib.prompts:hypothesis_and_feedback").render(trace=trace) + if len(trace.hist) > 0 + else "No previous hypothesis and feedback available since it's the first round." + ) + + 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." + ) + + system_prompt = T("scenarios.qlib.prompts:action_gen.system").r() + user_prompt = T("scenarios.qlib.prompts:action_gen.user").r( + hypothesis_and_feedback=hypothesis_and_feedback, + last_hypothesis_and_feedback=last_hypothesis_and_feedback, + ) + resp = APIBackend().build_messages_and_create_chat_completion(user_prompt, system_prompt, json_mode=True) + + action = json.loads(resp).get("action", "factor") + # ========= random ========== + elif QUANT_PROP_SETTING.action_selection == "random": + action = random.choice(["factor", "model"]) + self.targets = action + + qaunt_rag = None + if action == "factor": + if len(trace.hist) < 6: + qaunt_rag = "Try the easiest and fastest factors to experiment with from various perspectives first." + else: + qaunt_rag = "Now, you need to try factors that can achieve high IC (e.g., machine learning-based factors)! Do not include factors that are similar to those in the SOTA factor library!" + elif action == "model": + qaunt_rag = "1. In Quantitative Finance, market data could be time-series, and GRU model/LSTM model are suitable for them. Do not generate GNN model as for now.\n2. The training data consists of approximately 478,000 samples for the training set and about 128,000 samples for the validation set. Please design the hyperparameters accordingly and control the model size. This has a significant impact on the training results. If you believe that the previous model itself is good but the training hyperparameters or model hyperparameters are not optimal, you can return the same model and adjust these parameters instead.\n" + + if len(trace.hist) == 0: + hypothesis_and_feedback = "No previous hypothesis and feedback available since it's the first round." + else: + specific_trace = Trace(trace.scen) + if action == "factor": + # all factor experiments and the SOTA model experiment + model_inserted = False + for i in range(len(trace.hist) - 1, -1, -1): # Reverse iteration + if trace.hist[i][0].hypothesis.action == "factor": + specific_trace.hist.insert(0, trace.hist[i]) + elif ( + trace.hist[i][0].hypothesis.action == "model" + and trace.hist[i][1].decision is True + and model_inserted == False + ): + specific_trace.hist.insert(0, trace.hist[i]) + model_inserted = True + elif action == "model": + # all model experiments and all SOTA factor experiments + factor_inserted = False + for i in range(len(trace.hist) - 1, -1, -1): # Reverse iteration + if trace.hist[i][0].hypothesis.action == "model": + specific_trace.hist.insert(0, trace.hist[i]) + elif ( + trace.hist[i][0].hypothesis.action == "factor" + and trace.hist[i][1].decision is True + and factor_inserted == False + ): + specific_trace.hist.insert(0, trace.hist[i]) + factor_inserted = True + if len(specific_trace.hist) > 0: + specific_trace.hist.reverse() + hypothesis_and_feedback = T("scenarios.qlib.prompts:hypothesis_and_feedback").r( + trace=specific_trace, + ) + else: + hypothesis_and_feedback = "No previous hypothesis and feedback available." + + last_hypothesis_and_feedback = None + for i in range(len(trace.hist) - 1, -1, -1): + if trace.hist[i][0].hypothesis.action == action: + last_hypothesis_and_feedback = T("scenarios.qlib.prompts:last_hypothesis_and_feedback").r( + experiment=trace.hist[i][0], feedback=trace.hist[i][1] + ) + break + + sota_hypothesis_and_feedback = None + if action == "model": + for i in range(len(trace.hist) - 1, -1, -1): + if trace.hist[i][0].hypothesis.action == "model" and trace.hist[i][1].decision is True: + sota_hypothesis_and_feedback = T("scenarios.qlib.prompts:sota_hypothesis_and_feedback").r( + experiment=trace.hist[i][0], feedback=trace.hist[i][1] + ) + break + + context_dict = { + "hypothesis_and_feedback": hypothesis_and_feedback, + "last_hypothesis_and_feedback": last_hypothesis_and_feedback, + "SOTA_hypothesis_and_feedback": sota_hypothesis_and_feedback, + "RAG": qaunt_rag, + "hypothesis_output_format": T("scenarios.qlib.prompts:hypothesis_output_format_with_action").r(), + "hypothesis_specification": ( + T("scenarios.qlib.prompts:factor_hypothesis_specification").r() + if action == "factor" + else T("scenarios.qlib.prompts:model_hypothesis_specification").r() + ), + } + return context_dict, True + + def convert_response(self, response: str) -> Hypothesis: + response_dict = json.loads(response) + hypothesis = QlibQuantHypothesis( + 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"), + action=response_dict.get("action"), + ) + return hypothesis diff --git a/rdagent/utils/env.py b/rdagent/utils/env.py index 2ea9b1bb..1f3192c8 100644 --- a/rdagent/utils/env.py +++ b/rdagent/utils/env.py @@ -11,6 +11,7 @@ import json import os import pickle import re +import select import shutil import subprocess import time @@ -69,7 +70,7 @@ def pull_image_with_progress(image: str) -> None: class EnvConf(ExtendedBaseSettings): default_entry: str extra_volumes: dict = {} - running_timeout_period: int = 600 # 10 minutes + running_timeout_period: int = 3600 # 10 minutes # helper settings to support transparent; enable_cache: bool = True retry_count: int = 5 # retry count for the docker run @@ -307,6 +308,33 @@ class Env(Generic[ASpecificEnvConf]): """ pass + def dump_python_code_run_and_get_results( + self, + code: str, + dump_file_names: list[str], + local_path: str, + env: dict | None = None, + running_extra_volume: Mapping = MappingProxyType({}), + code_dump_file_py_name: Optional[str] = None, + ) -> tuple[str, list]: + """ + Dump the code into the local path and run the code. + """ + random_file_name = f"{uuid.uuid4()}.py" if code_dump_file_py_name is None else f"{code_dump_file_py_name}.py" + with open(os.path.join(local_path, random_file_name), "w") as f: + f.write(code) + entry = f"python {random_file_name}" + log_output = self.run(entry, local_path, env, running_extra_volume=dict(running_extra_volume)) + results = [] + os.remove(os.path.join(local_path, random_file_name)) + for name in dump_file_names: + if os.path.exists(os.path.join(local_path, f"{name}")): + results.append(pickle.load(open(os.path.join(local_path, f"{name}"), "rb"))) + os.remove(os.path.join(local_path, f"{name}")) + else: + return log_output, [] + return log_output, results + # class EnvWithCache # @@ -339,7 +367,8 @@ class LocalEnv(Env[ASpecificLocalConf]): running_extra_volume: Mapping = MappingProxyType({}), **kwargs: dict, ) -> tuple[str, int]: - # mocking the volumes + + # Handle volume links volumes = {} if self.conf.extra_volumes is not None: for lp, rp in self.conf.extra_volumes.items(): @@ -349,7 +378,6 @@ class LocalEnv(Env[ASpecificLocalConf]): volumes[cache_path] = "/tmp/cache" for lp, rp in running_extra_volume.items(): volumes[lp] = rp - for rp, lp in volumes.items(): link_path = Path(lp) real_path = Path(rp) @@ -359,9 +387,9 @@ class LocalEnv(Env[ASpecificLocalConf]): link_path.unlink() link_path.symlink_to(real_path) + # Setup environment if env is None: env = {} - path = [*self.conf.bin_path.split(":"), "/bin/", "/usr/bin/", *env.get("PATH", "").split(":")] env["PATH"] = ":".join(path) @@ -373,21 +401,68 @@ class LocalEnv(Env[ASpecificLocalConf]): table.add_column("Key", style="bold cyan") table.add_column("Value", style="bold magenta") table.add_row("Entry", entry) - table.add_row("Local Path", local_path) + table.add_row("Local Path", local_path or "") table.add_row("Env", "\n".join(f"{k}:{v}" for k, v in env.items())) table.add_row("Volumes", "\n".join(f"{k}:{v}" for k, v in volumes.items())) print(table) - cwd = None - if local_path: - cwd = Path(local_path).resolve() + cwd = Path(local_path).resolve() if local_path else None + env = {k: str(v) if isinstance(v, int) else v for k, v in env.items()} - result = subprocess.run(entry, cwd=cwd, env={**os.environ, **env}, capture_output=True, text=True, shell=True) - combined_output = result.stderr + result.stdout # Combine stdout and stderr - Console().print(combined_output, markup=False) + process = subprocess.Popen( + entry, + cwd=cwd, + env={**os.environ, **env}, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + shell=True, + bufsize=1, + universal_newlines=True, + ) + + # Setup polling + if process.stdout is None or process.stderr is None: + raise RuntimeError("The subprocess did not correctly create stdout/stderr pipes") + + stdout_fd = process.stdout.fileno() + stderr_fd = process.stderr.fileno() + + poller = select.poll() + poller.register(stdout_fd, select.POLLIN) + poller.register(stderr_fd, select.POLLIN) + + combined_output = "" + while True: + if process.poll() is not None: + break + events = poller.poll(100) + for fd, event in events: + if event & select.POLLIN: + if fd == stdout_fd: + output = process.stdout.readline() + if output: + Console().print(output.strip(), markup=False) + combined_output += output + elif fd == stderr_fd: + error = process.stderr.readline() + if error: + Console().print(error.strip(), markup=False) + combined_output += error + + # Capture any final output + remaining_output, remaining_error = process.communicate() + if remaining_output: + Console().print(remaining_output.strip(), markup=False) + combined_output += remaining_output + if remaining_error: + Console().print(remaining_error.strip(), markup=False) + combined_output += remaining_error + + return_code = process.returncode print(Rule("[bold green]LocalEnv Logs End[/bold green]", style="dark_orange")) - return combined_output, result.returncode + return combined_output, return_code class CondaConf(LocalConf): @@ -439,6 +514,44 @@ class DockerConf(EnvConf): retry_wait_seconds: int = 10 # retry wait seconds for the docker run +class QlibCondaConf(CondaConf): + conda_env_name: str = "rdagent4qlib" + enable_cache: bool = False + default_entry: str = "qrun conf.yaml" + # extra_volumes: dict = {str(Path("~/.qlib/").expanduser().resolve().absolute()): "/root/.qlib/"} + + +class QlibCondaEnv(LocalEnv[QlibCondaConf]): + def prepare(self) -> None: + """Prepare the conda environment if not already created.""" + try: + envs = subprocess.run("conda env list", capture_output=True, text=True, shell=True) + if self.conf.conda_env_name not in envs.stdout: + print(f"[yellow]Conda env '{self.conf.conda_env_name}' not found, creating...[/yellow]") + subprocess.check_call( + f"conda create -y -n {self.conf.conda_env_name} python=3.10", + shell=True, + ) + subprocess.check_call( + f"conda run -n {self.conf.conda_env_name} pip install --upgrade pip cython", + shell=True, + ) + subprocess.check_call( + f"conda run -n {self.conf.conda_env_name} rm -rf qlib; git clone https://github.com/microsoft/qlib.git", + shell=True, + ) + subprocess.check_call( + f"conda run -n {self.conf.conda_env_name} bash -c 'cd qlib && git reset --hard 3e72593b8c985f01979bebcf646658002ac43b00 && make dev .'", + shell=True, + ) + subprocess.check_call( + f"conda run -n {self.conf.conda_env_name} pip install catboost xgboost scipy==1.11.4 tables torch", + shell=True, + ) + except Exception as e: + print(f"[red]Failed to prepare conda env: {e}[/red]") + + class QlibDockerConf(DockerConf): model_config = SettingsConfigDict(env_prefix="QLIB_DOCKER_") @@ -706,33 +819,6 @@ class DockerEnv(Env[DockerConf]): except docker.errors.APIError as e: raise RuntimeError(f"Error while running the container: {e}") - def dump_python_code_run_and_get_results( - self, - code: str, - dump_file_names: list[str], - local_path: str, - env: dict | None = None, - running_extra_volume: Mapping = MappingProxyType({}), - code_dump_file_py_name: Optional[str] = None, - ) -> tuple[str, list]: - """ - Dump the code into the local path and run the code. - """ - random_file_name = f"{uuid.uuid4()}.py" if code_dump_file_py_name is None else f"{code_dump_file_py_name}.py" - with open(os.path.join(local_path, random_file_name), "w") as f: - f.write(code) - entry = f"python {random_file_name}" - log_output = self.run(entry, local_path, env, running_extra_volume=dict(running_extra_volume)) - results = [] - os.remove(os.path.join(local_path, random_file_name)) - for name in dump_file_names: - if os.path.exists(os.path.join(local_path, f"{name}")): - results.append(pickle.load(open(os.path.join(local_path, f"{name}"), "rb"))) - os.remove(os.path.join(local_path, f"{name}")) - else: - return log_output, [] - return log_output, results - class QTDockerEnv(DockerEnv): """Qlib Torch Docker"""