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 <xuyang1@microsoft.com>

* 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 <peteryang@vip.qq.com>
Co-authored-by: Xu Yang <xuyang1@microsoft.com>
This commit is contained in:
Yuante Li
2025-05-29 16:16:51 +08:00
committed by GitHub
parent b0e88c7375
commit d1019cb568
64 changed files with 2427 additions and 1025 deletions
+1
View File
@@ -5,6 +5,7 @@ Pipfile
public
release-notes.md
typescript*
qlib
# Byte-compiled / optimized / DLL files
__pycache__/
+2
View File
@@ -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,
+44
View File
@@ -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()
+4 -12
View File
@@ -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(
-4
View File
@@ -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: |-
+137
View File
@@ -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)
@@ -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):
@@ -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
@@ -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
@@ -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"
@@ -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(
@@ -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)
@@ -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):
@@ -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
@@ -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()
@@ -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,
@@ -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(
+24 -2
View File
@@ -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:
@@ -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)
@@ -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
+44 -43
View File
@@ -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)
+38 -22
View File
@@ -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.
+3 -1
View File
@@ -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)
+2
View File
@@ -82,5 +82,7 @@ class RDAgentSettings(ExtendedBaseSettings):
enable_mlflow: bool = False
initial_fator_library_size: int = 20
RD_AGENT_SETTINGS = RDAgentSettings()
+2 -15
View File
@@ -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
+326 -9
View File
@@ -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):
@@ -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
@@ -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
@@ -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:
@@ -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."
+6 -9
View File
@@ -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:
+6 -12
View File
@@ -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(
+5 -21
View File
@@ -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,
@@ -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:
+30 -46
View File
@@ -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":
@@ -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
@@ -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(
@@ -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):
@@ -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,
+14 -31
View File
@@ -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."
@@ -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.")
+69 -54
View File
@@ -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],
)
@@ -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
+67
View File
@@ -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}."
)
+1 -1
View File
@@ -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 .
@@ -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:
@@ -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:
@@ -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
@@ -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"
@@ -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: <PRED>
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: <MODEL>
dataset: <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
@@ -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:
@@ -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
@@ -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: <PRED>
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: <MODEL>
dataset: <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
+12 -6
View File
@@ -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: |-
@@ -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)
+49 -27
View File
@@ -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.",
+29 -11
View File
@@ -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
@@ -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(
+177 -150
View File
@@ -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": <true or false>,
}
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 %}
+109
View File
@@ -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)
@@ -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
@@ -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,
)
)
@@ -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
+125 -39
View File
@@ -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"""