From 7a1abab73f2975bb2efcead65275438964cf7316 Mon Sep 17 00:00:00 2001 From: WinstonLiyt <1957922024@qq.com> Date: Thu, 11 Jul 2024 08:49:37 +0000 Subject: [PATCH 01/12] re-commit --- .gitignore | 5 + rdagent/app/qlib_rd_loop/conf.py | 6 +- rdagent/app/qlib_rd_loop/factor.py | 19 +- rdagent/core/experiment.py | 7 + rdagent/core/proposal.py | 7 +- rdagent/scenarios/qlib/prompts.yaml | 34 ++- rdagent/scenarios/qlib/task_generator/data.py | 201 +++++++++++++++++- .../scenarios/qlib/task_generator/feedback.py | 91 +++++++- test/utils/README.md | 116 ++++++++++ test/utils/test_env.py | 26 +-- test/utils/test_env2.py | 38 ++++ 11 files changed, 527 insertions(+), 23 deletions(-) create mode 100644 test/utils/README.md create mode 100644 test/utils/test_env2.py diff --git a/.gitignore b/.gitignore index b2c38a81..a1dd635e 100644 --- a/.gitignore +++ b/.gitignore @@ -153,3 +153,8 @@ git_ignore_folder/ # DB files *.db + +# Docker +env_factor/ +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..f09ea203 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,7 @@ class PropSetting(BaseSettings): qlib_model_summarizer: str = "rdagent.scenarios.qlib.task_generator.feedback.QlibModelHypothesisExperiment2Feedback" evolving_n: int = 10 - - + + py_bin: str = "/usr/bin/python" + PROP_SETTING = PropSetting() diff --git a/rdagent/app/qlib_rd_loop/factor.py b/rdagent/app/qlib_rd_loop/factor.py index cfabe90a..0f5f5de8 100644 --- a/rdagent/app/qlib_rd_loop/factor.py +++ b/rdagent/app/qlib_rd_loop/factor.py @@ -27,7 +27,7 @@ hypothesis2experiment: Hypothesis2Experiment = import_class(PROP_SETTING.qlib_fa 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) @@ -39,3 +39,20 @@ for _ in range(PROP_SETTING.evolving_n): feedback = qlib_factor_summarizer.generateFeedback(exp, hypothesis, trace) trace.hist.append((hypothesis, exp, feedback)) + + +""" +trace = Trace(scen=scen) +# for _ in range(PROP_SETTING.evolving_n): +for _ in range(1): + hypothesis = hypothesis_gen.gen(trace) + exp = hypothesis2experiment.convert(hypothesis, trace) + # exp = qlib_factor_coder.generate(exp) + import pickle + file_path = '/home/finco/v-yuanteli/RD-Agent/git_ignore_folder/factor_data_output/exp_new.pkl' + with open(file_path, 'rb') as file: + exp = pickle.load(file) + exp = qlib_factor_runner.generate(exp) + feedback = qlib_factor_summarizer.generateFeedback(exp, hypothesis, trace) + # trace.hist.append((hypothesis, exp, feedback)) +""" \ No newline at end of file diff --git a/rdagent/core/experiment.py b/rdagent/core/experiment.py index 42125da7..e0cb6dfc 100644 --- a/rdagent/core/experiment.py +++ b/rdagent/core/experiment.py @@ -18,6 +18,10 @@ 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 +89,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): """ @@ -112,12 +117,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 93e20453..30370d21 100644 --- a/rdagent/core/proposal.py +++ b/rdagent/core/proposal.py @@ -92,9 +92,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/prompts.yaml b/rdagent/scenarios/qlib/prompts.yaml index a10ef598..7481c734 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" } # 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}} + 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..ab72dc08 100644 --- a/rdagent/scenarios/qlib/task_generator/data.py +++ b/rdagent/scenarios/qlib/task_generator/data.py @@ -1,6 +1,28 @@ +from pathlib import Path +import shutil +from typing import List +import pandas as pd +import pickle +from rdagent.app.qlib_rd_loop.conf import PROP_SETTING from rdagent.core.task_generator import TaskGenerator +from rdagent.utils.env import QTDockerEnv, LocalConf, LocalEnv from rdagent.scenarios.qlib.experiment.factor_experiment import QlibFactorExperiment +from rdagent.core.log import RDAgentLog +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") class QlibFactorRunner(TaskGenerator[QlibFactorExperiment]): """ @@ -13,6 +35,183 @@ class QlibFactorRunner(TaskGenerator[QlibFactorExperiment]): - TODO: implement a qlib handler """ + + def FetchAlpha158ResultFromDocker(self): + """ + Run Docker to get alpha158 result. + + This method prepares the Qlib Docker environment, executes the necessary commands to + run the backtest, and fetches the results stored in a pickle file. + + Returns: + Any: The alpha158 result. If successful, returns a pandas DataFrame. Otherwise, returns None. + """ + # Initialize and prepare the Qlib Docker environment + qtde = QTDockerEnv() + qtde.prepare() + + # Clean up any previous run artifacts by deleting the mlruns directory + result = qtde.run(local_path=str(DIRNAME / "env_factor"), entry="rm -r mlruns", env={"PYTHONPATH": "./"}) + + # Run the Qlib backtest using the configuration file conf.yaml + result = qtde.run(local_path=str(DIRNAME / "env_factor"), entry="qrun conf.yaml", env={"PYTHONPATH": "./"}) + + # Execute a Python script to extract the experiment results + result = 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) + + # Check if the loaded result is a pandas DataFrame and not empty + if isinstance(result, pd.DataFrame): + if not result.empty: + logger.info("Successfully retrieved alpha158 result.") + return result + else: + logger.error("Result DataFrame is empty.") + return None + else: + logger.error("Data format error.") + return None + 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. + """ + + SOTA_factor = self.process_factor_data(exp.based_experiments) + + if exp.based_experiments[-1].result is None: + exp.based_experiments[-1].result = self.FetchAlpha158ResultFromDocker() + + # 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: + 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 + result = qtde.run(local_path=str(DIRNAME / "env_factor"), entry="rm -r mlruns", env={"PYTHONPATH": "./"}) + # Run the Qlib backtest + result = qtde.run(local_path=str(DIRNAME / "env_factor"), entry="qrun conf_combined.yaml", env={"PYTHONPATH": "./"}) + + result = 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) + """ + + # Local run + # Clean up any previous run artifacts by deleting the mlruns directory + mlruns_path = DIRNAME_local / 'mlruns' / '1' + if mlruns_path.exists() and mlruns_path.is_dir(): + shutil.rmtree(mlruns_path) + + # Prepare local Qlib environment + local_conf = LocalConf( + py_bin=PROP_SETTING.py_bin, + default_entry="qrun conf_combined.yaml", + ) + qle = LocalEnv(conf=local_conf) + qle.prepare() + conf_path = str(DIRNAME / "env_factor" / "conf_combined.yaml") + qle.run(entry="qrun " + conf_path) + + # Verify if the new folder is created + mlrun_p = DIRNAME_local / 'mlruns' / '1' + assert mlrun_p.exists(), f"Expected output file {mlrun_p} not found" + + # Locate the newly generated folder in mlruns/1/ + new_folders = [folder for folder in mlrun_p.iterdir() if folder.is_dir()] + if not new_folders: + raise FileNotFoundError("No new folders found in 'mlruns/1/'.") + + new_folder = new_folders[0] # Assuming there's only one new folder + pickle_file = new_folder / 'artifacts' / 'portfolio_analysis' / 'port_analysis_1day.pkl' + assert pickle_file.exists(), f"Expected pickle file {pickle_file} not found" + + with open(pickle_file, 'rb') as f: + result = pickle.load(f) + + exp.result = 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() + + # Check if factor generation was successful + if 'Execution succeeded without error.\nExpected output file found.' in message: + factor_dfs.append(df) + + # Combine all successful factor data + if factor_dfs: + combined_factors = pd.concat(factor_dfs, axis=1) + + # Remove rows with NaN values + combined_factors = combined_factors.dropna() + + # print(combined_factors) + return combined_factors + 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/feedback.py b/rdagent/scenarios/qlib/task_generator/feedback.py index 0c6a09dd..fee41d26 100644 --- a/rdagent/scenarios/qlib/task_generator/feedback.py +++ b/rdagent/scenarios/qlib/task_generator/feedback.py @@ -1,10 +1,95 @@ # TODO: # Implement to feedback. +from pathlib import Path + +from jinja2 import Environment, StrictUndefined +from rdagent.core.prompts import Prompts from rdagent.core.proposal import HypothesisExperiment2Feedback +from rdagent.core.proposal import Trace +from rdagent.core.experiment import Experiment +from rdagent.core.proposal import Hypothesis, HypothesisFeedback +from rdagent.oai.llm_utils import APIBackend +from rdagent.utils.env import QTDockerEnv +from rdagent.core.log import RDAgentLog +import json +import pandas as pd +import pickle - -class QlibFactorHypothesisExperiment2Feedback(HypothesisExperiment2Feedback): ... - +feedback_prompts = Prompts(file_path=Path(__file__).parent.parent / "prompts.yaml") +DIRNAME = Path(__file__).absolute().resolve().parent +logger = RDAgentLog() class QlibModelHypothesisExperiment2Feedback(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_factor_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()) + + + # Prepare task details + task_details = "\n".join([f"Task: {factor_name}, Factor: {factor_description}" for factor_name, factor_description in tasks_factors]) + + # Generate the user prompt + usr_prompt = Environment(undefined=StrictUndefined).from_string(feedback_prompts["data_feedback_generation"]["user"]).format( + hypothesis_text=hypothesis_text, + task_details=task_details, + 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 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..f108d320 100644 --- a/test/utils/test_env.py +++ b/test/utils/test_env.py @@ -23,18 +23,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): """ @@ -51,7 +50,8 @@ class EnvUtils(unittest.TestCase): 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("here") print(result) diff --git a/test/utils/test_env2.py b/test/utils/test_env2.py new file mode 100644 index 00000000..543e89a0 --- /dev/null +++ b/test/utils/test_env2.py @@ -0,0 +1,38 @@ +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 + + +DIRNAME = Path(__file__).absolute().resolve().parent + + +class EnvUtils(unittest.TestCase): + def setUp(self): + pass + + def test_docker(self): + """ + We will mount `env_tpl` into the docker image. + 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 conf2.yaml") + + 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_res.py") + print("here") + # print(result) + + +if __name__ == "__main__": + unittest.main() From f84e90525e6a0030a3085e9c4f5906ff2c31cf72 Mon Sep 17 00:00:00 2001 From: WinstonLiyt <1957922024@qq.com> Date: Thu, 11 Jul 2024 09:06:52 +0000 Subject: [PATCH 02/12] fix based_experiments bug --- rdagent/components/coder/factor_coder/CoSTEER/__init__.py | 1 + .../coder/factor_coder/CoSTEER/evolving_strategy.py | 6 +++--- rdagent/components/coder/model_coder/CoSTEER/__init__.py | 1 + rdagent/scenarios/qlib/factor_proposal.py | 2 ++ 4 files changed, 7 insertions(+), 3 deletions(-) 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/evolving_strategy.py b/rdagent/components/coder/factor_coder/CoSTEER/evolving_strategy.py index fcda9ad8..94e1af9b 100644 --- a/rdagent/components/coder/factor_coder/CoSTEER/evolving_strategy.py +++ b/rdagent/components/coder/factor_coder/CoSTEER/evolving_strategy.py @@ -149,7 +149,7 @@ class FactorEvolvingStrategy(MultiProcessEvolvingStrategy): queried_former_failed_knowledge=queried_former_failed_knowledge_to_render, ) ) - session = APIBackend(use_chat_cache=False).build_chat_session( + session = APIBackend(use_chat_cache=True).build_chat_session( session_system_prompt=system_prompt, ) @@ -249,7 +249,7 @@ class FactorEvolvingStrategyWithGraph(MultiProcessEvolvingStrategy): ) ) - session = APIBackend(use_chat_cache=False).build_chat_session( + session = APIBackend(use_chat_cache=True).build_chat_session( session_system_prompt=system_prompt, ) @@ -276,7 +276,7 @@ class FactorEvolvingStrategyWithGraph(MultiProcessEvolvingStrategy): ) .strip("\n") ) - session_summary = APIBackend(use_chat_cache=False).build_chat_session( + session_summary = APIBackend(use_chat_cache=True).build_chat_session( session_system_prompt=error_summary_system_prompt, ) for _ in range(10): # max attempt to reduce the length of error_summary_user_prompt 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/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 From 033589bbe0657f467f780ecdca5728cb413fee36 Mon Sep 17 00:00:00 2001 From: WinstonLiyt <1957922024@qq.com> Date: Thu, 11 Jul 2024 11:24:41 +0000 Subject: [PATCH 03/12] String together the entire factor process --- rdagent/app/qlib_rd_loop/conf.py | 1 + rdagent/app/qlib_rd_loop/factor.py | 20 ++----------------- .../factor_coder/CoSTEER/evolving_strategy.py | 6 +++--- rdagent/scenarios/qlib/prompts.yaml | 8 ++++---- rdagent/scenarios/qlib/task_generator/data.py | 12 ++++++----- .../scenarios/qlib/task_generator/feedback.py | 9 ++------- 6 files changed, 19 insertions(+), 37 deletions(-) diff --git a/rdagent/app/qlib_rd_loop/conf.py b/rdagent/app/qlib_rd_loop/conf.py index f09ea203..5e8b8b14 100644 --- a/rdagent/app/qlib_rd_loop/conf.py +++ b/rdagent/app/qlib_rd_loop/conf.py @@ -25,5 +25,6 @@ class PropSetting(BaseSettings): 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 0f5f5de8..32bbbb65 100644 --- a/rdagent/app/qlib_rd_loop/factor.py +++ b/rdagent/app/qlib_rd_loop/factor.py @@ -25,6 +25,7 @@ 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)(scen) @@ -38,21 +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 = Trace(scen=scen) -# for _ in range(PROP_SETTING.evolving_n): -for _ in range(1): - hypothesis = hypothesis_gen.gen(trace) - exp = hypothesis2experiment.convert(hypothesis, trace) - # exp = qlib_factor_coder.generate(exp) - import pickle - file_path = '/home/finco/v-yuanteli/RD-Agent/git_ignore_folder/factor_data_output/exp_new.pkl' - with open(file_path, 'rb') as file: - exp = pickle.load(file) - exp = qlib_factor_runner.generate(exp) - feedback = qlib_factor_summarizer.generateFeedback(exp, hypothesis, trace) - # trace.hist.append((hypothesis, exp, feedback)) -""" \ No newline at end of file + trace.hist.append((hypothesis, exp, feedback)) \ No newline at end of file diff --git a/rdagent/components/coder/factor_coder/CoSTEER/evolving_strategy.py b/rdagent/components/coder/factor_coder/CoSTEER/evolving_strategy.py index 94e1af9b..fcda9ad8 100644 --- a/rdagent/components/coder/factor_coder/CoSTEER/evolving_strategy.py +++ b/rdagent/components/coder/factor_coder/CoSTEER/evolving_strategy.py @@ -149,7 +149,7 @@ class FactorEvolvingStrategy(MultiProcessEvolvingStrategy): queried_former_failed_knowledge=queried_former_failed_knowledge_to_render, ) ) - session = APIBackend(use_chat_cache=True).build_chat_session( + session = APIBackend(use_chat_cache=False).build_chat_session( session_system_prompt=system_prompt, ) @@ -249,7 +249,7 @@ class FactorEvolvingStrategyWithGraph(MultiProcessEvolvingStrategy): ) ) - session = APIBackend(use_chat_cache=True).build_chat_session( + session = APIBackend(use_chat_cache=False).build_chat_session( session_system_prompt=system_prompt, ) @@ -276,7 +276,7 @@ class FactorEvolvingStrategyWithGraph(MultiProcessEvolvingStrategy): ) .strip("\n") ) - session_summary = APIBackend(use_chat_cache=True).build_chat_session( + session_summary = APIBackend(use_chat_cache=False).build_chat_session( session_system_prompt=error_summary_system_prompt, ) for _ in range(10): # max attempt to reduce the length of error_summary_user_prompt diff --git a/rdagent/scenarios/qlib/prompts.yaml b/rdagent/scenarios/qlib/prompts.yaml index 7481c734..af79c378 100644 --- a/rdagent/scenarios/qlib/prompts.yaml +++ b/rdagent/scenarios/qlib/prompts.yaml @@ -67,13 +67,13 @@ data_feedback_generation: } user: |- Target hypothesis: - {{hypothesis}} + {{ hypothesis_text }} Tasks and Factors: - {{task_details}} + {{ task_details }} Current Result: - {{current_result}} + {{ current_result }} SOTA 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. diff --git a/rdagent/scenarios/qlib/task_generator/data.py b/rdagent/scenarios/qlib/task_generator/data.py index ab72dc08..ec2b4ed0 100644 --- a/rdagent/scenarios/qlib/task_generator/data.py +++ b/rdagent/scenarios/qlib/task_generator/data.py @@ -24,6 +24,8 @@ logger = RDAgentLog() # de = DockerEnv() # de.run(local_path=self.ws_path, entry="qrun conf.yaml") +# TODO: supporting multiprocessing and keep previous results + class QlibFactorRunner(TaskGenerator[QlibFactorExperiment]): """ Docker run @@ -32,8 +34,6 @@ class QlibFactorRunner(TaskGenerator[QlibFactorExperiment]): - price-volume data dumper - `data.py` + Adaptor to Factor implementation - results in `mlflow` - - - TODO: implement a qlib handler """ def FetchAlpha158ResultFromDocker(self): @@ -86,8 +86,9 @@ class QlibFactorRunner(TaskGenerator[QlibFactorExperiment]): Generate the experiment by processing and combining factor data, then passing the combined data to Docker for backtest results. """ - - SOTA_factor = self.process_factor_data(exp.based_experiments) + SOTA_factor = None + if exp.based_experiments.__len__() != 1: + SOTA_factor = self.process_factor_data(exp.based_experiments) if exp.based_experiments[-1].result is None: exp.based_experiments[-1].result = self.FetchAlpha158ResultFromDocker() @@ -133,6 +134,7 @@ class QlibFactorRunner(TaskGenerator[QlibFactorExperiment]): result = pickle.load(f) """ + # TODO: Implement the Docker run in the following way # Local run # Clean up any previous run artifacts by deleting the mlruns directory mlruns_path = DIRNAME_local / 'mlruns' / '1' @@ -147,7 +149,7 @@ class QlibFactorRunner(TaskGenerator[QlibFactorExperiment]): qle = LocalEnv(conf=local_conf) qle.prepare() conf_path = str(DIRNAME / "env_factor" / "conf_combined.yaml") - qle.run(entry="qrun " + conf_path) + qle.run(entry="qrun " + conf_path, local_path=PROP_SETTING.local_qlib_folder) # Verify if the new folder is created mlrun_p = DIRNAME_local / 'mlruns' / '1' diff --git a/rdagent/scenarios/qlib/task_generator/feedback.py b/rdagent/scenarios/qlib/task_generator/feedback.py index fee41d26..431fc29b 100644 --- a/rdagent/scenarios/qlib/task_generator/feedback.py +++ b/rdagent/scenarios/qlib/task_generator/feedback.py @@ -39,20 +39,15 @@ class QlibFactorHypothesisExperiment2Feedback(HypothesisExperiment2Feedback): hypothesis_text = hypothesis.hypothesis current_result = exp.result tasks_factors = [task.get_factor_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()) - - - # Prepare task details - task_details = "\n".join([f"Task: {factor_name}, Factor: {factor_description}" for factor_name, factor_description in tasks_factors]) # Generate the user prompt - usr_prompt = Environment(undefined=StrictUndefined).from_string(feedback_prompts["data_feedback_generation"]["user"]).format( + usr_prompt = Environment(undefined=StrictUndefined).from_string(feedback_prompts["data_feedback_generation"]["user"]).render( hypothesis_text=hypothesis_text, - task_details=task_details, + task_details=tasks_factors, current_result=current_result, sota_result=sota_result ) From 6fb12fc7532c2cff428e1010d17847b19ea8ffe8 Mon Sep 17 00:00:00 2001 From: WinstonLiyt <1957922024@qq.com> Date: Fri, 12 Jul 2024 03:30:25 +0000 Subject: [PATCH 04/12] Switch from local_env to Docker for running Qlib --- rdagent/scenarios/qlib/task_generator/data.py | 12 +++++++----- rdagent/utils/env.py | 2 +- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/rdagent/scenarios/qlib/task_generator/data.py b/rdagent/scenarios/qlib/task_generator/data.py index ec2b4ed0..2bac903a 100644 --- a/rdagent/scenarios/qlib/task_generator/data.py +++ b/rdagent/scenarios/qlib/task_generator/data.py @@ -106,13 +106,15 @@ class QlibFactorRunner(TaskGenerator[QlibFactorExperiment]): combined_factors = combined_factors.sort_index() new_columns = pd.MultiIndex.from_product([['feature'], combined_factors.columns]) combined_factors.columns = new_columns - + + # logger.info(combined_factors) + # 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 + # Docker run # Call Docker, pass the combined factors to Docker, and generate backtest results qtde = QTDockerEnv() qtde.prepare() @@ -132,8 +134,8 @@ class QlibFactorRunner(TaskGenerator[QlibFactorExperiment]): with open(pkl_path, 'rb') as f: result = pickle.load(f) - """ + """ # TODO: Implement the Docker run in the following way # Local run # Clean up any previous run artifacts by deleting the mlruns directory @@ -166,9 +168,9 @@ class QlibFactorRunner(TaskGenerator[QlibFactorExperiment]): with open(pickle_file, 'rb') as f: result = pickle.load(f) - + """ exp.result = result - + # Check if the result is valid and is a DataFrame if isinstance(result, pd.DataFrame): if not result.empty: diff --git a/rdagent/utils/env.py b/rdagent/utils/env.py index 8d079d6a..f45f9021 100644 --- a/rdagent/utils/env.py +++ b/rdagent/utils/env.py @@ -125,7 +125,7 @@ class DockerConf(BaseModel): QLIB_TORCH_IMAGE = DockerConf( - image="linlanglv/qlib_image_nightly_pytorch:nightly", + image="linlanglv/qlib_image_nightly_pytorch:240711", mount_path="/workspace", default_entry="qrun conf.yaml", extra_volumes={Path("~/.qlib/").expanduser().resolve(): "/root/.qlib/"}, From 9cbb7263891cb7e4cca4555ab6220096698c93d7 Mon Sep 17 00:00:00 2001 From: WinstonLiyt <1957922024@qq.com> Date: Fri, 12 Jul 2024 05:32:00 +0000 Subject: [PATCH 05/12] Upload the configuration file for running Docker. --- .gitignore | 4 +- .../qlib/task_generator/env_factor/conf.yaml | 73 +++++++++++++++ .../env_factor/conf_combined.yaml | 93 +++++++++++++++++++ .../task_generator/env_factor/read_exp_res.py | 52 +++++++++++ 4 files changed, 220 insertions(+), 2 deletions(-) create mode 100644 rdagent/scenarios/qlib/task_generator/env_factor/conf.yaml create mode 100644 rdagent/scenarios/qlib/task_generator/env_factor/conf_combined.yaml create mode 100644 rdagent/scenarios/qlib/task_generator/env_factor/read_exp_res.py diff --git a/.gitignore b/.gitignore index a1dd635e..176e76d2 100644 --- a/.gitignore +++ b/.gitignore @@ -155,6 +155,6 @@ git_ignore_folder/ *.db # Docker -env_factor/ -env_tpl/ +env_factor/mlruns/ +env_tpl mlruns/ \ No newline at end of file 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..9695008f --- /dev/null +++ b/rdagent/scenarios/qlib/task_generator/env_factor/read_exp_res.py @@ -0,0 +1,52 @@ +from pathlib import Path +import qlib +from mlflow.tracking import MlflowClient +from mlflow.entities import ViewType +import pandas as pd +import pickle +import os + +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("here2") + print(output_path) \ No newline at end of file From 601fb0186ed184a7f16dc9560aa5e3f561661fe9 Mon Sep 17 00:00:00 2001 From: Xu Yang Date: Mon, 15 Jul 2024 08:28:34 +0000 Subject: [PATCH 06/12] help yuante on the final version of data code --- rdagent/app/qlib_rd_loop/model.py | 2 +- .../coder/factor_coder/CoSTEER/evaluators.py | 31 ++- .../factor_coder/CoSTEER/evolving_strategy.py | 6 +- .../CoSTEER/knowledge_management.py | 12 +- .../coder/factor_coder/CoSTEER/scheduler.py | 2 +- .../components/coder/factor_coder/factor.py | 2 +- .../coder/factor_coder/prompts.yaml | 8 +- .../coder/model_coder/CoSTEER/evaluators.py | 6 +- .../model_coder/CoSTEER/evolving_strategy.py | 4 +- .../CoSTEER/knowledge_management.py | 4 +- rdagent/components/coder/model_coder/model.py | 2 +- rdagent/core/experiment.py | 11 +- rdagent/scenarios/qlib/conf.py | 19 ++ rdagent/scenarios/qlib/docker/Dockerfile | 21 ++ rdagent/scenarios/qlib/task_generator/data.py | 206 +++++++----------- .../task_generator/env_factor/read_exp_res.py | 25 ++- .../scenarios/qlib/task_generator/feedback.py | 50 +++-- rdagent/utils/env.py | 69 +++--- requirements/package.txt | 1 + 19 files changed, 267 insertions(+), 214 deletions(-) create mode 100644 rdagent/scenarios/qlib/conf.py create mode 100644 rdagent/scenarios/qlib/docker/Dockerfile 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/evaluators.py b/rdagent/components/coder/factor_coder/CoSTEER/evaluators.py index 7cb861dd..9ead0c1d 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,28 @@ 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 + + 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 +336,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 +398,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 +500,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/factor.py b/rdagent/components/coder/factor_coder/factor.py index 4932a9ae..1cf70dcd 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} 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/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/model.py b/rdagent/components/coder/model_coder/model.py index 93c9451d..27d1f13c 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} diff --git a/rdagent/core/experiment.py b/rdagent/core/experiment.py index e0cb6dfc..a71545dc 100644 --- a/rdagent/core/experiment.py +++ b/rdagent/core/experiment.py @@ -7,11 +7,17 @@ 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) @@ -117,6 +123,7 @@ 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: 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..ff418d3a --- /dev/null +++ b/rdagent/scenarios/qlib/docker/Dockerfile @@ -0,0 +1,21 @@ +FROM pytorch/pytorch:latest + +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/task_generator/data.py b/rdagent/scenarios/qlib/task_generator/data.py index 2bac903a..8b4ed8cc 100644 --- a/rdagent/scenarios/qlib/task_generator/data.py +++ b/rdagent/scenarios/qlib/task_generator/data.py @@ -1,20 +1,23 @@ -from pathlib import Path -import shutil -from typing import List -import pandas as pd import pickle -from rdagent.app.qlib_rd_loop.conf import PROP_SETTING -from rdagent.core.task_generator import TaskGenerator -from rdagent.utils.env import QTDockerEnv, LocalConf, LocalEnv -from rdagent.scenarios.qlib.experiment.factor_experiment import QlibFactorExperiment +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 @@ -26,6 +29,7 @@ logger = RDAgentLog() # TODO: supporting multiprocessing and keep previous results + class QlibFactorRunner(TaskGenerator[QlibFactorExperiment]): """ Docker run @@ -35,141 +39,95 @@ class QlibFactorRunner(TaskGenerator[QlibFactorExperiment]): - `data.py` + Adaptor to Factor implementation - results in `mlflow` """ - - def FetchAlpha158ResultFromDocker(self): - """ - Run Docker to get alpha158 result. - This method prepares the Qlib Docker environment, executes the necessary commands to - run the backtest, and fetches the results stored in a pickle file. + 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) - Returns: - Any: The alpha158 result. If successful, returns a pandas DataFrame. Otherwise, returns None. - """ - # Initialize and prepare the Qlib Docker environment - qtde = QTDockerEnv() - qtde.prepare() - - # Clean up any previous run artifacts by deleting the mlruns directory - result = qtde.run(local_path=str(DIRNAME / "env_factor"), entry="rm -r mlruns", env={"PYTHONPATH": "./"}) - - # Run the Qlib backtest using the configuration file conf.yaml - result = qtde.run(local_path=str(DIRNAME / "env_factor"), entry="qrun conf.yaml", env={"PYTHONPATH": "./"}) - - # Execute a Python script to extract the experiment results - result = 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) - - # Check if the loaded result is a pandas DataFrame and not empty - if isinstance(result, pd.DataFrame): - if not result.empty: - logger.info("Successfully retrieved alpha158 result.") - return result - else: - logger.error("Result DataFrame is empty.") - return None + 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: - logger.error("Data format error.") - return None + 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: """ Generate the experiment by processing and combining factor data, then passing the combined data to Docker for backtest results. """ - SOTA_factor = None - if exp.based_experiments.__len__() != 1: - SOTA_factor = self.process_factor_data(exp.based_experiments) - - if exp.based_experiments[-1].result is None: - exp.based_experiments[-1].result = self.FetchAlpha158ResultFromDocker() - - # 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: - 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 - - # logger.info(combined_factors) - - # 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) + 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 - result = qtde.run(local_path=str(DIRNAME / "env_factor"), entry="rm -r mlruns", env={"PYTHONPATH": "./"}) + execute_log = qtde.run(local_path=str(DIRNAME / "env_factor"), entry="rm -r mlruns") # Run the Qlib backtest - result = qtde.run(local_path=str(DIRNAME / "env_factor"), entry="qrun conf_combined.yaml", env={"PYTHONPATH": "./"}) + 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", + ) - result = qtde.run(local_path=str(DIRNAME / "env_factor"), entry="python read_exp_res.py") + execute_log = qtde.run(local_path=str(DIRNAME / "env_factor"), entry="python read_exp_res.py") - pkl_path = DIRNAME / 'env_factor/qlib_res.pkl' + 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: + with open(pkl_path, "rb") as f: result = pickle.load(f) - - """ - # TODO: Implement the Docker run in the following way - # Local run - # Clean up any previous run artifacts by deleting the mlruns directory - mlruns_path = DIRNAME_local / 'mlruns' / '1' - if mlruns_path.exists() and mlruns_path.is_dir(): - shutil.rmtree(mlruns_path) - # Prepare local Qlib environment - local_conf = LocalConf( - py_bin=PROP_SETTING.py_bin, - default_entry="qrun conf_combined.yaml", - ) - qle = LocalEnv(conf=local_conf) - qle.prepare() - conf_path = str(DIRNAME / "env_factor" / "conf_combined.yaml") - qle.run(entry="qrun " + conf_path, local_path=PROP_SETTING.local_qlib_folder) - - # Verify if the new folder is created - mlrun_p = DIRNAME_local / 'mlruns' / '1' - assert mlrun_p.exists(), f"Expected output file {mlrun_p} not found" - - # Locate the newly generated folder in mlruns/1/ - new_folders = [folder for folder in mlrun_p.iterdir() if folder.is_dir()] - if not new_folders: - raise FileNotFoundError("No new folders found in 'mlruns/1/'.") - - new_folder = new_folders[0] # Assuming there's only one new folder - pickle_file = new_folder / 'artifacts' / 'portfolio_analysis' / 'port_analysis_1day.pkl' - assert pickle_file.exists(), f"Expected pickle file {pickle_file} not found" - - with open(pickle_file, '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): @@ -204,18 +162,14 @@ class QlibFactorRunner(TaskGenerator[QlibFactorExperiment]): message, df = implementation.execute() # Check if factor generation was successful - if 'Execution succeeded without error.\nExpected output file found.' in message: - factor_dfs.append(df) + 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: - combined_factors = pd.concat(factor_dfs, axis=1) - - # Remove rows with NaN values - combined_factors = combined_factors.dropna() - - # print(combined_factors) - return combined_factors + 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/read_exp_res.py b/rdagent/scenarios/qlib/task_generator/env_factor/read_exp_res.py index 9695008f..6e05ec5e 100644 --- a/rdagent/scenarios/qlib/task_generator/env_factor/read_exp_res.py +++ b/rdagent/scenarios/qlib/task_generator/env_factor/read_exp_res.py @@ -1,17 +1,19 @@ -from pathlib import Path -import qlib -from mlflow.tracking import MlflowClient -from mlflow.entities import ViewType -import pandas as pd -import pickle 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 +# TODO: list all the recorder and metrics # Assuming you have already listed the experiments experiments = R.list_experiments() @@ -26,8 +28,8 @@ for experiment in experiments: 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']: + 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 @@ -43,10 +45,9 @@ else: # 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("here2") - print(output_path) \ No newline at end of file + 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 431fc29b..3ad83cd4 100644 --- a/rdagent/scenarios/qlib/task_generator/feedback.py +++ b/rdagent/scenarios/qlib/task_generator/feedback.py @@ -1,27 +1,33 @@ # TODO: # Implement to feedback. +import json +import pickle from pathlib import Path +import pandas as pd from jinja2 import Environment, StrictUndefined -from rdagent.core.prompts import Prompts -from rdagent.core.proposal import HypothesisExperiment2Feedback -from rdagent.core.proposal import Trace + from rdagent.core.experiment import Experiment -from rdagent.core.proposal import Hypothesis, HypothesisFeedback +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 from rdagent.utils.env import QTDockerEnv -from rdagent.core.log import RDAgentLog -import json -import pandas as pd -import pickle feedback_prompts = Prompts(file_path=Path(__file__).parent.parent / "prompts.yaml") DIRNAME = Path(__file__).absolute().resolve().parent logger = RDAgentLog() + class QlibModelHypothesisExperiment2Feedback(HypothesisExperiment2Feedback): ... + class QlibFactorHypothesisExperiment2Feedback(HypothesisExperiment2Feedback): def generateFeedback(self, exp: Experiment, hypothesis: Hypothesis, trace: Trace) -> HypothesisFeedback: """ @@ -38,18 +44,26 @@ class QlibFactorHypothesisExperiment2Feedback(HypothesisExperiment2Feedback): logger.info("Generating feedback...") hypothesis_text = hypothesis.hypothesis current_result = exp.result - tasks_factors = [task.get_factor_information() for task in exp.sub_tasks] + 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()) + 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 + 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 @@ -61,21 +75,21 @@ class QlibFactorHypothesisExperiment2Feedback(HypothesisExperiment2Feedback): # 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 + decision=decision, ) logger.info( diff --git a/rdagent/utils/env.py b/rdagent/utils/env.py index f45f9021..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:240711", - 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 From b1f62a475cea50aa9ae9f6e87ca50dc03401eebf Mon Sep 17 00:00:00 2001 From: Xu Yang Date: Mon, 15 Jul 2024 08:30:21 +0000 Subject: [PATCH 07/12] remove the new test file --- test/utils/test_env2.py | 38 -------------------------------------- 1 file changed, 38 deletions(-) delete mode 100644 test/utils/test_env2.py diff --git a/test/utils/test_env2.py b/test/utils/test_env2.py deleted file mode 100644 index 543e89a0..00000000 --- a/test/utils/test_env2.py +++ /dev/null @@ -1,38 +0,0 @@ -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 - - -DIRNAME = Path(__file__).absolute().resolve().parent - - -class EnvUtils(unittest.TestCase): - def setUp(self): - pass - - def test_docker(self): - """ - We will mount `env_tpl` into the docker image. - 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 conf2.yaml") - - 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_res.py") - print("here") - # print(result) - - -if __name__ == "__main__": - unittest.main() From 7bc2d83e75609aea3b8b2beca3c36be0bdf84976 Mon Sep 17 00:00:00 2001 From: Xu Yang Date: Mon, 15 Jul 2024 08:31:27 +0000 Subject: [PATCH 08/12] remove useless print command --- test/utils/test_env.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/test/utils/test_env.py b/test/utils/test_env.py index f108d320..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 @@ -30,9 +31,9 @@ class EnvUtils(unittest.TestCase): ) qle = LocalEnv(conf=local_conf) qle.prepare() - conf_path = str(DIRNAME / "env_tpl" / "conf.yaml") + conf_path = str(DIRNAME / "env_tpl" / "conf.yaml") qle.run(entry="qrun " + conf_path) - 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") def test_docker(self): @@ -41,17 +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_res.py") - print("here") print(result) From c4f5bd2f18e77e7e789575595bba5445629905d9 Mon Sep 17 00:00:00 2001 From: Xu Yang Date: Mon, 15 Jul 2024 09:40:40 +0000 Subject: [PATCH 09/12] fix a small bug --- .../components/coder/factor_coder/CoSTEER/evaluators.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/rdagent/components/coder/factor_coder/CoSTEER/evaluators.py b/rdagent/components/coder/factor_coder/CoSTEER/evaluators.py index 9ead0c1d..6e19ddfa 100644 --- a/rdagent/components/coder/factor_coder/CoSTEER/evaluators.py +++ b/rdagent/components/coder/factor_coder/CoSTEER/evaluators.py @@ -194,6 +194,14 @@ class FactorDatetimeDailyEvaluator(FactorEvaluator): 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 ( From b4d89b30948b84176d769a775b6d020a96751fb2 Mon Sep 17 00:00:00 2001 From: Xu Yang Date: Mon, 15 Jul 2024 10:08:20 +0000 Subject: [PATCH 10/12] add a comment for GPU support --- rdagent/scenarios/qlib/docker/Dockerfile | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/rdagent/scenarios/qlib/docker/Dockerfile b/rdagent/scenarios/qlib/docker/Dockerfile index ff418d3a..26419c6e 100644 --- a/rdagent/scenarios/qlib/docker/Dockerfile +++ b/rdagent/scenarios/qlib/docker/Dockerfile @@ -1,4 +1,6 @@ -FROM pytorch/pytorch:latest +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 \ From e6be18932d52dc19cdcc947e23ab80149a0b5025 Mon Sep 17 00:00:00 2001 From: Xu Yang Date: Mon, 15 Jul 2024 10:23:32 +0000 Subject: [PATCH 11/12] enable debug data and all data in config --- .../components/coder/factor_coder/config.py | 9 ++++--- .../components/coder/factor_coder/factor.py | 26 ++++++++++--------- .../components/coder/factor_coder/utils.py | 2 +- rdagent/components/coder/model_coder/conf.py | 4 +-- rdagent/components/coder/model_coder/model.py | 8 +++--- rdagent/scenarios/qlib/task_generator/data.py | 2 +- 6 files changed, 27 insertions(+), 24 deletions(-) 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 1cf70dcd..aa4fecac 100644 --- a/rdagent/components/coder/factor_coder/factor.py +++ b/rdagent/components/coder/factor_coder/factor.py @@ -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/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/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 27d1f13c..7ddf634b 100644 --- a/rdagent/components/coder/model_coder/model.py +++ b/rdagent/components/coder/model_coder/model.py @@ -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/scenarios/qlib/task_generator/data.py b/rdagent/scenarios/qlib/task_generator/data.py index 8b4ed8cc..bc44f203 100644 --- a/rdagent/scenarios/qlib/task_generator/data.py +++ b/rdagent/scenarios/qlib/task_generator/data.py @@ -159,7 +159,7 @@ class QlibFactorRunner(TaskGenerator[QlibFactorExperiment]): 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() + message, df = implementation.execute(data_type="All") # Check if factor generation was successful if df is not None: From 0b250b93e9cf22f14ec1da508dd313f7677a6942 Mon Sep 17 00:00:00 2001 From: WinstonLiyt <1957922024@qq.com> Date: Mon, 15 Jul 2024 10:31:57 +0000 Subject: [PATCH 12/12] Fix a bug. --- rdagent/core/proposal.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rdagent/core/proposal.py b/rdagent/core/proposal.py index 03d74cce..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