re-commit

This commit is contained in:
WinstonLiyt
2024-07-11 08:49:37 +00:00
parent 29184aff9b
commit bf1b140f68
11 changed files with 527 additions and 23 deletions
+5
View File
@@ -153,3 +153,8 @@ git_ignore_folder/
# DB files
*.db
# Docker
env_factor/
env_tpl/
mlruns/
+4 -2
View File
@@ -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()
+18 -1
View File
@@ -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))
"""
+7
View File
@@ -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)
+5 -2
View File
@@ -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.")
+33 -1
View File
@@ -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!
}
}
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.
+200 -1
View File
@@ -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
@@ -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
+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.
+13 -13
View File
@@ -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)
+38
View File
@@ -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()