Files
NexQuant/rdagent/components/coder/factor_coder/factor.py
T

229 lines
9.8 KiB
Python
Raw Normal View History

2024-06-14 12:59:44 +08:00
from __future__ import annotations
import pickle
import subprocess
import uuid
2024-06-14 12:59:44 +08:00
from pathlib import Path
from typing import Tuple, Union
2024-06-14 12:59:44 +08:00
import pandas as pd
from filelock import FileLock
2024-06-14 12:59:44 +08:00
from rdagent.app.kaggle.conf import KAGGLE_IMPLEMENT_SETTING
2024-07-05 17:42:00 +08:00
from rdagent.components.coder.factor_coder.config import FACTOR_IMPLEMENT_SETTINGS
from rdagent.core.exception import CodeFormatError, CustomRuntimeError, NoOutputError
2024-07-17 15:00:13 +08:00
from rdagent.core.experiment import Experiment, FBWorkspace, Task
2024-07-16 20:35:42 +08:00
from rdagent.log import rdagent_logger as logger
from rdagent.oai.llm_utils import md5_hash
2024-05-21 22:48:41 +08:00
class FactorTask(Task):
# TODO: generalized the attributes into the Task
# - factor_* -> *
2024-05-21 22:48:41 +08:00
def __init__(
self,
factor_name,
factor_description,
factor_formulation,
*args,
2024-05-21 22:48:41 +08:00
variables: dict = {},
2024-06-14 12:59:44 +08:00
resource: str = None,
factor_implementation: bool = False,
**kwargs,
2024-05-21 22:48:41 +08:00
) -> None:
2024-09-23 19:53:05 +08:00
self.factor_name = (
factor_name # TODO: remove it in the later version. Keep it only for pickle version compatibility
)
2024-05-21 22:48:41 +08:00
self.factor_description = factor_description
self.factor_formulation = factor_formulation
self.variables = variables
2024-06-14 12:59:44 +08:00
self.factor_resources = resource
self.factor_implementation = factor_implementation
2024-09-23 19:53:05 +08:00
super().__init__(name=factor_name, *args, **kwargs)
2024-05-21 22:48:41 +08:00
def get_task_information(self):
2024-05-21 22:48:41 +08:00
return f"""factor_name: {self.factor_name}
factor_description: {self.factor_description}
factor_formulation: {self.factor_formulation}
variables: {str(self.variables)}"""
2024-05-21 22:48:41 +08:00
def get_task_information_and_implementation_result(self):
return {
"factor_name": self.factor_name,
"factor_description": self.factor_description,
"factor_formulation": self.factor_formulation,
"variables": str(self.variables),
"factor_implementation": str(self.factor_implementation),
}
2024-05-21 22:48:41 +08:00
@staticmethod
def from_dict(dict):
return FactorTask(**dict)
2024-05-21 22:48:41 +08:00
def __repr__(self) -> str:
return f"<{self.__class__.__name__}[{self.factor_name}]>"
2024-07-17 15:00:13 +08:00
class FactorFBWorkspace(FBWorkspace):
2024-05-21 22:48:41 +08:00
"""
This class is used to implement a factor by writing the code to a file.
Input data and output factor value are also written to files.
"""
# TODO: (Xiao) think raising errors may get better information for processing
FB_FROM_CACHE = "The factor value has been executed and stored in the instance variable."
FB_EXEC_SUCCESS = "Execution succeeded without error."
FB_CODE_NOT_SET = "code is not set."
FB_EXECUTION_SUCCEEDED = "Execution succeeded without error."
FB_OUTPUT_FILE_NOT_FOUND = "\nExpected output file not found."
FB_OUTPUT_FILE_FOUND = "\nExpected output file found."
def __init__(
self,
*args,
executed_factor_value_dataframe: pd.DataFrame = None,
raise_exception: bool = False,
**kwargs,
2024-05-21 22:48:41 +08:00
) -> None:
super().__init__(*args, **kwargs)
2024-05-21 22:48:41 +08:00
self.executed_factor_value_dataframe = executed_factor_value_dataframe
self.raise_exception = raise_exception
2024-07-15 10:23:32 +00:00
def execute(self, store_result: bool = False, data_type: str = "Debug") -> Tuple[str, pd.DataFrame]:
2024-05-21 22:48:41 +08:00
"""
execute the implementation and get the factor value by the following steps:
1. make the directory in workspace path
2. write the code to the file in the workspace path
3. link all the source data to the workspace path folder
if call_factor_py is True:
4. execute the code
else:
4. generate a script from template to import the factor.py dump get the factor value to result.h5
2024-05-21 22:48:41 +08:00
5. read the factor value from the output file in the workspace path folder
returns the execution feedback as a string and the factor value as a pandas dataframe
parameters:
store_result: if True, store the factor value in the instance variable, this feature is to be used in the gt implementation to avoid multiple execution on the same gt implementation
"""
2024-07-17 15:00:13 +08:00
super().execute()
if self.code_dict is None or "factor.py" not in self.code_dict:
2024-05-21 22:48:41 +08:00
if self.raise_exception:
2024-07-18 22:36:04 +08:00
raise CodeFormatError(self.FB_CODE_NOT_SET)
2024-05-21 22:48:41 +08:00
else:
return self.FB_CODE_NOT_SET, None
2024-05-21 22:48:41 +08:00
with FileLock(self.workspace_path / "execution.lock"):
if FACTOR_IMPLEMENT_SETTINGS.enable_execution_cache:
2024-07-15 10:23:32 +00:00
# NOTE: cache the result for the same code and same data type
target_file_name = md5_hash(data_type + self.code_dict["factor.py"])
2024-07-17 15:00:13 +08:00
cache_file_path = Path(FACTOR_IMPLEMENT_SETTINGS.cache_location) / f"{target_file_name}.pkl"
Path(FACTOR_IMPLEMENT_SETTINGS.cache_location).mkdir(exist_ok=True, parents=True)
2024-05-21 22:48:41 +08:00
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:
self.executed_factor_value_dataframe = cached_res[1]
return cached_res
if self.executed_factor_value_dataframe is not None:
return self.FB_FROM_CACHE, self.executed_factor_value_dataframe
if self.target_task.version == 1:
source_data_path = (
Path(
FACTOR_IMPLEMENT_SETTINGS.data_folder_debug,
)
if data_type == "Debug"
else Path(
FACTOR_IMPLEMENT_SETTINGS.data_folder,
)
2024-07-15 10:23:32 +00:00
)
elif self.target_task.version == 2:
# TODO you can change the name of the data folder for a better understanding
source_data_path = Path(KAGGLE_IMPLEMENT_SETTING.local_data_path) / KAGGLE_IMPLEMENT_SETTING.competition
2024-06-27 09:39:17 +01:00
source_data_path.mkdir(exist_ok=True, parents=True)
code_path = self.workspace_path / f"factor.py"
2024-05-21 22:48:41 +08:00
self.link_all_files_in_folder_to_workspace(source_data_path, self.workspace_path)
2024-05-21 22:48:41 +08:00
execution_feedback = self.FB_EXECUTION_SUCCEEDED
2024-07-18 15:01:07 +08:00
execution_success = False
if self.target_task.version == 1:
execution_code_path = code_path
elif self.target_task.version == 2:
execution_code_path = self.workspace_path / f"{uuid.uuid4()}.py"
execution_code_path.write_text((Path(__file__).parent / "factor_execution_template.txt").read_text())
2024-05-21 22:48:41 +08:00
try:
subprocess.check_output(
f"{FACTOR_IMPLEMENT_SETTINGS.python_bin} {execution_code_path}",
2024-05-21 22:48:41 +08:00
shell=True,
cwd=self.workspace_path,
stderr=subprocess.STDOUT,
timeout=FACTOR_IMPLEMENT_SETTINGS.file_based_execution_timeout,
2024-05-21 22:48:41 +08:00
)
2024-07-18 15:01:07 +08:00
execution_success = True
2024-05-21 22:48:41 +08:00
except subprocess.CalledProcessError as e:
import site
execution_feedback = (
e.output.decode()
.replace(str(execution_code_path.parent.absolute()), r"/path/to")
2024-05-21 22:48:41 +08:00
.replace(str(site.getsitepackages()[0]), r"/path/to/site-packages")
)
if len(execution_feedback) > 2000:
execution_feedback = (
execution_feedback[:1000] + "....hidden long error message...." + execution_feedback[-1000:]
)
if self.raise_exception:
2024-07-18 22:36:04 +08:00
raise CustomRuntimeError(execution_feedback)
2024-05-21 22:48:41 +08:00
except subprocess.TimeoutExpired:
execution_feedback += f"Execution timeout error and the timeout is set to {FACTOR_IMPLEMENT_SETTINGS.file_based_execution_timeout} seconds."
2024-05-21 22:48:41 +08:00
if self.raise_exception:
2024-07-18 22:36:04 +08:00
raise CustomRuntimeError(execution_feedback)
2024-05-21 22:48:41 +08:00
workspace_output_file_path = self.workspace_path / "result.h5"
2024-07-18 15:01:07 +08:00
if workspace_output_file_path.exists() and execution_success:
2024-05-21 22:48:41 +08:00
try:
executed_factor_value_dataframe = pd.read_hdf(workspace_output_file_path)
execution_feedback += self.FB_OUTPUT_FILE_FOUND
except Exception as e:
execution_feedback += f"Error found when reading hdf file: {e}"[:1000]
executed_factor_value_dataframe = None
2024-07-18 15:01:07 +08:00
else:
execution_feedback += self.FB_OUTPUT_FILE_NOT_FOUND
executed_factor_value_dataframe = None
if self.raise_exception:
2024-07-22 12:58:09 +08:00
raise NoOutputError(execution_feedback)
2024-05-21 22:48:41 +08:00
if store_result and executed_factor_value_dataframe is not None:
self.executed_factor_value_dataframe = executed_factor_value_dataframe
if FACTOR_IMPLEMENT_SETTINGS.enable_execution_cache:
2024-05-21 22:48:41 +08:00
pickle.dump(
(execution_feedback, executed_factor_value_dataframe),
open(cache_file_path, "wb"),
)
return execution_feedback, executed_factor_value_dataframe
def __str__(self) -> str:
# NOTE:
# If the code cache works, the workspace will be None.
return f"File Factor[{self.target_task.factor_name}]: {self.workspace_path}"
def __repr__(self) -> str:
return self.__str__()
@staticmethod
def from_folder(task: FactorTask, path: Union[str, Path], **kwargs):
2024-05-21 22:48:41 +08:00
path = Path(path)
code_dict = {}
for file_path in path.iterdir():
if file_path.suffix == ".py":
code_dict[file_path.name] = file_path.read_text()
2024-07-17 15:00:13 +08:00
return FactorFBWorkspace(target_task=task, code_dict=code_dict, **kwargs)
2024-07-17 15:00:13 +08:00
FactorExperiment = Experiment
FeatureExperiment = Experiment