diff --git a/.gitignore b/.gitignore index b2c38a81..176e76d2 100644 --- a/.gitignore +++ b/.gitignore @@ -153,3 +153,8 @@ git_ignore_folder/ # DB files *.db + +# Docker +env_factor/mlruns/ +env_tpl +mlruns/ \ No newline at end of file diff --git a/rdagent/app/qlib_rd_loop/conf.py b/rdagent/app/qlib_rd_loop/conf.py index a97f76ff..5e8b8b14 100644 --- a/rdagent/app/qlib_rd_loop/conf.py +++ b/rdagent/app/qlib_rd_loop/conf.py @@ -1,4 +1,5 @@ from pydantic_settings import BaseSettings +from pathlib import Path class PropSetting(BaseSettings): @@ -22,6 +23,8 @@ class PropSetting(BaseSettings): qlib_model_summarizer: str = "rdagent.scenarios.qlib.task_generator.feedback.QlibModelHypothesisExperiment2Feedback" evolving_n: int = 10 - - + + py_bin: str = "/usr/bin/python" + local_qlib_folder: Path = Path("/home/rdagent/qlib") + PROP_SETTING = PropSetting() diff --git a/rdagent/app/qlib_rd_loop/factor.py b/rdagent/app/qlib_rd_loop/factor.py index cfabe90a..32bbbb65 100644 --- a/rdagent/app/qlib_rd_loop/factor.py +++ b/rdagent/app/qlib_rd_loop/factor.py @@ -25,9 +25,10 @@ hypothesis_gen: HypothesisGen = import_class(PROP_SETTING.qlib_factor_hypothesis hypothesis2experiment: Hypothesis2Experiment = import_class(PROP_SETTING.qlib_factor_hypothesis2experiment)() qlib_factor_coder: TaskGenerator = import_class(PROP_SETTING.qlib_factor_coder)(scen) + qlib_factor_runner: TaskGenerator = import_class(PROP_SETTING.qlib_factor_runner)(scen) -qlib_factor_summarizer: HypothesisExperiment2Feedback = import_class(PROP_SETTING.qlib_factor_summarizer)() +qlib_factor_summarizer: HypothesisExperiment2Feedback = import_class(PROP_SETTING.qlib_factor_summarizer)(scen) trace = Trace(scen=scen) @@ -38,4 +39,4 @@ for _ in range(PROP_SETTING.evolving_n): exp = qlib_factor_runner.generate(exp) feedback = qlib_factor_summarizer.generateFeedback(exp, hypothesis, trace) - trace.hist.append((hypothesis, exp, feedback)) + trace.hist.append((hypothesis, exp, feedback)) \ No newline at end of file diff --git a/rdagent/app/qlib_rd_loop/model.py b/rdagent/app/qlib_rd_loop/model.py index f8b07211..cd3bb605 100644 --- a/rdagent/app/qlib_rd_loop/model.py +++ b/rdagent/app/qlib_rd_loop/model.py @@ -25,7 +25,7 @@ hypothesis2experiment: Hypothesis2Experiment = import_class(PROP_SETTING.qlib_mo qlib_model_coder: TaskGenerator = import_class(PROP_SETTING.qlib_model_coder)(scen) qlib_model_runner: TaskGenerator = import_class(PROP_SETTING.qlib_model_runner)(scen) -qlib_model_summarizer: HypothesisExperiment2Feedback = import_class(PROP_SETTING.qlib_model_hypothesis2experiment)() +qlib_model_summarizer: HypothesisExperiment2Feedback = import_class(PROP_SETTING.qlib_model_summarizer)() trace = Trace(scen=scen) for _ in range(PROP_SETTING.evolving_n): diff --git a/rdagent/components/coder/factor_coder/CoSTEER/__init__.py b/rdagent/components/coder/factor_coder/CoSTEER/__init__.py index e3a81409..9fa2f84e 100644 --- a/rdagent/components/coder/factor_coder/CoSTEER/__init__.py +++ b/rdagent/components/coder/factor_coder/CoSTEER/__init__.py @@ -98,4 +98,5 @@ class FactorCoSTEER(TaskGenerator[FactorExperiment]): if self.new_knowledge_base_path is not None: pickle.dump(factor_knowledge_base, open(self.new_knowledge_base_path, "wb")) self.knowledge_base = factor_knowledge_base + factor_experiment.based_experiments = exp.based_experiments return factor_experiment diff --git a/rdagent/components/coder/factor_coder/CoSTEER/evaluators.py b/rdagent/components/coder/factor_coder/CoSTEER/evaluators.py index 7cb861dd..6e19ddfa 100644 --- a/rdagent/components/coder/factor_coder/CoSTEER/evaluators.py +++ b/rdagent/components/coder/factor_coder/CoSTEER/evaluators.py @@ -84,7 +84,7 @@ class FactorCodeEvaluator(FactorEvaluator): gt_implementation: Implementation = None, **kwargs, ): - factor_information = target_task.get_factor_information() + factor_information = target_task.get_task_information() code = implementation.code system_prompt = ( @@ -181,6 +181,36 @@ class FactorOutputFormatEvaluator(FactorEvaluator): ) +class FactorDatetimeDailyEvaluator(FactorEvaluator): + def evaluate( + self, + implementation: Implementation, + gt_implementation: Implementation, + ) -> Tuple[str | object]: + _, gen_df = self._get_df(gt_implementation, implementation) + if gen_df is None: + return "The source dataframe is None. Skip the evaluation of the datetime format.", False + + if "datetime" not in gen_df.index.names: + return "The source dataframe does not have a datetime index. Please check the implementation.", False + + try: + pd.to_datetime(gen_df.index.get_level_values("datetime")) + except Exception: + return ( + "The source dataframe has a datetime index but it is not in the correct format (maybe a regular string or other objects). Please check the implementation.", + False, + ) + + time_diff = gen_df.index.get_level_values("datetime").to_series().diff().dropna().unique() + if pd.Timedelta(minutes=1) in time_diff: + return ( + "The generated dataframe is not daily. The implementation is definitely wrong. Please check the implementation.", + False, + ) + return "The generated dataframe is daily.", True + + class FactorRowCountEvaluator(FactorEvaluator): def evaluate( self, @@ -314,6 +344,9 @@ class FactorValueEvaluator(FactorEvaluator): feedback_str, _ = FactorOutputFormatEvaluator(self.scen).evaluate(implementation, gt_implementation) conclusions.append(feedback_str) + feedback_str, _ = FactorDatetimeDailyEvaluator(self.scen).evaluate(implementation, gt_implementation) + conclusions.append(feedback_str) + # Check if both dataframe have the same rows count if gt_implementation is not None: feedback_str, _ = FactorRowCountEvaluator(self.scen).evaluate(implementation, gt_implementation) @@ -373,7 +406,7 @@ class FactorFinalDecisionEvaluator(Evaluator): evaluate_prompts["evaluator_final_decision_v1_user"], ) .render( - factor_information=target_task.get_factor_information(), + factor_information=target_task.get_task_information(), execution_feedback=execution_feedback_to_render, code_feedback=code_feedback, factor_value_feedback=( @@ -475,7 +508,7 @@ class FactorEvaluatorForCoder(FactorEvaluator): if implementation is None: return None - target_task_information = target_task.get_factor_information() + target_task_information = target_task.get_task_information() if ( queried_knowledge is not None and target_task_information in queried_knowledge.success_task_to_knowledge_dict diff --git a/rdagent/components/coder/factor_coder/CoSTEER/evolving_strategy.py b/rdagent/components/coder/factor_coder/CoSTEER/evolving_strategy.py index fcda9ad8..aa3a9eb4 100644 --- a/rdagent/components/coder/factor_coder/CoSTEER/evolving_strategy.py +++ b/rdagent/components/coder/factor_coder/CoSTEER/evolving_strategy.py @@ -59,7 +59,7 @@ class MultiProcessEvolvingStrategy(EvolvingStrategy): # 1.找出需要evolve的factor to_be_finished_task_index = [] for index, target_factor_task in enumerate(new_evo.sub_tasks): - target_factor_task_desc = target_factor_task.get_factor_information() + target_factor_task_desc = target_factor_task.get_task_information() if target_factor_task_desc in queried_knowledge.success_task_to_knowledge_dict: new_evo.sub_implementations[index] = queried_knowledge.success_task_to_knowledge_dict[ target_factor_task_desc @@ -119,7 +119,7 @@ class FactorEvolvingStrategy(MultiProcessEvolvingStrategy): target_task: FactorTask, queried_knowledge: FactorQueriedKnowledgeV1 = None, ) -> Implementation: - factor_information_str = target_task.get_factor_information() + factor_information_str = target_task.get_task_information() if queried_knowledge is not None and factor_information_str in queried_knowledge.success_task_to_knowledge_dict: return queried_knowledge.success_task_to_knowledge_dict[factor_information_str].implementation @@ -208,7 +208,7 @@ class FactorEvolvingStrategyWithGraph(MultiProcessEvolvingStrategy): ) -> Implementation: error_summary = FACTOR_IMPLEMENT_SETTINGS.v2_error_summary # 1. 提取因子的背景信息 - target_factor_task_information = target_task.get_factor_information() + target_factor_task_information = target_task.get_task_information() # 2. 检查该因子是否需要继续做(是否已经作对,是否做错太多) if ( diff --git a/rdagent/components/coder/factor_coder/CoSTEER/knowledge_management.py b/rdagent/components/coder/factor_coder/CoSTEER/knowledge_management.py index c238d176..b18529ae 100644 --- a/rdagent/components/coder/factor_coder/CoSTEER/knowledge_management.py +++ b/rdagent/components/coder/factor_coder/CoSTEER/knowledge_management.py @@ -114,7 +114,7 @@ class FactorRAGStrategyV1(RAGStrategy): feedback = evo_step.feedback for task_index in range(len(implementations.sub_tasks)): target_task = implementations.sub_tasks[task_index] - target_task_information = target_task.get_factor_information() + target_task_information = target_task.get_task_information() implementation = implementations.sub_implementations[task_index] single_feedback = feedback[task_index] if single_feedback is None: @@ -147,7 +147,7 @@ class FactorRAGStrategyV1(RAGStrategy): queried_knowledge = FactorQueriedKnowledgeV1() for target_factor_task in evo.sub_tasks: - target_factor_task_information = target_factor_task.get_factor_information() + target_factor_task_information = target_factor_task.get_task_information() if target_factor_task_information in self.knowledgebase.success_task_info_set: queried_knowledge.success_task_to_knowledge_dict[target_factor_task_information] = ( self.knowledgebase.implementation_trace[target_factor_task_information][-1] @@ -233,7 +233,7 @@ class FactorGraphRAGStrategy(RAGStrategy): for task_index in range(len(implementations.sub_tasks)): single_feedback = feedback[task_index] target_task = implementations.sub_tasks[task_index] - target_task_information = target_task.get_factor_information() + target_task_information = target_task.get_task_information() implementation = implementations.sub_implementations[task_index] single_feedback = feedback[task_index] if single_feedback is None: @@ -395,7 +395,7 @@ class FactorGraphRAGStrategy(RAGStrategy): fail_task_trial_limit = FACTOR_IMPLEMENT_SETTINGS.fail_task_trial_limit for target_factor_task in evo.sub_tasks: - target_factor_task_information = target_factor_task.get_factor_information() + target_factor_task_information = target_factor_task.get_task_information() if ( target_factor_task_information not in self.knowledgebase.success_task_to_knowledge_dict and target_factor_task_information in self.knowledgebase.working_trace_knowledge @@ -442,7 +442,7 @@ class FactorGraphRAGStrategy(RAGStrategy): ) -> QueriedKnowledge | None: # queried_component_knowledge = FactorQueriedGraphComponentKnowledge() for target_factor_task in evo.sub_tasks: - target_factor_task_information = target_factor_task.get_factor_information() + target_factor_task_information = target_factor_task.get_task_information() if ( target_factor_task_information in self.knowledgebase.success_task_to_knowledge_dict or target_factor_task_information in factor_implementation_queried_graph_knowledge.failed_task_info_set @@ -582,7 +582,7 @@ class FactorGraphRAGStrategy(RAGStrategy): ) -> QueriedKnowledge | None: # queried_error_knowledge = FactorQueriedGraphErrorKnowledge() for task_index, target_factor_task in enumerate(evo.sub_tasks): - target_factor_task_information = target_factor_task.get_factor_information() + target_factor_task_information = target_factor_task.get_task_information() factor_implementation_queried_graph_knowledge.error_with_success_task[target_factor_task_information] = {} if ( target_factor_task_information in self.knowledgebase.success_task_to_knowledge_dict diff --git a/rdagent/components/coder/factor_coder/CoSTEER/scheduler.py b/rdagent/components/coder/factor_coder/CoSTEER/scheduler.py index a0f56dca..90e00a9c 100644 --- a/rdagent/components/coder/factor_coder/CoSTEER/scheduler.py +++ b/rdagent/components/coder/factor_coder/CoSTEER/scheduler.py @@ -39,7 +39,7 @@ def LLMSelect( tasks = [] for i in to_be_finished_task_index: # find corresponding former trace for each task - target_factor_task_information = evo.sub_tasks[i].get_factor_information() + target_factor_task_information = evo.sub_tasks[i].get_task_information() if target_factor_task_information in former_trace: tasks.append((i, evo.sub_tasks[i], former_trace[target_factor_task_information])) diff --git a/rdagent/components/coder/factor_coder/config.py b/rdagent/components/coder/factor_coder/config.py index 236275a3..1adb0d92 100644 --- a/rdagent/components/coder/factor_coder/config.py +++ b/rdagent/components/coder/factor_coder/config.py @@ -7,13 +7,16 @@ SELECT_METHOD = Literal["random", "scheduler"] class FactorImplementSettings(BaseSettings): - file_based_execution_data_folder: str = str( + factor_data_folder: str = str( (Path().cwd() / "git_ignore_folder" / "factor_implementation_source_data").absolute(), ) - file_based_execution_workspace: str = str( + factor_data_folder_debug: str = str( + (Path().cwd() / "git_ignore_folder" / "factor_implementation_source_data_debug").absolute(), + ) + factor_execution_workspace: str = str( (Path().cwd() / "git_ignore_folder" / "factor_implementation_workspace").absolute(), ) - implementation_execution_cache_location: str = str( + factor_cache_location: str = str( (Path().cwd() / "git_ignore_folder" / "factor_implementation_execution_cache").absolute(), ) enable_execution_cache: bool = True # whether to enable the execution cache diff --git a/rdagent/components/coder/factor_coder/factor.py b/rdagent/components/coder/factor_coder/factor.py index 4932a9ae..aa4fecac 100644 --- a/rdagent/components/coder/factor_coder/factor.py +++ b/rdagent/components/coder/factor_coder/factor.py @@ -37,7 +37,7 @@ class FactorTask(Task): self.variables = variables self.factor_resources = resource - def get_factor_information(self): + def get_task_information(self): return f"""factor_name: {self.factor_name} factor_description: {self.factor_description} factor_formulation: {self.factor_formulation} @@ -95,11 +95,11 @@ class FileBasedFactorImplementation(FBImplementation): def prepare(self, *args, **kwargs): self.workspace_path = Path( - FACTOR_IMPLEMENT_SETTINGS.file_based_execution_workspace, + FACTOR_IMPLEMENT_SETTINGS.factor_execution_workspace, ) / str(uuid.uuid4()) self.workspace_path.mkdir(exist_ok=True, parents=True) - def execute(self, store_result: bool = False) -> Tuple[str, pd.DataFrame]: + def execute(self, store_result: bool = False, data_type: str = "Debug") -> Tuple[str, pd.DataFrame]: """ execute the implementation and get the factor value by the following steps: 1. make the directory in workspace path @@ -120,14 +120,10 @@ class FileBasedFactorImplementation(FBImplementation): raise ValueError(self.FB_CODE_NOT_SET) with FileLock(self.workspace_path / "execution.lock"): if FACTOR_IMPLEMENT_SETTINGS.enable_execution_cache: - # NOTE: cache the result for the same code - target_file_name = md5_hash(self.code_dict["factor.py"]) - cache_file_path = ( - Path(FACTOR_IMPLEMENT_SETTINGS.implementation_execution_cache_location) / f"{target_file_name}.pkl" - ) - Path(FACTOR_IMPLEMENT_SETTINGS.implementation_execution_cache_location).mkdir( - exist_ok=True, parents=True - ) + # NOTE: cache the result for the same code and same data type + target_file_name = md5_hash(data_type + self.code_dict["factor.py"]) + cache_file_path = Path(FACTOR_IMPLEMENT_SETTINGS.factor_cache_location) / f"{target_file_name}.pkl" + Path(FACTOR_IMPLEMENT_SETTINGS.factor_cache_location).mkdir(exist_ok=True, parents=True) if cache_file_path.exists() and not self.raise_exception: cached_res = pickle.load(open(cache_file_path, "rb")) if store_result and cached_res[1] is not None: @@ -137,8 +133,14 @@ class FileBasedFactorImplementation(FBImplementation): if self.executed_factor_value_dataframe is not None: return self.FB_FROM_CACHE, self.executed_factor_value_dataframe - source_data_path = Path( - FACTOR_IMPLEMENT_SETTINGS.file_based_execution_data_folder, + source_data_path = ( + Path( + FACTOR_IMPLEMENT_SETTINGS.factor_data_folder_debug, + ) + if data_type == "Debug" + else Path( + FACTOR_IMPLEMENT_SETTINGS.factor_data_folder, + ) ) source_data_path.mkdir(exist_ok=True, parents=True) diff --git a/rdagent/components/coder/factor_coder/prompts.yaml b/rdagent/components/coder/factor_coder/prompts.yaml index 53d62fca..61d8b69e 100644 --- a/rdagent/components/coder/factor_coder/prompts.yaml +++ b/rdagent/components/coder/factor_coder/prompts.yaml @@ -68,7 +68,7 @@ evolving_strategy_factor_implementation_v1_user: |- --------------Correct code to similar factors:--------------- {% for similar_successful_knowledge in queried_similar_successful_knowledge %} =====Factor {{loop.index}}:===== - {{ similar_successful_knowledge.target_task.get_factor_information() }} + {{ similar_successful_knowledge.target_task.get_task_information() }} =====Code:===== {{ similar_successful_knowledge.implementation.code }} {% endfor %} @@ -94,7 +94,7 @@ evolving_strategy_factor_implementation_v2_user: |- When doing other tasks, you met some similar errors but you finally solve them. Here are some examples: {% for error_content, similar_error_knowledge in queried_similar_error_knowledge %} --------------Factor information to similar error ({{error_content}}):--------------- - {{ similar_error_knowledge[0].target_task.get_factor_information() }} + {{ similar_error_knowledge[0].target_task.get_task_information() }} =====Code with similar error ({{error_content}}):===== {{ similar_error_knowledge[0].implementation.code }} =====Success code to former code with similar error ({{error_content}}):===== @@ -111,7 +111,7 @@ evolving_strategy_factor_implementation_v2_user: |- --------------Correct code to similar factors:--------------- {% for similar_component_knowledge in queried_similar_component_knowledge %} =====Factor {{loop.index}}:===== - {{ similar_component_knowledge.target_task.get_factor_information() }} + {{ similar_component_knowledge.target_task.get_task_information() }} =====Code:===== {{ similar_component_knowledge.implementation.code }} {% endfor %} @@ -137,7 +137,7 @@ evolving_strategy_error_summary_v2_user: |- {% if queried_similar_error_knowledge|length != 0 %} {% for error_content, similar_error_knowledge in queried_similar_error_knowledge %} --------------Factor information to similar error ({{error_content}}):--------------- - {{ similar_error_knowledge[0].target_task.get_factor_information() }} + {{ similar_error_knowledge[0].target_task.get_task_information() }} =====Code with similar error ({{error_content}}):===== {{ similar_error_knowledge[0].implementation.code }} =====Success code to former code with similar error ({{error_content}}):===== diff --git a/rdagent/components/coder/factor_coder/utils.py b/rdagent/components/coder/factor_coder/utils.py index dd705b29..283f7624 100644 --- a/rdagent/components/coder/factor_coder/utils.py +++ b/rdagent/components/coder/factor_coder/utils.py @@ -22,7 +22,7 @@ def get_data_folder_intro(): It is for preparing prompting message. """ content_l = [] - for p in Path(FACTOR_IMPLEMENT_SETTINGS.file_based_execution_data_folder).iterdir(): + for p in Path(FACTOR_IMPLEMENT_SETTINGS.factor_data_folder).iterdir(): if p.name.endswith(".h5"): df = pd.read_hdf(p) # get df.head() as string with full width diff --git a/rdagent/components/coder/model_coder/CoSTEER/__init__.py b/rdagent/components/coder/model_coder/CoSTEER/__init__.py index be9ede38..a5818987 100644 --- a/rdagent/components/coder/model_coder/CoSTEER/__init__.py +++ b/rdagent/components/coder/model_coder/CoSTEER/__init__.py @@ -83,4 +83,5 @@ class ModelCoSTEER(TaskGenerator[ModelExperiment]): if self.new_knowledge_base_path is not None: pickle.dump(model_knowledge_base, open(self.new_knowledge_base_path, "wb")) self.knowledge_base = model_knowledge_base + model_experiment.based_experiments = exp.based_experiments return model_experiment diff --git a/rdagent/components/coder/model_coder/CoSTEER/evaluators.py b/rdagent/components/coder/model_coder/CoSTEER/evaluators.py index 15098314..168251c5 100644 --- a/rdagent/components/coder/model_coder/CoSTEER/evaluators.py +++ b/rdagent/components/coder/model_coder/CoSTEER/evaluators.py @@ -72,7 +72,7 @@ class ModelCodeEvaluator(Evaluator): if gt_implementation is not None: assert isinstance(gt_implementation, ModelImplementation) - model_task_information = target_task.get_information() + model_task_information = target_task.get_task_information() code = implementation.code system_prompt = ( @@ -146,7 +146,7 @@ class ModelFinalEvaluator(Evaluator): evaluate_prompts["evaluator_final_feedback"]["user"], ) .render( - model_information=target_task.get_information(), + model_information=target_task.get_task_information(), model_execution_feedback=execution_feedback_to_render, model_code_feedback=model_code_feedback, model_value_feedback=model_value_feedback, @@ -224,7 +224,7 @@ class ModelCoderEvaluator(Evaluator): queried_knowledge: QueriedKnowledge = None, **kwargs, ) -> ModelCoderFeedback: - target_task_information = target_task.get_information() + target_task_information = target_task.get_task_information() if ( queried_knowledge is not None and target_task_information in queried_knowledge.success_task_to_knowledge_dict diff --git a/rdagent/components/coder/model_coder/CoSTEER/evolving_strategy.py b/rdagent/components/coder/model_coder/CoSTEER/evolving_strategy.py index 27ba37fb..71e9e5e7 100644 --- a/rdagent/components/coder/model_coder/CoSTEER/evolving_strategy.py +++ b/rdagent/components/coder/model_coder/CoSTEER/evolving_strategy.py @@ -27,7 +27,7 @@ class ModelCoderEvolvingStrategy(EvolvingStrategy): target_task: ModelTask, queried_knowledge: ModelQueriedKnowledge = None, ) -> ModelImplementation: - model_information_str = target_task.get_information() + model_information_str = target_task.get_task_information() if queried_knowledge is not None and model_information_str in queried_knowledge.success_task_to_knowledge_dict: return queried_knowledge.success_task_to_knowledge_dict[model_information_str].implementation @@ -113,7 +113,7 @@ class ModelCoderEvolvingStrategy(EvolvingStrategy): # 1.找出需要evolve的model to_be_finished_task_index = [] for index, target_model_task in enumerate(new_evo.sub_tasks): - target_model_task_desc = target_model_task.get_information() + target_model_task_desc = target_model_task.get_task_information() if target_model_task_desc in queried_knowledge.success_task_to_knowledge_dict: new_evo.sub_implementations[index] = queried_knowledge.success_task_to_knowledge_dict[ target_model_task_desc diff --git a/rdagent/components/coder/model_coder/CoSTEER/knowledge_management.py b/rdagent/components/coder/model_coder/CoSTEER/knowledge_management.py index 7e46f136..a706bf65 100644 --- a/rdagent/components/coder/model_coder/CoSTEER/knowledge_management.py +++ b/rdagent/components/coder/model_coder/CoSTEER/knowledge_management.py @@ -86,7 +86,7 @@ class ModelRAGStrategy(RAGStrategy): feedback = evo_step.feedback for task_index in range(len(implementations.sub_tasks)): target_task = implementations.sub_tasks[task_index] - target_task_information = target_task.get_information() + target_task_information = target_task.get_task_information() implementation = implementations.sub_implementations[task_index] single_feedback = feedback[task_index] if single_feedback is None: @@ -119,7 +119,7 @@ class ModelRAGStrategy(RAGStrategy): queried_knowledge = ModelQueriedKnowledge() for target_model_task in evo.sub_tasks: - target_model_task_information = target_model_task.get_information() + target_model_task_information = target_model_task.get_task_information() if target_model_task_information in self.knowledgebase.success_task_info_set: queried_knowledge.success_task_to_knowledge_dict[target_model_task_information] = ( self.knowledgebase.implementation_trace[target_model_task_information][-1] diff --git a/rdagent/components/coder/model_coder/conf.py b/rdagent/components/coder/model_coder/conf.py index 74283e1a..70af7d29 100644 --- a/rdagent/components/coder/model_coder/conf.py +++ b/rdagent/components/coder/model_coder/conf.py @@ -8,10 +8,10 @@ class ModelImplSettings(BaseSettings): class Config: env_prefix = "MODEL_IMPL_" # Use MODEL_IMPL_ as prefix for environment variables - file_based_execution_workspace: str = str( + model_execution_workspace: str = str( (Path().cwd() / "git_ignore_folder" / "model_implementation_workspace").absolute(), ) - implementation_execution_cache_location: str = str( + model_cache_location: str = str( (Path().cwd() / "git_ignore_folder" / "model_implementation_execution_cache").absolute(), ) diff --git a/rdagent/components/coder/model_coder/model.py b/rdagent/components/coder/model_coder/model.py index 93c9451d..7ddf634b 100644 --- a/rdagent/components/coder/model_coder/model.py +++ b/rdagent/components/coder/model_coder/model.py @@ -24,7 +24,7 @@ class ModelTask(Task): self.variables: str = variables self.model_type: str = model_type # Tabular for tabular model, TimesSeries for time series model - def get_information(self): + def get_task_information(self): return f"""name: {self.name} description: {self.description} formulation: {self.formulation} @@ -68,7 +68,7 @@ class ModelImplementation(FBImplementation): Prepare for the workspace; """ unique_id = uuid.uuid4() - self.workspace_path = Path(MODEL_IMPL_SETTINGS.file_based_execution_workspace) / f"M{unique_id}" + self.workspace_path = Path(MODEL_IMPL_SETTINGS.model_execution_workspace) / f"M{unique_id}" # start with `M` so that it can be imported via python self.workspace_path.mkdir(parents=True, exist_ok=True) @@ -84,10 +84,8 @@ class ModelImplementation(FBImplementation): if MODEL_IMPL_SETTINGS.enable_execution_cache: # NOTE: cache the result for the same code target_file_name = md5_hash(self.code_dict["model.py"]) - cache_file_path = ( - Path(MODEL_IMPL_SETTINGS.implementation_execution_cache_location) / f"{target_file_name}.pkl" - ) - Path(MODEL_IMPL_SETTINGS.implementation_execution_cache_location).mkdir(exist_ok=True, parents=True) + cache_file_path = Path(MODEL_IMPL_SETTINGS.model_cache_location) / f"{target_file_name}.pkl" + Path(MODEL_IMPL_SETTINGS.model_cache_location).mkdir(exist_ok=True, parents=True) if cache_file_path.exists(): return pickle.load(open(cache_file_path, "rb")) mod = get_module_by_module_path(str(self.workspace_path / "model.py")) diff --git a/rdagent/core/experiment.py b/rdagent/core/experiment.py index 42125da7..a71545dc 100644 --- a/rdagent/core/experiment.py +++ b/rdagent/core/experiment.py @@ -7,17 +7,27 @@ This file contains the all the class about organizing the task in RD-Agent. """ -class Task: +class Task(ABC): # TODO: 把name放在这里作为主键 # Please refer to rdagent/model_implementation/task.py for the implementation # I think the task version applies to the base class. - pass + + @abstractmethod + def get_task_information(self): + """ + Get the task information string to build the unique key + """ + pass ASpecificTask = TypeVar("ASpecificTask", bound=Task) class Implementation(ABC, Generic[ASpecificTask]): + # TODO: workspace; + # - code or data(optional) + # - Execute logic + # - `env is not included`. It is a underlying infra def __init__(self, target_task: ASpecificTask) -> None: self.target_task = target_task @@ -85,6 +95,7 @@ class FBImplementation(Implementation): typical usage of `*args, **kwargs`: Different methods shares the same data. The data are passed by the arguments. """ + # TODO: model and factor prepare; def inject_code(self, **files: str): """ @@ -113,11 +124,14 @@ class Experiment(ABC, Generic[ASpecificTask, ASpecificImp]): The experiment is a sequence of tasks and the implementations of the tasks after generated by the TaskGenerator. """ + result_ws: Optional[FBImplementation] + def __init__(self, sub_tasks: Sequence[ASpecificTask]) -> None: self.sub_tasks = sub_tasks self.sub_implementations: Sequence[ASpecificImp] = [None for _ in self.sub_tasks] self.based_experiments: Sequence[Experiment] = [] self.result: object = None # The result of the experiment, can be different types in different scenarios. + self.result_ws = None TaskOrExperiment = TypeVar("TaskOrExperiment", Task, Experiment) diff --git a/rdagent/core/proposal.py b/rdagent/core/proposal.py index 58192be9..9b5e1bad 100644 --- a/rdagent/core/proposal.py +++ b/rdagent/core/proposal.py @@ -3,10 +3,10 @@ """ from abc import ABC, abstractmethod -from typing import Dict, Generic, List, Tuple, TypeVar +from typing import Any, Dict, Generic, List, Tuple, TypeVar from rdagent.core.evaluation import Feedback -from rdagent.core.experiment import Experiment +from rdagent.core.experiment import ASpecificTask, Experiment from rdagent.core.scenario import Scenario # class data_ana: XXX @@ -101,9 +101,12 @@ class Hypothesis2Experiment(ABC, Generic[ASpecificExp]): class HypothesisExperiment2Feedback: """ "Generated feedbacks on the hypothesis from **Executed** Implementations of different tasks & their comparisons with previous performances""" - def generateFeedback(self, ti: Experiment, hypothesis: Hypothesis, trace: Trace) -> HypothesisFeedback: + def __init__(self, scen: Scenario): + self.scen = scen + + def generateFeedback(self, exp: Experiment, hypothesis: Hypothesis, 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). + The `exp` 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. """ raise NotImplementedError("generateFeedback method is not implemented.") diff --git a/rdagent/scenarios/qlib/conf.py b/rdagent/scenarios/qlib/conf.py new file mode 100644 index 00000000..e4fbc363 --- /dev/null +++ b/rdagent/scenarios/qlib/conf.py @@ -0,0 +1,19 @@ +from __future__ import annotations + +from pathlib import Path + +from dotenv import load_dotenv +from pydantic_settings import BaseSettings + +# make sure that env variable is loaded while calling Config() +load_dotenv(verbose=True, override=True) + +from pydantic_settings import BaseSettings + + +class QlibRDAgentSettings(BaseSettings): + runner_cache_result: bool = True # whether to cache the result of the docker execution + runner_cache_path: str = str(Path.cwd() / "runner_cache/") # the path to store the cache + + +Qlib_RD_AGENT_SETTINGS = QlibRDAgentSettings() diff --git a/rdagent/scenarios/qlib/docker/Dockerfile b/rdagent/scenarios/qlib/docker/Dockerfile new file mode 100644 index 00000000..26419c6e --- /dev/null +++ b/rdagent/scenarios/qlib/docker/Dockerfile @@ -0,0 +1,23 @@ +FROM pytorch/pytorch:latest + +# For GPU support, please choose the proper tag from https://hub.docker.com/r/pytorch/pytorch/tags + +RUN apt-get clean && apt-get update && apt-get install -y \ + curl \ + vim \ + git \ + build-essential \ + && rm -rf /var/lib/apt/lists/* + +RUN git clone https://github.com/microsoft/qlib.git + +WORKDIR /workspace/qlib + +RUN git reset c9ed050ef034fe6519c14b59f3d207abcb693282 --hard + +RUN python -m pip install --upgrade numpy +RUN python -m pip install --upgrade cython +RUN python -m pip install -e . + +RUN pip install catboost +RUN pip install xgboost \ No newline at end of file diff --git a/rdagent/scenarios/qlib/factor_proposal.py b/rdagent/scenarios/qlib/factor_proposal.py index 7be92420..b66d752d 100644 --- a/rdagent/scenarios/qlib/factor_proposal.py +++ b/rdagent/scenarios/qlib/factor_proposal.py @@ -77,4 +77,6 @@ class QlibFactorHypothesis2Experiment(FactorHypothesis2Experiment): tasks.append(FactorTask(factor_name, description, formulation, variables)) exp = FactorExperiment(tasks) exp.based_experiments = [t[1] for t in trace.hist if t[2]] + if len(exp.based_experiments) == 0: + exp.based_experiments.append(FactorExperiment(sub_tasks=[])) return exp diff --git a/rdagent/scenarios/qlib/prompts.yaml b/rdagent/scenarios/qlib/prompts.yaml index 5ae3ba69..f917b220 100644 --- a/rdagent/scenarios/qlib/prompts.yaml +++ b/rdagent/scenarios/qlib/prompts.yaml @@ -47,4 +47,36 @@ model_experiment_output_format: |- "model_type": "type of model 1, Tabular or TimesSeries" # Should be one of "Tabular" or "TimeSeries" } # Don't add ellipsis (...) or any filler text that might cause JSON parsing errors here! - } \ No newline at end of file + } + +data_feedback_generation: + system: |- + You are a professional result analysis assistant on data driven R&D. + The task is described in the following scenario: + {{ scenario }} + You will receive a hypothesis, multiple tasks with their factors, and some results. + Your feedback should specify whether the current result supports or refutes the hypothesis, compare it with previous results, and suggest improvements or new directions. + Please provide detailed and constructive feedback for the future exploration. + Please respond in JSON format, and 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.", + "Replace Best Result": "yes or no" + } + user: |- + Target hypothesis: + {{ hypothesis_text }} + Tasks and Factors: + {{ task_details }} + Current Result: + {{ current_result }} + SOTA Result: + {{ sota_result }} + Analyze the current result in the context of its ability to: + 1. Support or refute the hypothesis. + 2. Show improvement or deterioration compared to the last experiment. + 3. Demonstrate positive or negative effects when compared to Alpha158. + + Provide detailed feedback and recommend whether to replace the best result if the new factor proves superior. diff --git a/rdagent/scenarios/qlib/task_generator/data.py b/rdagent/scenarios/qlib/task_generator/data.py index abc63a85..bc44f203 100644 --- a/rdagent/scenarios/qlib/task_generator/data.py +++ b/rdagent/scenarios/qlib/task_generator/data.py @@ -1,5 +1,33 @@ +import pickle +import shutil +from pathlib import Path +from typing import List, Tuple + +import pandas as pd + +from rdagent.core.log import RDAgentLog from rdagent.core.task_generator import TaskGenerator +from rdagent.oai.llm_utils import md5_hash +from rdagent.scenarios.qlib.conf import Qlib_RD_AGENT_SETTINGS from rdagent.scenarios.qlib.experiment.factor_experiment import QlibFactorExperiment +from rdagent.utils.env import QTDockerEnv + +DIRNAME = Path(__file__).absolute().resolve().parent +DIRNAME_local = Path.cwd() +logger = RDAgentLog() + +# class QlibFactorExpWorkspace: + +# def prepare(): +# # create a folder; +# # copy template +# # place data inside the folder `combined_factors` +# # +# def execute(): +# de = DockerEnv() +# de.run(local_path=self.ws_path, entry="qrun conf.yaml") + +# TODO: supporting multiprocessing and keep previous results class QlibFactorRunner(TaskGenerator[QlibFactorExperiment]): @@ -10,9 +38,138 @@ class QlibFactorRunner(TaskGenerator[QlibFactorExperiment]): - price-volume data dumper - `data.py` + Adaptor to Factor implementation - results in `mlflow` - - - TODO: implement a qlib handler """ + def get_cache_key(self, exp: QlibFactorExperiment) -> str: + all_tasks = [] + for based_exp in exp.based_experiments: + all_tasks.extend(based_exp.sub_tasks) + all_tasks.extend(exp.sub_tasks) + task_info_list = [task.get_task_information() for task in all_tasks] + task_info_str = "\n".join(task_info_list) + return md5_hash(task_info_str) + + def get_cache_result(self, exp: QlibFactorExperiment) -> Tuple[bool, object]: + task_info_key = self.get_cache_key(exp) + Path(Qlib_RD_AGENT_SETTINGS.runner_cache_path).mkdir(parents=True, exist_ok=True) + cache_path = Path(Qlib_RD_AGENT_SETTINGS.runner_cache_path) / f"{task_info_key}.pkl" + if cache_path.exists(): + return True, pickle.load(open(cache_path, "rb")) + else: + return False, None + + def dump_cache_result(self, exp: QlibFactorExperiment, result: object): + task_info_key = self.get_cache_key(exp) + cache_path = Path(Qlib_RD_AGENT_SETTINGS.runner_cache_path) / f"{task_info_key}.pkl" + pickle.dump(result, open(cache_path, "wb")) + def generate(self, exp: QlibFactorExperiment) -> QlibFactorExperiment: - return exp # TODO IMPLEMENT THIS + """ + Generate the experiment by processing and combining factor data, + then passing the combined data to Docker for backtest results. + """ + if exp.based_experiments and exp.based_experiments[-1].result is None: + exp.based_experiments[-1] = self.generate(exp.based_experiments[-1]) + + if Qlib_RD_AGENT_SETTINGS.runner_cache_result: + cache_hit, result = self.get_cache_result(exp) + if cache_hit: + exp.result = result + return exp + + if exp.based_experiments: + SOTA_factor = None + if exp.based_experiments.__len__() != 1: + SOTA_factor = self.process_factor_data(exp.based_experiments) + + # Process the new factors data + new_factors = self.process_factor_data(exp) + + # Combine the SOTA factor and new factors if SOTA factor exists + if SOTA_factor is not None and not SOTA_factor.empty: + combined_factors = pd.concat([SOTA_factor, new_factors], axis=1).dropna() + else: + combined_factors = new_factors + + # Sort and nest the combined factors under 'feature' + combined_factors = combined_factors.sort_index() + new_columns = pd.MultiIndex.from_product([["feature"], combined_factors.columns]) + combined_factors.columns = new_columns + + # Save the combined factors to a pickle file + combined_factors_path = DIRNAME / "env_factor/combined_factors_df.pkl" + with open(combined_factors_path, "wb") as f: + pickle.dump(combined_factors, f) + + # Docker run + # Call Docker, pass the combined factors to Docker, and generate backtest results + qtde = QTDockerEnv() + qtde.prepare() + + # Run the Docker command + execute_log = qtde.run(local_path=str(DIRNAME / "env_factor"), entry="rm -r mlruns") + # Run the Qlib backtest + execute_log = qtde.run( + local_path=str(DIRNAME / "env_factor"), + entry=f"qrun conf.yaml" if len(exp.based_experiments) == 0 else "qrun conf_combined.yaml", + ) + + execute_log = qtde.run(local_path=str(DIRNAME / "env_factor"), entry="python read_exp_res.py") + + pkl_path = DIRNAME / "env_factor/qlib_res.pkl" + + if not pkl_path.exists(): + logger.error(f"File {pkl_path} does not exist.") + return None + + with open(pkl_path, "rb") as f: + result = pickle.load(f) + + exp.result = result + if Qlib_RD_AGENT_SETTINGS.runner_cache_result: + self.dump_cache_result(exp, result) + + # Check if the result is valid and is a DataFrame + if isinstance(result, pd.DataFrame): + if not result.empty: + logger.info("Successfully retrieved experiment result.") + return exp + else: + logger.error("Result DataFrame is empty.") + return None + else: + logger.error("Data format error.") + return None + + 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: + # Iterate over sub-implementations and execute them to get each factor data + for implementation in exp.sub_implementations: + message, df = implementation.execute(data_type="All") + + # Check if factor generation was successful + if df is not None: + 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: + logger.error("No valid factor data found to merge.") + return pd.DataFrame() # Return an empty DataFrame if no valid data diff --git a/rdagent/scenarios/qlib/task_generator/env_factor/conf.yaml b/rdagent/scenarios/qlib/task_generator/env_factor/conf.yaml new file mode 100644 index 00000000..b8483b02 --- /dev/null +++ b/rdagent/scenarios/qlib/task_generator/env_factor/conf.yaml @@ -0,0 +1,73 @@ +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: 2020-08-01 + fit_start_time: 2008-01-01 + fit_end_time: 2014-12-31 + instruments: *market +port_analysis_config: &port_analysis_config + strategy: + class: TopkDropoutStrategy + module_path: qlib.contrib.strategy + kwargs: + signal: + topk: 50 + n_drop: 5 + backtest: + start_time: 2017-01-01 + end_time: 2020-08-01 + account: 100000000 + benchmark: *benchmark + exchange_kwargs: + limit_threshold: 0.095 + deal_price: close + open_cost: 0.0005 + close_cost: 0.0015 + min_cost: 5 +task: + model: + class: LGBModel + module_path: qlib.contrib.model.gbdt + kwargs: + loss: mse + colsample_bytree: 0.8879 + learning_rate: 0.2 + subsample: 0.8789 + lambda_l1: 205.6999 + lambda_l2: 580.9768 + max_depth: 8 + num_leaves: 210 + num_threads: 20 + dataset: + class: DatasetH + module_path: qlib.data.dataset + kwargs: + handler: + class: Alpha158 + 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] + record: + - class: SignalRecord + module_path: qlib.workflow.record_temp + kwargs: + model: + dataset: + - class: SigAnaRecord + module_path: qlib.workflow.record_temp + kwargs: + ana_long_short: False + ann_scaler: 252 + - class: PortAnaRecord + module_path: qlib.workflow.record_temp + kwargs: + config: *port_analysis_config diff --git a/rdagent/scenarios/qlib/task_generator/env_factor/conf_combined.yaml b/rdagent/scenarios/qlib/task_generator/env_factor/conf_combined.yaml new file mode 100644 index 00000000..b7768bc0 --- /dev/null +++ b/rdagent/scenarios/qlib/task_generator/env_factor/conf_combined.yaml @@ -0,0 +1,93 @@ +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"] + - class: qlib.data.dataset.loader.StaticDataLoader + kwargs: + # config: "/home/finco/v-yuanteli/RD-Agent/rdagent/scenarios/qlib/task_generator/env_factor/combined_factors_df.pkl" + config: "combined_factors_df.pkl" + + learn_processors: + - class: DropnaLabel + - class: CSZScoreNorm + kwargs: + fields_group: label + +port_analysis_config: &port_analysis_config + strategy: + class: TopkDropoutStrategy + module_path: qlib.contrib.strategy + kwargs: + signal: + topk: 50 + n_drop: 5 + backtest: + start_time: 2017-01-01 + end_time: 2020-08-01 + account: 100000000 + benchmark: *benchmark + exchange_kwargs: + limit_threshold: 0.095 + deal_price: close + open_cost: 0.0005 + close_cost: 0.0015 + min_cost: 5 + +task: + model: + class: LGBModel + module_path: qlib.contrib.model.gbdt + kwargs: + loss: mse + colsample_bytree: 0.8879 + learning_rate: 0.2 + subsample: 0.8789 + lambda_l1: 205.6999 + lambda_l2: 580.9768 + max_depth: 8 + num_leaves: 210 + num_threads: 20 + dataset: + class: 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] + record: + - class: SignalRecord + module_path: qlib.workflow.record_temp + kwargs: + model: + dataset: + - class: SigAnaRecord + module_path: qlib.workflow.record_temp + kwargs: + ana_long_short: False + ann_scaler: 252 + - class: PortAnaRecord + module_path: qlib.workflow.record_temp + kwargs: + config: *port_analysis_config \ No newline at end of file diff --git a/rdagent/scenarios/qlib/task_generator/env_factor/read_exp_res.py b/rdagent/scenarios/qlib/task_generator/env_factor/read_exp_res.py new file mode 100644 index 00000000..6e05ec5e --- /dev/null +++ b/rdagent/scenarios/qlib/task_generator/env_factor/read_exp_res.py @@ -0,0 +1,53 @@ +import os +import pickle +from pathlib import Path + +import pandas as pd +import qlib +from mlflow.entities import ViewType +from mlflow.tracking import MlflowClient + +qlib.init() + +from qlib.workflow import R + +# here is the documents of the https://qlib.readthedocs.io/en/latest/component/recorder.html + +# TODO: list all the recorder and metrics + +# Assuming you have already listed the experiments +experiments = R.list_experiments() + +# Iterate through each experiment to find the latest recorder +experiment_name = None +latest_recorder = None +for experiment in experiments: + # print(f"Experiment: {experiment}") + recorders = R.list_recorders(experiment_name=experiment) + for recorder_id in recorders: + if recorder_id is not None: + experiment_name = experiment + recorder = R.get_recorder(recorder_id=recorder_id, experiment_name=experiment) + end_time = recorder.info["end_time"] + if latest_recorder is None or end_time > latest_recorder.info["end_time"]: + latest_recorder = recorder + +# Check if the latest recorder is found +if latest_recorder is None: + print("No recorders found") +else: + print(f"Latest recorder: {latest_recorder}") + + # Load the specified file from the latest recorder + file_path = "portfolio_analysis/port_analysis_1day.pkl" + indicator_analysis_df = latest_recorder.load_object(file_path) + + # Optionally convert to DataFrame if not already in DataFrame format + if not isinstance(indicator_analysis_df, pd.DataFrame): + indicator_analysis_df = pd.DataFrame(indicator_analysis_df) + + output_path = os.path.join(str(Path(__file__).resolve().parent), "qlib_res.pkl") + with open(output_path, "wb") as f: + pickle.dump(indicator_analysis_df, f) + + print(f"Output has been saved to {output_path}") diff --git a/rdagent/scenarios/qlib/task_generator/feedback.py b/rdagent/scenarios/qlib/task_generator/feedback.py index 4054ef9f..4629f2cc 100644 --- a/rdagent/scenarios/qlib/task_generator/feedback.py +++ b/rdagent/scenarios/qlib/task_generator/feedback.py @@ -1,13 +1,101 @@ # TODO: # Implement to feedback. + import json -from rdagent.oai.llm_utils import APIBackend -from rdagent.core.proposal import HypothesisExperiment2Feedback, Trace, Hypothesis, HypothesisFeedback, Scenario +from pathlib import Path + +from jinja2 import Environment, StrictUndefined + from rdagent.core.experiment import Experiment +from rdagent.core.log import RDAgentLog +from rdagent.core.prompts import Prompts +from rdagent.core.proposal import ( + Hypothesis, + HypothesisExperiment2Feedback, + HypothesisFeedback, + Trace, +) +from rdagent.oai.llm_utils import APIBackend + +feedback_prompts = Prompts(file_path=Path(__file__).parent.parent / "prompts.yaml") +DIRNAME = Path(__file__).absolute().resolve().parent +logger = RDAgentLog() -class QlibFactorHypothesisExperiment2Feedback(HypothesisExperiment2Feedback): ... +class QlibFactorHypothesisExperiment2Feedback(HypothesisExperiment2Feedback): + def generateFeedback(self, exp: Experiment, hypothesis: Hypothesis, trace: Trace) -> HypothesisFeedback: + """ + Generate feedback for the given experiment and hypothesis. + Args: + exp (QlibFactorExperiment): The experiment to generate feedback for. + hypothesis (QlibFactorHypothesis): The hypothesis to generate feedback for. + trace (Trace): The trace of the experiment. + + Returns: + Any: The feedback generated for the given experiment and hypothesis. + """ + logger.info("Generating feedback...") + hypothesis_text = hypothesis.hypothesis + current_result = exp.result + tasks_factors = [task.get_task_information() for task in exp.sub_tasks] + sota_result = exp.based_experiments[-1].result + + # Generate the system prompt + sys_prompt = ( + Environment(undefined=StrictUndefined) + .from_string(feedback_prompts["data_feedback_generation"]["system"]) + .render(scenario=self.scen.get_scenario_all_desc()) + ) + + # Generate the user prompt + usr_prompt = ( + Environment(undefined=StrictUndefined) + .from_string(feedback_prompts["data_feedback_generation"]["user"]) + .render( + hypothesis_text=hypothesis_text, + task_details=tasks_factors, + current_result=current_result, + sota_result=sota_result, + ) + ) + + # Call the APIBackend to generate the response for hypothesis feedback + response = APIBackend().build_messages_and_create_chat_completion( + user_prompt=usr_prompt, + system_prompt=sys_prompt, + json_mode=True, + ) + + # Parse the JSON response to extract the feedback + response_json = json.loads(response) + + # Extract fields from JSON response + observations = response_json.get("Observations", "No observations provided") + hypothesis_evaluation = response_json.get("Feedback for Hypothesis", "No feedback provided") + new_hypothesis = response_json.get("New Hypothesis", "No new hypothesis provided") + reason = response_json.get("Reasoning", "No reasoning provided") + decision = response_json.get("Replace Best Result", "no").lower() == "yes" + + # Create HypothesisFeedback object + hypothesis_feedback = HypothesisFeedback( + observations=observations, + hypothesis_evaluation=hypothesis_evaluation, + new_hypothesis=new_hypothesis, + reason=reason, + decision=decision, + ) + + logger.info( + "Generated Hypothesis Feedback:\n" + f"Observations: {observations}\n" + f"Feedback for Hypothesis: {hypothesis_evaluation}\n" + f"New Hypothesis: {new_hypothesis}\n" + f"Reason: {reason}\n" + f"Replace Best Result: {'Yes' if decision else 'No'}" + ) + + return hypothesis_feedback class QlibModelHypothesisExperiment2Feedback(HypothesisExperiment2Feedback): @@ -40,7 +128,7 @@ class QlibModelHypothesisExperiment2Feedback(HypothesisExperiment2Feedback): else: last_info_str = "This is the first round. No previous information available." - usr_prompt_hypothesis = f''' + usr_prompt_hypothesis = f""" 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}. {last_info_str} @@ -51,8 +139,8 @@ class QlibModelHypothesisExperiment2Feedback(HypothesisExperiment2Feedback): Result: {exp.result}\n 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 reasonings, and results in mind (comparison), provide detailed and constructive feedback and suggest a new hypothesis. - ''' + Hence, with the hypotheses, relevant reasoning, and results in mind (comparison), provide detailed and constructive feedback and suggest a new hypothesis. + """ try: # Call the APIBackend to generate the response for hypothesis feedback @@ -61,7 +149,7 @@ class QlibModelHypothesisExperiment2Feedback(HypothesisExperiment2Feedback): system_prompt=sys_prompt_hypothesis, json_mode=True, ) - + # Parse the JSON response to extract the feedback response_json_hypothesis = json.loads(response_hypothesis) hypothesis_feedback = HypothesisFeedback( @@ -69,13 +157,13 @@ class QlibModelHypothesisExperiment2Feedback(HypothesisExperiment2Feedback): hypothesis_evaluation=response_json_hypothesis.get("Feedback for Hypothesis", "No feedback provided"), new_hypothesis=response_json_hypothesis.get("New Hypothesis", "No new hypothesis provided"), reason=response_json_hypothesis.get("Reasoning", "No reasoning provided"), - decision=response_json_hypothesis.get("Decision", "false").lower() == "true" + decision=response_json_hypothesis.get("Decision", "false").lower() == "true", ) return hypothesis_feedback except json.JSONDecodeError as e: - # TODO: (Xiao) I think raising a specific type of ERROR to make caller know sth bad has happend would be more reasonable + # TODO: (Xiao) I think raising a specific type of ERROR to make caller know sth bad has happened would be more reasonable print("Error parsing JSON response from LLM for hypothesis feedback:", e) except Exception as e: print("An unexpected error occurred while generating hypothesis feedback:", e) @@ -85,6 +173,5 @@ class QlibModelHypothesisExperiment2Feedback(HypothesisExperiment2Feedback): hypothesis_evaluation="No feedback", new_hypothesis="No new hypothesis", reason="No reasoning", - decision=False + decision=False, ) - diff --git a/rdagent/utils/env.py b/rdagent/utils/env.py index 8d079d6a..fb07f61e 100644 --- a/rdagent/utils/env.py +++ b/rdagent/utils/env.py @@ -5,14 +5,21 @@ Tries to create uniform environment for the agent to run; - All the code and data is expected included in one folder """ + import os -import sys -import docker import subprocess +import sys from abc import abstractmethod -from pydantic import BaseModel -from typing import Generic, TypeVar, Optional, Dict from pathlib import Path +from typing import Dict, Generic, Optional, TypeVar + +import docker +import docker.models +import docker.models.containers +from pydantic import BaseModel +from pydantic_settings import BaseSettings + +from rdagent.core.log import RDAgentLog ASpecificBaseModel = TypeVar("ASpecificBaseModel", bound=BaseModel) @@ -71,6 +78,7 @@ class LocalEnv(Env[LocalConf]): """ Sometimes local environment may be more convinient for testing """ + def prepare(self): if not (Path("~/.qlib/qlib_data/cn_data").expanduser().resolve().exists()): self.run( @@ -79,10 +87,7 @@ class LocalEnv(Env[LocalConf]): else: print("Data already exists. Download skipped.") - def run(self, - entry: str | None = None, - local_path: Optional[str] = None, - env: dict | None = None) -> str: + def run(self, entry: str | None = None, local_path: Optional[str] = None, env: dict | None = None) -> str: if env is None: env = {} @@ -94,15 +99,7 @@ class LocalEnv(Env[LocalConf]): cwd = None if local_path: cwd = Path(local_path).resolve() - print(f"CWD: {cwd}") - - result = subprocess.run( - command, - cwd=cwd, - env={**os.environ, **env}, - capture_output=True, - text=True - ) + result = subprocess.run(command, cwd=cwd, env={**os.environ, **env}, capture_output=True, text=True) if result.returncode != 0: raise RuntimeError(f"Error while running the command: {result.stderr}") @@ -113,8 +110,10 @@ class LocalEnv(Env[LocalConf]): ## Docker Environment ----- -class DockerConf(BaseModel): - image: str # the image you want to run +class DockerConf(BaseSettings): + build_from_dockerfile: bool = False + dockerfile_folder_path: Path # the path to the dockerfile + image: str # the image you want to build mount_path: str # the path in the docker image to mount the folder default_entry: str # the entry point of the image @@ -122,14 +121,16 @@ class DockerConf(BaseModel): # Sometime, we need maintain some extra data for the workspace. # And the extra data may be shared and the downloading can be time consuming. # So we just want to download it once. + network: str | None = "bridge" # the network mode for the docker -QLIB_TORCH_IMAGE = DockerConf( - image="linlanglv/qlib_image_nightly_pytorch:nightly", - mount_path="/workspace", - default_entry="qrun conf.yaml", - extra_volumes={Path("~/.qlib/").expanduser().resolve(): "/root/.qlib/"}, -) +class QlibDockerConf(DockerConf): + build_from_dockerfile: bool = True + dockerfile_folder_path: Path = Path(__file__).parent.parent / "scenarios" / "qlib" / "docker" + image: str = "local_qlib:latest" + mount_path: str = "/workspace/qlib_workspace/" + default_entry: str = "qrun conf.yaml" + extra_volumes: dict = {Path("~/.qlib/").expanduser().resolve(): "/root/.qlib/"} class DockerEnv(Env[DockerConf]): @@ -140,6 +141,12 @@ class DockerEnv(Env[DockerConf]): Download image if it doesn't exist """ client = docker.from_env() + if self.conf.build_from_dockerfile is not None and self.conf.dockerfile_folder_path.exists(): + RDAgentLog().info(f"Building the image from dockerfile: {self.conf.dockerfile_folder_path}") + image, logs = client.images.build( + path=str(self.conf.dockerfile_folder_path), tag=self.conf.image, network_mode=self.conf.network + ) + RDAgentLog().info(f"Finished building the image from dockerfile: {self.conf.dockerfile_folder_path}") try: client.images.get(self.conf.image) except docker.errors.ImageNotFound: @@ -164,14 +171,15 @@ class DockerEnv(Env[DockerConf]): log_output = "" try: - container = client.containers.run( + container: docker.models.containers.Container = client.containers.run( image=self.conf.image, command=entry, volumes=volumns, environment=env, detach=True, working_dir=self.conf.mount_path, - auto_remove=True, + # auto_remove=True, # remove too fast might cause the logs not to be get + network=self.conf.network, ) logs = container.logs(stream=True) for log in logs: @@ -179,6 +187,8 @@ class DockerEnv(Env[DockerConf]): print(decoded_log) log_output += decoded_log + "\n" container.wait() + container.stop() + container.remove() return log_output except docker.errors.ContainerError as e: raise RuntimeError(f"Error while running the container: {e}") @@ -191,7 +201,7 @@ class DockerEnv(Env[DockerConf]): class QTDockerEnv(DockerEnv): """Qlib Torch Docker""" - def __init__(self, conf: DockerConf = QLIB_TORCH_IMAGE): + def __init__(self, conf: DockerConf = QlibDockerConf()): super().__init__(conf) def prepare(self): @@ -201,7 +211,8 @@ class QTDockerEnv(DockerEnv): super().prepare() qlib_data_path = next(iter(self.conf.extra_volumes.keys())) if not (Path(qlib_data_path) / "qlib_data" / "cn_data").exists(): + RDAgentLog().info("We are downloading!") cmd = "python -m qlib.run.get_data qlib_data --target_dir ~/.qlib/qlib_data/cn_data --region cn --interval 1d --delete_old False" self.run(entry=cmd) else: - print("Data already exists. Download skipped.") + RDAgentLog().info("Data already exists. Download skipped.") diff --git a/requirements/package.txt b/requirements/package.txt index 092e94dc..0f2ee084 100644 --- a/requirements/package.txt +++ b/requirements/package.txt @@ -18,6 +18,7 @@ matplotlib langchain tiktoken scikit-learn +docker # azure identity related azure.identity diff --git a/test/utils/README.md b/test/utils/README.md new file mode 100644 index 00000000..23a36a14 --- /dev/null +++ b/test/utils/README.md @@ -0,0 +1,116 @@ +# 🐳 Run Docker & Qlib +--- + +## 📄 Description +This guide explains how to run the Qlib Docker test file located at `test/utils/test_env.py` in the RD-Agent repository. + +--- + +## 🚀 Running Instructions + +### 1. Install the required Python libraries +- Ensure that the `docker` Python library is installed: + ```sh + pip install docker + ``` + +### 2. Run the test script +- Execute the test script to verify the Docker environment setup: + ```sh + python test/utils/test_env.py + ``` + +### Troubleshooting +- **PermissionError: [Errno 13] Permission denied.** + > This error occurs when the current user does not have the necessary permissions to access the Docker socket. To resolve this issue, follow these steps: + +1. **Add the current user to the `docker` group** +Docker requires root or `docker` group user permissions to access the Docker socket. Add the current user to the `docker` group: + ```sh + sudo usermod -aG docker $USER + ``` + +2. **Refresh group changes** +To apply the group changes, log out and log back in, or use the following command: + ```sh + newgrp docker + ``` + +3. **Verify Docker access** +Run the following command to ensure that Docker can be accessed: + ```sh + docker run hello-world + ``` + +4. **Rerun the test script** + After completing these steps, rerun the test script: + ```sh + python test/utils/test_env.py + ``` +--- +## 🛠️ Detailed Qlib Docker Function Framework + +Here, we provide an overview of the specific functions within the Qlib Docker framework, their purposes, and examples of how to call them. + +### QTDockerEnv Class in `env.py` + +The `QTDockerEnv` class is responsible for setting up and running Docker environments for Qlib experiments. + +#### Methods: + +1. **prepare()** + - **Purpose**: Prepares the Docker environment for running experiments. This includes building the Docker image if necessary. + - **Example**: + ```python + qtde = QTDockerEnv() + qtde.prepare() + ``` + +2. **run(local_path: str, entry: str) -> str** + - **Purpose**: Runs a specified entry point (e.g., a configuration file) in the prepared Docker environment. + - **Parameters**: + - `local_path`: Path to the local directory to mount into the Docker container. + - `entry`: Command or entry point to run inside the Docker container. + - **Returns**: The stdout output from the Docker container. + - **Example**: + ```python + result = qtde.run(local_path="/path/to/env_tpl", entry="qrun conf.yaml") + ``` +--- +### 📊 Expected Output + +Upon successful execution, the test script will produce analysis results of benchmark returns and various risk metrics. The expected output should be similar to: + +``` +'The following are analysis results of benchmark return (1 day).' +risk +mean 0.000477 +std 0.012295 +annualized_return 0.113561 +information_ratio 0.598699 +max_drawdown -0.370479 + +'The following are analysis results of the excess return without cost (1 day).' +risk +mean 0.000530 +std 0.005718 +annualized_return 0.126029 +information_ratio 1.428574 +max_drawdown -0.072310 + +'The following are analysis results of the excess return with cost (1 day).' +risk +mean 0.000339 +std 0.005717 +annualized_return 0.080654 +information_ratio 0.914486 +max_drawdown -0.086083 + +'The following are analysis results of indicators (1 day).' +value +ffr 1.0 +pa 0.0 +pos 0.0 +``` + +By following these steps and using the provided functions, you should be able to run the Qlib Docker tests and obtain the expected analysis results. \ No newline at end of file diff --git a/test/utils/test_env.py b/test/utils/test_env.py index 2f41764f..79c7a901 100644 --- a/test/utils/test_env.py +++ b/test/utils/test_env.py @@ -2,10 +2,11 @@ import os import sys import unittest from pathlib import Path + sys.path.append(str(Path(__file__).resolve().parent.parent)) -from rdagent.utils.env import QTDockerEnv, LocalEnv, LocalConf import shutil +from rdagent.utils.env import LocalConf, LocalEnv, QTDockerEnv DIRNAME = Path(__file__).absolute().resolve().parent @@ -23,18 +24,17 @@ class EnvUtils(unittest.TestCase): # NOTE: Since I don't know the exact environment in which it will be used, here's just an example. # NOTE: Because you need to download the data during the prepare process. So you need to have pyqlib in your environment. - # def test_local(self): - # local_conf = LocalConf( - # py_bin="/home/v-linlanglv/miniconda3/envs/RD-Agent-310/bin", - # default_entry="qrun conf.yaml", - # ) - # qle = LocalEnv(conf=local_conf) - # qle.prepare() - # exe_path = str(DIRNAME / "env_tpl") - # conf_path = str(DIRNAME / "env_tpl" / "conf.yaml") - # qle.run(entry="qrun " + conf_path, local_path=exe_path) - # mlrun_p = DIRNAME / "env_tpl" / "mlruns" - # self.assertTrue(mlrun_p.exists(), f"Expected output file {mlrun_p} not found") + def test_local(self): + local_conf = LocalConf( + py_bin="/home/v-linlanglv/miniconda3/envs/RD-Agent-310/bin", + default_entry="qrun conf.yaml", + ) + qle = LocalEnv(conf=local_conf) + qle.prepare() + conf_path = str(DIRNAME / "env_tpl" / "conf.yaml") + qle.run(entry="qrun " + conf_path) + mlrun_p = DIRNAME / "env_tpl" / "mlruns" + self.assertTrue(mlrun_p.exists(), f"Expected output file {mlrun_p} not found") def test_docker(self): """ @@ -42,16 +42,15 @@ class EnvUtils(unittest.TestCase): And run the docker image with `qrun conf.yaml` """ qtde = QTDockerEnv() - qtde.prepare() qtde.prepare() # you can prepare for multiple times. It is expected to handle it correctly # the stdout are returned as result result = qtde.run(local_path=str(DIRNAME / "env_tpl"), entry="qrun conf.yaml") - - mlrun_p = DIRNAME / "env_tpl" / "mlruns" + + mlrun_p = DIRNAME / "env_tpl" / "mlruns" self.assertTrue(mlrun_p.exists(), f"Expected output file {mlrun_p} not found") # read experiment - result = qtde.run(local_path=str(DIRNAME / "env_tpl"), entry="python read_exp.py") + result = qtde.run(local_path=str(DIRNAME / "env_tpl"), entry="python read_exp_res.py") print(result)