Merge pull request #65 from microsoft/ytli_update

Completed and integrated the entire factor process
This commit is contained in:
WinstonLiyt
2024-07-15 18:58:30 +08:00
committed by GitHub
34 changed files with 851 additions and 121 deletions
+5
View File
@@ -153,3 +153,8 @@ git_ignore_folder/
# DB files
*.db
# Docker
env_factor/mlruns/
env_tpl
mlruns/
+5 -2
View File
@@ -1,4 +1,5 @@
from pydantic_settings import BaseSettings
from pathlib import Path
class PropSetting(BaseSettings):
@@ -22,6 +23,8 @@ class PropSetting(BaseSettings):
qlib_model_summarizer: str = "rdagent.scenarios.qlib.task_generator.feedback.QlibModelHypothesisExperiment2Feedback"
evolving_n: int = 10
py_bin: str = "/usr/bin/python"
local_qlib_folder: Path = Path("/home/rdagent/qlib")
PROP_SETTING = PropSetting()
+3 -2
View File
@@ -25,9 +25,10 @@ hypothesis_gen: HypothesisGen = import_class(PROP_SETTING.qlib_factor_hypothesis
hypothesis2experiment: Hypothesis2Experiment = import_class(PROP_SETTING.qlib_factor_hypothesis2experiment)()
qlib_factor_coder: TaskGenerator = import_class(PROP_SETTING.qlib_factor_coder)(scen)
qlib_factor_runner: TaskGenerator = import_class(PROP_SETTING.qlib_factor_runner)(scen)
qlib_factor_summarizer: HypothesisExperiment2Feedback = import_class(PROP_SETTING.qlib_factor_summarizer)()
qlib_factor_summarizer: HypothesisExperiment2Feedback = import_class(PROP_SETTING.qlib_factor_summarizer)(scen)
trace = Trace(scen=scen)
@@ -38,4 +39,4 @@ for _ in range(PROP_SETTING.evolving_n):
exp = qlib_factor_runner.generate(exp)
feedback = qlib_factor_summarizer.generateFeedback(exp, hypothesis, trace)
trace.hist.append((hypothesis, exp, feedback))
trace.hist.append((hypothesis, exp, feedback))
+1 -1
View File
@@ -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):
@@ -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
@@ -84,7 +84,7 @@ class FactorCodeEvaluator(FactorEvaluator):
gt_implementation: Implementation = None,
**kwargs,
):
factor_information = target_task.get_factor_information()
factor_information = target_task.get_task_information()
code = implementation.code
system_prompt = (
@@ -181,6 +181,36 @@ class FactorOutputFormatEvaluator(FactorEvaluator):
)
class FactorDatetimeDailyEvaluator(FactorEvaluator):
def evaluate(
self,
implementation: Implementation,
gt_implementation: Implementation,
) -> Tuple[str | object]:
_, gen_df = self._get_df(gt_implementation, implementation)
if gen_df is None:
return "The source dataframe is None. Skip the evaluation of the datetime format.", False
if "datetime" not in gen_df.index.names:
return "The source dataframe does not have a datetime index. Please check the implementation.", False
try:
pd.to_datetime(gen_df.index.get_level_values("datetime"))
except Exception:
return (
"The source dataframe has a datetime index but it is not in the correct format (maybe a regular string or other objects). Please check the implementation.",
False,
)
time_diff = gen_df.index.get_level_values("datetime").to_series().diff().dropna().unique()
if pd.Timedelta(minutes=1) in time_diff:
return (
"The generated dataframe is not daily. The implementation is definitely wrong. Please check the implementation.",
False,
)
return "The generated dataframe is daily.", True
class FactorRowCountEvaluator(FactorEvaluator):
def evaluate(
self,
@@ -314,6 +344,9 @@ class FactorValueEvaluator(FactorEvaluator):
feedback_str, _ = FactorOutputFormatEvaluator(self.scen).evaluate(implementation, gt_implementation)
conclusions.append(feedback_str)
feedback_str, _ = FactorDatetimeDailyEvaluator(self.scen).evaluate(implementation, gt_implementation)
conclusions.append(feedback_str)
# Check if both dataframe have the same rows count
if gt_implementation is not None:
feedback_str, _ = FactorRowCountEvaluator(self.scen).evaluate(implementation, gt_implementation)
@@ -373,7 +406,7 @@ class FactorFinalDecisionEvaluator(Evaluator):
evaluate_prompts["evaluator_final_decision_v1_user"],
)
.render(
factor_information=target_task.get_factor_information(),
factor_information=target_task.get_task_information(),
execution_feedback=execution_feedback_to_render,
code_feedback=code_feedback,
factor_value_feedback=(
@@ -475,7 +508,7 @@ class FactorEvaluatorForCoder(FactorEvaluator):
if implementation is None:
return None
target_task_information = target_task.get_factor_information()
target_task_information = target_task.get_task_information()
if (
queried_knowledge is not None
and target_task_information in queried_knowledge.success_task_to_knowledge_dict
@@ -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 (
@@ -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
@@ -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]))
@@ -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
+15 -13
View File
@@ -37,7 +37,7 @@ class FactorTask(Task):
self.variables = variables
self.factor_resources = resource
def get_factor_information(self):
def get_task_information(self):
return f"""factor_name: {self.factor_name}
factor_description: {self.factor_description}
factor_formulation: {self.factor_formulation}
@@ -95,11 +95,11 @@ class FileBasedFactorImplementation(FBImplementation):
def prepare(self, *args, **kwargs):
self.workspace_path = Path(
FACTOR_IMPLEMENT_SETTINGS.file_based_execution_workspace,
FACTOR_IMPLEMENT_SETTINGS.factor_execution_workspace,
) / str(uuid.uuid4())
self.workspace_path.mkdir(exist_ok=True, parents=True)
def execute(self, store_result: bool = False) -> Tuple[str, pd.DataFrame]:
def execute(self, store_result: bool = False, data_type: str = "Debug") -> Tuple[str, pd.DataFrame]:
"""
execute the implementation and get the factor value by the following steps:
1. make the directory in workspace path
@@ -120,14 +120,10 @@ class FileBasedFactorImplementation(FBImplementation):
raise ValueError(self.FB_CODE_NOT_SET)
with FileLock(self.workspace_path / "execution.lock"):
if FACTOR_IMPLEMENT_SETTINGS.enable_execution_cache:
# NOTE: cache the result for the same code
target_file_name = md5_hash(self.code_dict["factor.py"])
cache_file_path = (
Path(FACTOR_IMPLEMENT_SETTINGS.implementation_execution_cache_location) / f"{target_file_name}.pkl"
)
Path(FACTOR_IMPLEMENT_SETTINGS.implementation_execution_cache_location).mkdir(
exist_ok=True, parents=True
)
# NOTE: cache the result for the same code and same data type
target_file_name = md5_hash(data_type + self.code_dict["factor.py"])
cache_file_path = Path(FACTOR_IMPLEMENT_SETTINGS.factor_cache_location) / f"{target_file_name}.pkl"
Path(FACTOR_IMPLEMENT_SETTINGS.factor_cache_location).mkdir(exist_ok=True, parents=True)
if cache_file_path.exists() and not self.raise_exception:
cached_res = pickle.load(open(cache_file_path, "rb"))
if store_result and cached_res[1] is not None:
@@ -137,8 +133,14 @@ class FileBasedFactorImplementation(FBImplementation):
if self.executed_factor_value_dataframe is not None:
return self.FB_FROM_CACHE, self.executed_factor_value_dataframe
source_data_path = Path(
FACTOR_IMPLEMENT_SETTINGS.file_based_execution_data_folder,
source_data_path = (
Path(
FACTOR_IMPLEMENT_SETTINGS.factor_data_folder_debug,
)
if data_type == "Debug"
else Path(
FACTOR_IMPLEMENT_SETTINGS.factor_data_folder,
)
)
source_data_path.mkdir(exist_ok=True, parents=True)
@@ -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}}):=====
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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]
+2 -2
View File
@@ -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(),
)
@@ -24,7 +24,7 @@ class ModelTask(Task):
self.variables: str = variables
self.model_type: str = model_type # Tabular for tabular model, TimesSeries for time series model
def get_information(self):
def get_task_information(self):
return f"""name: {self.name}
description: {self.description}
formulation: {self.formulation}
@@ -68,7 +68,7 @@ class ModelImplementation(FBImplementation):
Prepare for the workspace;
"""
unique_id = uuid.uuid4()
self.workspace_path = Path(MODEL_IMPL_SETTINGS.file_based_execution_workspace) / f"M{unique_id}"
self.workspace_path = Path(MODEL_IMPL_SETTINGS.model_execution_workspace) / f"M{unique_id}"
# start with `M` so that it can be imported via python
self.workspace_path.mkdir(parents=True, exist_ok=True)
@@ -84,10 +84,8 @@ class ModelImplementation(FBImplementation):
if MODEL_IMPL_SETTINGS.enable_execution_cache:
# NOTE: cache the result for the same code
target_file_name = md5_hash(self.code_dict["model.py"])
cache_file_path = (
Path(MODEL_IMPL_SETTINGS.implementation_execution_cache_location) / f"{target_file_name}.pkl"
)
Path(MODEL_IMPL_SETTINGS.implementation_execution_cache_location).mkdir(exist_ok=True, parents=True)
cache_file_path = Path(MODEL_IMPL_SETTINGS.model_cache_location) / f"{target_file_name}.pkl"
Path(MODEL_IMPL_SETTINGS.model_cache_location).mkdir(exist_ok=True, parents=True)
if cache_file_path.exists():
return pickle.load(open(cache_file_path, "rb"))
mod = get_module_by_module_path(str(self.workspace_path / "model.py"))
+16 -2
View File
@@ -7,17 +7,27 @@ This file contains the all the class about organizing the task in RD-Agent.
"""
class Task:
class Task(ABC):
# TODO: 把name放在这里作为主键
# Please refer to rdagent/model_implementation/task.py for the implementation
# I think the task version applies to the base class.
pass
@abstractmethod
def get_task_information(self):
"""
Get the task information string to build the unique key
"""
pass
ASpecificTask = TypeVar("ASpecificTask", bound=Task)
class Implementation(ABC, Generic[ASpecificTask]):
# TODO: workspace;
# - code or data(optional)
# - Execute logic
# - `env is not included`. It is a underlying infra
def __init__(self, target_task: ASpecificTask) -> None:
self.target_task = target_task
@@ -85,6 +95,7 @@ class FBImplementation(Implementation):
typical usage of `*args, **kwargs`:
Different methods shares the same data. The data are passed by the arguments.
"""
# TODO: model and factor prepare;
def inject_code(self, **files: str):
"""
@@ -113,11 +124,14 @@ class Experiment(ABC, Generic[ASpecificTask, ASpecificImp]):
The experiment is a sequence of tasks and the implementations of the tasks after generated by the TaskGenerator.
"""
result_ws: Optional[FBImplementation]
def __init__(self, sub_tasks: Sequence[ASpecificTask]) -> None:
self.sub_tasks = sub_tasks
self.sub_implementations: Sequence[ASpecificImp] = [None for _ in self.sub_tasks]
self.based_experiments: Sequence[Experiment] = []
self.result: object = None # The result of the experiment, can be different types in different scenarios.
self.result_ws = None
TaskOrExperiment = TypeVar("TaskOrExperiment", Task, Experiment)
+7 -4
View File
@@ -3,10 +3,10 @@
"""
from abc import ABC, abstractmethod
from typing import Dict, Generic, List, Tuple, TypeVar
from typing import Any, Dict, Generic, List, Tuple, TypeVar
from rdagent.core.evaluation import Feedback
from rdagent.core.experiment import Experiment
from rdagent.core.experiment import ASpecificTask, Experiment
from rdagent.core.scenario import Scenario
# class data_ana: XXX
@@ -101,9 +101,12 @@ class Hypothesis2Experiment(ABC, Generic[ASpecificExp]):
class HypothesisExperiment2Feedback:
""" "Generated feedbacks on the hypothesis from **Executed** Implementations of different tasks & their comparisons with previous performances"""
def generateFeedback(self, ti: Experiment, hypothesis: Hypothesis, trace: Trace) -> HypothesisFeedback:
def __init__(self, scen: Scenario):
self.scen = scen
def generateFeedback(self, exp: Experiment, hypothesis: Hypothesis, trace: Trace) -> HypothesisFeedback:
"""
The `ti` should be executed and the results should be included, as well as the comparison between previous results (done by LLM).
The `exp` should be executed and the results should be included, as well as the comparison between previous results (done by LLM).
For example: `mlflow` of Qlib will be included.
"""
raise NotImplementedError("generateFeedback method is not implemented.")
+19
View File
@@ -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()
+23
View File
@@ -0,0 +1,23 @@
FROM pytorch/pytorch:latest
# For GPU support, please choose the proper tag from https://hub.docker.com/r/pytorch/pytorch/tags
RUN apt-get clean && apt-get update && apt-get install -y \
curl \
vim \
git \
build-essential \
&& rm -rf /var/lib/apt/lists/*
RUN git clone https://github.com/microsoft/qlib.git
WORKDIR /workspace/qlib
RUN git reset c9ed050ef034fe6519c14b59f3d207abcb693282 --hard
RUN python -m pip install --upgrade numpy
RUN python -m pip install --upgrade cython
RUN python -m pip install -e .
RUN pip install catboost
RUN pip install xgboost
@@ -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
+33 -1
View File
@@ -47,4 +47,36 @@ model_experiment_output_format: |-
"model_type": "type of model 1, Tabular or TimesSeries" # Should be one of "Tabular" or "TimeSeries"
}
# Don't add ellipsis (...) or any filler text that might cause JSON parsing errors here!
}
}
data_feedback_generation:
system: |-
You are a professional result analysis assistant on data driven R&D.
The task is described in the following scenario:
{{ scenario }}
You will receive a hypothesis, multiple tasks with their factors, and some results.
Your feedback should specify whether the current result supports or refutes the hypothesis, compare it with previous results, and suggest improvements or new directions.
Please provide detailed and constructive feedback for the future exploration.
Please respond in JSON format, and example JSON Structure for Result Analysis:
{
"Observations": "Your overall observations here",
"Feedback for Hypothesis": "Observations related to the hypothesis",
"New Hypothesis": "Put your new hypothesis here.",
"Reasoning": "Provide reasoning for the hypothesis here.",
"Replace Best Result": "yes or no"
}
user: |-
Target hypothesis:
{{ hypothesis_text }}
Tasks and Factors:
{{ task_details }}
Current Result:
{{ current_result }}
SOTA Result:
{{ sota_result }}
Analyze the current result in the context of its ability to:
1. Support or refute the hypothesis.
2. Show improvement or deterioration compared to the last experiment.
3. Demonstrate positive or negative effects when compared to Alpha158.
Provide detailed feedback and recommend whether to replace the best result if the new factor proves superior.
+160 -3
View File
@@ -1,5 +1,33 @@
import pickle
import shutil
from pathlib import Path
from typing import List, Tuple
import pandas as pd
from rdagent.core.log import RDAgentLog
from rdagent.core.task_generator import TaskGenerator
from rdagent.oai.llm_utils import md5_hash
from rdagent.scenarios.qlib.conf import Qlib_RD_AGENT_SETTINGS
from rdagent.scenarios.qlib.experiment.factor_experiment import QlibFactorExperiment
from rdagent.utils.env import QTDockerEnv
DIRNAME = Path(__file__).absolute().resolve().parent
DIRNAME_local = Path.cwd()
logger = RDAgentLog()
# class QlibFactorExpWorkspace:
# def prepare():
# # create a folder;
# # copy template
# # place data inside the folder `combined_factors`
# #
# def execute():
# de = DockerEnv()
# de.run(local_path=self.ws_path, entry="qrun conf.yaml")
# TODO: supporting multiprocessing and keep previous results
class QlibFactorRunner(TaskGenerator[QlibFactorExperiment]):
@@ -10,9 +38,138 @@ class QlibFactorRunner(TaskGenerator[QlibFactorExperiment]):
- price-volume data dumper
- `data.py` + Adaptor to Factor implementation
- results in `mlflow`
- TODO: implement a qlib handler
"""
def get_cache_key(self, exp: QlibFactorExperiment) -> str:
all_tasks = []
for based_exp in exp.based_experiments:
all_tasks.extend(based_exp.sub_tasks)
all_tasks.extend(exp.sub_tasks)
task_info_list = [task.get_task_information() for task in all_tasks]
task_info_str = "\n".join(task_info_list)
return md5_hash(task_info_str)
def get_cache_result(self, exp: QlibFactorExperiment) -> Tuple[bool, object]:
task_info_key = self.get_cache_key(exp)
Path(Qlib_RD_AGENT_SETTINGS.runner_cache_path).mkdir(parents=True, exist_ok=True)
cache_path = Path(Qlib_RD_AGENT_SETTINGS.runner_cache_path) / f"{task_info_key}.pkl"
if cache_path.exists():
return True, pickle.load(open(cache_path, "rb"))
else:
return False, None
def dump_cache_result(self, exp: QlibFactorExperiment, result: object):
task_info_key = self.get_cache_key(exp)
cache_path = Path(Qlib_RD_AGENT_SETTINGS.runner_cache_path) / f"{task_info_key}.pkl"
pickle.dump(result, open(cache_path, "wb"))
def generate(self, exp: QlibFactorExperiment) -> QlibFactorExperiment:
return exp # TODO IMPLEMENT THIS
"""
Generate the experiment by processing and combining factor data,
then passing the combined data to Docker for backtest results.
"""
if exp.based_experiments and exp.based_experiments[-1].result is None:
exp.based_experiments[-1] = self.generate(exp.based_experiments[-1])
if Qlib_RD_AGENT_SETTINGS.runner_cache_result:
cache_hit, result = self.get_cache_result(exp)
if cache_hit:
exp.result = result
return exp
if exp.based_experiments:
SOTA_factor = None
if exp.based_experiments.__len__() != 1:
SOTA_factor = self.process_factor_data(exp.based_experiments)
# Process the new factors data
new_factors = self.process_factor_data(exp)
# Combine the SOTA factor and new factors if SOTA factor exists
if SOTA_factor is not None and not SOTA_factor.empty:
combined_factors = pd.concat([SOTA_factor, new_factors], axis=1).dropna()
else:
combined_factors = new_factors
# Sort and nest the combined factors under 'feature'
combined_factors = combined_factors.sort_index()
new_columns = pd.MultiIndex.from_product([["feature"], combined_factors.columns])
combined_factors.columns = new_columns
# Save the combined factors to a pickle file
combined_factors_path = DIRNAME / "env_factor/combined_factors_df.pkl"
with open(combined_factors_path, "wb") as f:
pickle.dump(combined_factors, f)
# Docker run
# Call Docker, pass the combined factors to Docker, and generate backtest results
qtde = QTDockerEnv()
qtde.prepare()
# Run the Docker command
execute_log = qtde.run(local_path=str(DIRNAME / "env_factor"), entry="rm -r mlruns")
# Run the Qlib backtest
execute_log = qtde.run(
local_path=str(DIRNAME / "env_factor"),
entry=f"qrun conf.yaml" if len(exp.based_experiments) == 0 else "qrun conf_combined.yaml",
)
execute_log = qtde.run(local_path=str(DIRNAME / "env_factor"), entry="python read_exp_res.py")
pkl_path = DIRNAME / "env_factor/qlib_res.pkl"
if not pkl_path.exists():
logger.error(f"File {pkl_path} does not exist.")
return None
with open(pkl_path, "rb") as f:
result = pickle.load(f)
exp.result = result
if Qlib_RD_AGENT_SETTINGS.runner_cache_result:
self.dump_cache_result(exp, result)
# Check if the result is valid and is a DataFrame
if isinstance(result, pd.DataFrame):
if not result.empty:
logger.info("Successfully retrieved experiment result.")
return exp
else:
logger.error("Result DataFrame is empty.")
return None
else:
logger.error("Data format error.")
return None
def process_factor_data(self, exp_or_list: List[QlibFactorExperiment] | QlibFactorExperiment) -> pd.DataFrame:
"""
Process and combine factor data from experiment implementations.
Args:
exp (ASpecificExp): The experiment containing factor data.
Returns:
pd.DataFrame: Combined factor data without NaN values.
"""
if isinstance(exp_or_list, QlibFactorExperiment):
exp_or_list = [exp_or_list]
factor_dfs = []
# Collect all exp's dataframes
for exp in exp_or_list:
# Iterate over sub-implementations and execute them to get each factor data
for implementation in exp.sub_implementations:
message, df = implementation.execute(data_type="All")
# Check if factor generation was successful
if df is not None:
time_diff = df.index.get_level_values("datetime").to_series().diff().dropna().unique()
if pd.Timedelta(minutes=1) not in time_diff:
factor_dfs.append(df)
# Combine all successful factor data
if factor_dfs:
return pd.concat(factor_dfs, axis=1)
else:
logger.error("No valid factor data found to merge.")
return pd.DataFrame() # Return an empty DataFrame if no valid data
@@ -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: <PRED>
topk: 50
n_drop: 5
backtest:
start_time: 2017-01-01
end_time: 2020-08-01
account: 100000000
benchmark: *benchmark
exchange_kwargs:
limit_threshold: 0.095
deal_price: close
open_cost: 0.0005
close_cost: 0.0015
min_cost: 5
task:
model:
class: 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: <MODEL>
dataset: <DATASET>
- class: SigAnaRecord
module_path: qlib.workflow.record_temp
kwargs:
ana_long_short: False
ann_scaler: 252
- class: PortAnaRecord
module_path: qlib.workflow.record_temp
kwargs:
config: *port_analysis_config
@@ -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: <PRED>
topk: 50
n_drop: 5
backtest:
start_time: 2017-01-01
end_time: 2020-08-01
account: 100000000
benchmark: *benchmark
exchange_kwargs:
limit_threshold: 0.095
deal_price: close
open_cost: 0.0005
close_cost: 0.0015
min_cost: 5
task:
model:
class: 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: <MODEL>
dataset: <DATASET>
- class: SigAnaRecord
module_path: qlib.workflow.record_temp
kwargs:
ana_long_short: False
ann_scaler: 252
- class: PortAnaRecord
module_path: qlib.workflow.record_temp
kwargs:
config: *port_analysis_config
@@ -0,0 +1,53 @@
import os
import pickle
from pathlib import Path
import pandas as pd
import qlib
from mlflow.entities import ViewType
from mlflow.tracking import MlflowClient
qlib.init()
from qlib.workflow import R
# here is the documents of the https://qlib.readthedocs.io/en/latest/component/recorder.html
# TODO: list all the recorder and metrics
# Assuming you have already listed the experiments
experiments = R.list_experiments()
# Iterate through each experiment to find the latest recorder
experiment_name = None
latest_recorder = None
for experiment in experiments:
# print(f"Experiment: {experiment}")
recorders = R.list_recorders(experiment_name=experiment)
for recorder_id in recorders:
if recorder_id is not None:
experiment_name = experiment
recorder = R.get_recorder(recorder_id=recorder_id, experiment_name=experiment)
end_time = recorder.info["end_time"]
if latest_recorder is None or end_time > latest_recorder.info["end_time"]:
latest_recorder = recorder
# Check if the latest recorder is found
if latest_recorder is None:
print("No recorders found")
else:
print(f"Latest recorder: {latest_recorder}")
# Load the specified file from the latest recorder
file_path = "portfolio_analysis/port_analysis_1day.pkl"
indicator_analysis_df = latest_recorder.load_object(file_path)
# Optionally convert to DataFrame if not already in DataFrame format
if not isinstance(indicator_analysis_df, pd.DataFrame):
indicator_analysis_df = pd.DataFrame(indicator_analysis_df)
output_path = os.path.join(str(Path(__file__).resolve().parent), "qlib_res.pkl")
with open(output_path, "wb") as f:
pickle.dump(indicator_analysis_df, f)
print(f"Output has been saved to {output_path}")
@@ -1,13 +1,101 @@
# TODO:
# Implement to feedback.
import json
from rdagent.oai.llm_utils import APIBackend
from rdagent.core.proposal import HypothesisExperiment2Feedback, Trace, Hypothesis, HypothesisFeedback, Scenario
from pathlib import Path
from jinja2 import Environment, StrictUndefined
from rdagent.core.experiment import Experiment
from rdagent.core.log import RDAgentLog
from rdagent.core.prompts import Prompts
from rdagent.core.proposal import (
Hypothesis,
HypothesisExperiment2Feedback,
HypothesisFeedback,
Trace,
)
from rdagent.oai.llm_utils import APIBackend
feedback_prompts = Prompts(file_path=Path(__file__).parent.parent / "prompts.yaml")
DIRNAME = Path(__file__).absolute().resolve().parent
logger = RDAgentLog()
class QlibFactorHypothesisExperiment2Feedback(HypothesisExperiment2Feedback): ...
class QlibFactorHypothesisExperiment2Feedback(HypothesisExperiment2Feedback):
def generateFeedback(self, exp: Experiment, hypothesis: Hypothesis, trace: Trace) -> HypothesisFeedback:
"""
Generate feedback for the given experiment and hypothesis.
Args:
exp (QlibFactorExperiment): The experiment to generate feedback for.
hypothesis (QlibFactorHypothesis): The hypothesis to generate feedback for.
trace (Trace): The trace of the experiment.
Returns:
Any: The feedback generated for the given experiment and hypothesis.
"""
logger.info("Generating feedback...")
hypothesis_text = hypothesis.hypothesis
current_result = exp.result
tasks_factors = [task.get_task_information() for task in exp.sub_tasks]
sota_result = exp.based_experiments[-1].result
# Generate the system prompt
sys_prompt = (
Environment(undefined=StrictUndefined)
.from_string(feedback_prompts["data_feedback_generation"]["system"])
.render(scenario=self.scen.get_scenario_all_desc())
)
# Generate the user prompt
usr_prompt = (
Environment(undefined=StrictUndefined)
.from_string(feedback_prompts["data_feedback_generation"]["user"])
.render(
hypothesis_text=hypothesis_text,
task_details=tasks_factors,
current_result=current_result,
sota_result=sota_result,
)
)
# Call the APIBackend to generate the response for hypothesis feedback
response = APIBackend().build_messages_and_create_chat_completion(
user_prompt=usr_prompt,
system_prompt=sys_prompt,
json_mode=True,
)
# Parse the JSON response to extract the feedback
response_json = json.loads(response)
# Extract fields from JSON response
observations = response_json.get("Observations", "No observations provided")
hypothesis_evaluation = response_json.get("Feedback for Hypothesis", "No feedback provided")
new_hypothesis = response_json.get("New Hypothesis", "No new hypothesis provided")
reason = response_json.get("Reasoning", "No reasoning provided")
decision = response_json.get("Replace Best Result", "no").lower() == "yes"
# Create HypothesisFeedback object
hypothesis_feedback = HypothesisFeedback(
observations=observations,
hypothesis_evaluation=hypothesis_evaluation,
new_hypothesis=new_hypothesis,
reason=reason,
decision=decision,
)
logger.info(
"Generated Hypothesis Feedback:\n"
f"Observations: {observations}\n"
f"Feedback for Hypothesis: {hypothesis_evaluation}\n"
f"New Hypothesis: {new_hypothesis}\n"
f"Reason: {reason}\n"
f"Replace Best Result: {'Yes' if decision else 'No'}"
)
return hypothesis_feedback
class QlibModelHypothesisExperiment2Feedback(HypothesisExperiment2Feedback):
@@ -40,7 +128,7 @@ class QlibModelHypothesisExperiment2Feedback(HypothesisExperiment2Feedback):
else:
last_info_str = "This is the first round. No previous information available."
usr_prompt_hypothesis = f'''
usr_prompt_hypothesis = f"""
We are in an experiment of finding hypothesis and validating or rejecting them so that in the end we have a powerful model generated.
Here are the context: {context}.
{last_info_str}
@@ -51,8 +139,8 @@ class QlibModelHypothesisExperiment2Feedback(HypothesisExperiment2Feedback):
Result: {exp.result}\n
Compare and observe. Which result has a better return and lower risk? If the performance increases the hypothesis should be considered positive (working).
Hence, with the hypotheses, relevant reasonings, and results in mind (comparison), provide detailed and constructive feedback and suggest a new hypothesis.
'''
Hence, with the hypotheses, relevant reasoning, and results in mind (comparison), provide detailed and constructive feedback and suggest a new hypothesis.
"""
try:
# Call the APIBackend to generate the response for hypothesis feedback
@@ -61,7 +149,7 @@ class QlibModelHypothesisExperiment2Feedback(HypothesisExperiment2Feedback):
system_prompt=sys_prompt_hypothesis,
json_mode=True,
)
# Parse the JSON response to extract the feedback
response_json_hypothesis = json.loads(response_hypothesis)
hypothesis_feedback = HypothesisFeedback(
@@ -69,13 +157,13 @@ class QlibModelHypothesisExperiment2Feedback(HypothesisExperiment2Feedback):
hypothesis_evaluation=response_json_hypothesis.get("Feedback for Hypothesis", "No feedback provided"),
new_hypothesis=response_json_hypothesis.get("New Hypothesis", "No new hypothesis provided"),
reason=response_json_hypothesis.get("Reasoning", "No reasoning provided"),
decision=response_json_hypothesis.get("Decision", "false").lower() == "true"
decision=response_json_hypothesis.get("Decision", "false").lower() == "true",
)
return hypothesis_feedback
except json.JSONDecodeError as e:
# TODO: (Xiao) I think raising a specific type of ERROR to make caller know sth bad has happend would be more reasonable
# TODO: (Xiao) I think raising a specific type of ERROR to make caller know sth bad has happened would be more reasonable
print("Error parsing JSON response from LLM for hypothesis feedback:", e)
except Exception as e:
print("An unexpected error occurred while generating hypothesis feedback:", e)
@@ -85,6 +173,5 @@ class QlibModelHypothesisExperiment2Feedback(HypothesisExperiment2Feedback):
hypothesis_evaluation="No feedback",
new_hypothesis="No new hypothesis",
reason="No reasoning",
decision=False
decision=False,
)
+40 -29
View File
@@ -5,14 +5,21 @@ Tries to create uniform environment for the agent to run;
- All the code and data is expected included in one folder
"""
import os
import sys
import docker
import subprocess
import sys
from abc import abstractmethod
from pydantic import BaseModel
from typing import Generic, TypeVar, Optional, Dict
from pathlib import Path
from typing import Dict, Generic, Optional, TypeVar
import docker
import docker.models
import docker.models.containers
from pydantic import BaseModel
from pydantic_settings import BaseSettings
from rdagent.core.log import RDAgentLog
ASpecificBaseModel = TypeVar("ASpecificBaseModel", bound=BaseModel)
@@ -71,6 +78,7 @@ class LocalEnv(Env[LocalConf]):
"""
Sometimes local environment may be more convinient for testing
"""
def prepare(self):
if not (Path("~/.qlib/qlib_data/cn_data").expanduser().resolve().exists()):
self.run(
@@ -79,10 +87,7 @@ class LocalEnv(Env[LocalConf]):
else:
print("Data already exists. Download skipped.")
def run(self,
entry: str | None = None,
local_path: Optional[str] = None,
env: dict | None = None) -> str:
def run(self, entry: str | None = None, local_path: Optional[str] = None, env: dict | None = None) -> str:
if env is None:
env = {}
@@ -94,15 +99,7 @@ class LocalEnv(Env[LocalConf]):
cwd = None
if local_path:
cwd = Path(local_path).resolve()
print(f"CWD: {cwd}")
result = subprocess.run(
command,
cwd=cwd,
env={**os.environ, **env},
capture_output=True,
text=True
)
result = subprocess.run(command, cwd=cwd, env={**os.environ, **env}, capture_output=True, text=True)
if result.returncode != 0:
raise RuntimeError(f"Error while running the command: {result.stderr}")
@@ -113,8 +110,10 @@ class LocalEnv(Env[LocalConf]):
## Docker Environment -----
class DockerConf(BaseModel):
image: str # the image you want to run
class DockerConf(BaseSettings):
build_from_dockerfile: bool = False
dockerfile_folder_path: Path # the path to the dockerfile
image: str # the image you want to build
mount_path: str # the path in the docker image to mount the folder
default_entry: str # the entry point of the image
@@ -122,14 +121,16 @@ class DockerConf(BaseModel):
# Sometime, we need maintain some extra data for the workspace.
# And the extra data may be shared and the downloading can be time consuming.
# So we just want to download it once.
network: str | None = "bridge" # the network mode for the docker
QLIB_TORCH_IMAGE = DockerConf(
image="linlanglv/qlib_image_nightly_pytorch:nightly",
mount_path="/workspace",
default_entry="qrun conf.yaml",
extra_volumes={Path("~/.qlib/").expanduser().resolve(): "/root/.qlib/"},
)
class QlibDockerConf(DockerConf):
build_from_dockerfile: bool = True
dockerfile_folder_path: Path = Path(__file__).parent.parent / "scenarios" / "qlib" / "docker"
image: str = "local_qlib:latest"
mount_path: str = "/workspace/qlib_workspace/"
default_entry: str = "qrun conf.yaml"
extra_volumes: dict = {Path("~/.qlib/").expanduser().resolve(): "/root/.qlib/"}
class DockerEnv(Env[DockerConf]):
@@ -140,6 +141,12 @@ class DockerEnv(Env[DockerConf]):
Download image if it doesn't exist
"""
client = docker.from_env()
if self.conf.build_from_dockerfile is not None and self.conf.dockerfile_folder_path.exists():
RDAgentLog().info(f"Building the image from dockerfile: {self.conf.dockerfile_folder_path}")
image, logs = client.images.build(
path=str(self.conf.dockerfile_folder_path), tag=self.conf.image, network_mode=self.conf.network
)
RDAgentLog().info(f"Finished building the image from dockerfile: {self.conf.dockerfile_folder_path}")
try:
client.images.get(self.conf.image)
except docker.errors.ImageNotFound:
@@ -164,14 +171,15 @@ class DockerEnv(Env[DockerConf]):
log_output = ""
try:
container = client.containers.run(
container: docker.models.containers.Container = client.containers.run(
image=self.conf.image,
command=entry,
volumes=volumns,
environment=env,
detach=True,
working_dir=self.conf.mount_path,
auto_remove=True,
# auto_remove=True, # remove too fast might cause the logs not to be get
network=self.conf.network,
)
logs = container.logs(stream=True)
for log in logs:
@@ -179,6 +187,8 @@ class DockerEnv(Env[DockerConf]):
print(decoded_log)
log_output += decoded_log + "\n"
container.wait()
container.stop()
container.remove()
return log_output
except docker.errors.ContainerError as e:
raise RuntimeError(f"Error while running the container: {e}")
@@ -191,7 +201,7 @@ class DockerEnv(Env[DockerConf]):
class QTDockerEnv(DockerEnv):
"""Qlib Torch Docker"""
def __init__(self, conf: DockerConf = QLIB_TORCH_IMAGE):
def __init__(self, conf: DockerConf = QlibDockerConf()):
super().__init__(conf)
def prepare(self):
@@ -201,7 +211,8 @@ class QTDockerEnv(DockerEnv):
super().prepare()
qlib_data_path = next(iter(self.conf.extra_volumes.keys()))
if not (Path(qlib_data_path) / "qlib_data" / "cn_data").exists():
RDAgentLog().info("We are downloading!")
cmd = "python -m qlib.run.get_data qlib_data --target_dir ~/.qlib/qlib_data/cn_data --region cn --interval 1d --delete_old False"
self.run(entry=cmd)
else:
print("Data already exists. Download skipped.")
RDAgentLog().info("Data already exists. Download skipped.")
+1
View File
@@ -18,6 +18,7 @@ matplotlib
langchain
tiktoken
scikit-learn
docker
# azure identity related
azure.identity
+116
View File
@@ -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.
+16 -17
View File
@@ -2,10 +2,11 @@ import os
import sys
import unittest
from pathlib import Path
sys.path.append(str(Path(__file__).resolve().parent.parent))
from rdagent.utils.env import QTDockerEnv, LocalEnv, LocalConf
import shutil
from rdagent.utils.env import LocalConf, LocalEnv, QTDockerEnv
DIRNAME = Path(__file__).absolute().resolve().parent
@@ -23,18 +24,17 @@ class EnvUtils(unittest.TestCase):
# NOTE: Since I don't know the exact environment in which it will be used, here's just an example.
# NOTE: Because you need to download the data during the prepare process. So you need to have pyqlib in your environment.
# def test_local(self):
# local_conf = LocalConf(
# py_bin="/home/v-linlanglv/miniconda3/envs/RD-Agent-310/bin",
# default_entry="qrun conf.yaml",
# )
# qle = LocalEnv(conf=local_conf)
# qle.prepare()
# exe_path = str(DIRNAME / "env_tpl")
# conf_path = str(DIRNAME / "env_tpl" / "conf.yaml")
# qle.run(entry="qrun " + conf_path, local_path=exe_path)
# mlrun_p = DIRNAME / "env_tpl" / "mlruns"
# self.assertTrue(mlrun_p.exists(), f"Expected output file {mlrun_p} not found")
def test_local(self):
local_conf = LocalConf(
py_bin="/home/v-linlanglv/miniconda3/envs/RD-Agent-310/bin",
default_entry="qrun conf.yaml",
)
qle = LocalEnv(conf=local_conf)
qle.prepare()
conf_path = str(DIRNAME / "env_tpl" / "conf.yaml")
qle.run(entry="qrun " + conf_path)
mlrun_p = DIRNAME / "env_tpl" / "mlruns"
self.assertTrue(mlrun_p.exists(), f"Expected output file {mlrun_p} not found")
def test_docker(self):
"""
@@ -42,16 +42,15 @@ class EnvUtils(unittest.TestCase):
And run the docker image with `qrun conf.yaml`
"""
qtde = QTDockerEnv()
qtde.prepare()
qtde.prepare() # you can prepare for multiple times. It is expected to handle it correctly
# the stdout are returned as result
result = qtde.run(local_path=str(DIRNAME / "env_tpl"), entry="qrun conf.yaml")
mlrun_p = DIRNAME / "env_tpl" / "mlruns"
mlrun_p = DIRNAME / "env_tpl" / "mlruns"
self.assertTrue(mlrun_p.exists(), f"Expected output file {mlrun_p} not found")
# read experiment
result = qtde.run(local_path=str(DIRNAME / "env_tpl"), entry="python read_exp.py")
result = qtde.run(local_path=str(DIRNAME / "env_tpl"), entry="python read_exp_res.py")
print(result)