Skip to content

Commit b5ea040

Browse files
WinstonLiytyou-n-gtaozhiwangSH-Src
authored
fix: Comprehensive update to factor extraction. (#143)
* Init todo * update all code * update * Extract factors from financial reports loop finished * Fix two small bugs. * Delete rdagent/app/qlib_rd_loop/run_script.sh * Minor mod * Delete rdagent/app/qlib_rd_loop/nohup.out * Fix a small bug in file reading. * some updates * Update the detailed process and prompt of factor loop. * Evaluation & dataset * Optimize the prompt for generating hypotheses and feedback in the factor loop. * Generate new data * dataset generation * Performed further optimizations on the factor loop and report extraction loop, added log handling for both processes, and implemented a screenshot feature for report extraction. * Update rdagent/components/coder/factor_coder/CoSTEER/evaluators.py * Update package.txt for fitz. * add the result * Performed further optimizations on the factor loop and report extraction loop, added log handling for both processes, and implemented a screenshot feature for report extraction. (#100) (#102) - Performed further optimizations on the factor loop and report extraction loop. - Added log handling for both processes. - Implemented a screenshot feature for report extraction. * Analysis * Optimized log output. * Factor update * A draft of the "Quick Start" section for README * Add scenario descriptions. * Updates * Adjust content * Enable logging of backtesting in Qlib and store rich-text descriptions in Trace. Support one-step debugging for factor extraction. * Reformat analysis.py * CI fix * Refactor * remove useless code * fix bugs (#111) * Fix two small bugs. * Fix a merge bug. * Fix two small bugs. * fix some bugs. * Fix some format bugs. * Restore a file. * Fix a format bug. * draft renew of evaluators * fix a small bug. * fix a small bug * Support Factor Report Loop * Update framework for extracting factors from research reports. * Refactor report-based factor extraction and fix minor bugs. * fix a small bug of log. * change some prompts * improve factor_runner * fix a small bug * change some prompts * cancel some comments * cancel some comments and fix some bugs --------- Co-authored-by: Young <[email protected]> Co-authored-by: you-n-g <[email protected]> Co-authored-by: Taozhi Wang <[email protected]> Co-authored-by: Suhan Cui <[email protected]>
1 parent e818326 commit b5ea040

File tree

10 files changed

+250
-193
lines changed

10 files changed

+250
-193
lines changed

rdagent/app/qlib_rd_loop/conf.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,10 @@ class Config:
3535
# 2) sub task specific:
3636
origin_report_path: str = "data/report_origin"
3737
local_report_path: str = "data/report"
38-
report_result_json_file_path: str = "git_ignore_folder/res_dict.json"
38+
report_result_json_file_path: str = "git_ignore_folder/res_dict.csv"
3939
progress_file_path: str = "git_ignore_folder/progress.pkl"
40+
report_extract_result: str = "git_ignore_folder/hypo_exp_cache.pkl"
41+
max_factor_per_report: int = 10000
4042

4143

4244
FACTOR_PROP_SETTING = FactorBasePropSetting()
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
# TODO: we should have more advanced mechanism to handle such requirements for saving sessions.
2+
import csv
23
import json
34
import pickle
45
from pathlib import Path
6+
from typing import Any
57

8+
import fire
69
import pandas as pd
710
from dotenv import load_dotenv
811
from jinja2 import Environment, StrictUndefined
@@ -12,7 +15,10 @@
1215
extract_first_page_screenshot_from_pdf,
1316
load_and_process_pdfs_by_langchain,
1417
)
18+
from rdagent.components.workflow.conf import BasePropSetting
19+
from rdagent.components.workflow.rd_loop import RDLoop
1520
from rdagent.core.developer import Developer
21+
from rdagent.core.exception import FactorEmptyError
1622
from rdagent.core.prompts import Prompts
1723
from rdagent.core.proposal import (
1824
Hypothesis,
@@ -34,40 +40,16 @@
3440
FactorExperimentLoaderFromPDFfiles,
3541
classify_report_from_dict,
3642
)
43+
from rdagent.utils.workflow import LoopBase, LoopMeta
3744

38-
assert load_dotenv()
39-
40-
scen: Scenario = import_class(FACTOR_PROP_SETTING.scen)()
41-
42-
hypothesis_gen: HypothesisGen = import_class(FACTOR_PROP_SETTING.hypothesis_gen)(scen)
43-
44-
hypothesis2experiment: Hypothesis2Experiment = import_class(FACTOR_PROP_SETTING.hypothesis2experiment)()
45-
46-
qlib_factor_coder: Developer = import_class(FACTOR_PROP_SETTING.coder)(scen)
47-
48-
qlib_factor_runner: Developer = import_class(FACTOR_PROP_SETTING.runner)(scen)
49-
50-
qlib_factor_summarizer: HypothesisExperiment2Feedback = import_class(FACTOR_PROP_SETTING.summarizer)(scen)
51-
52-
with open(FACTOR_PROP_SETTING.report_result_json_file_path, "r") as f:
53-
judge_pdf_data = json.load(f)
45+
with open(FACTOR_PROP_SETTING.report_result_json_file_path, "r") as input_file:
46+
csv_reader = csv.reader(input_file)
47+
judge_pdf_data = [row[0] for row in csv_reader]
5448

5549
prompts_path = Path(__file__).parent / "prompts.yaml"
5650
prompts = Prompts(file_path=prompts_path)
5751

5852

59-
def save_progress(trace, current_index):
60-
with open(FACTOR_PROP_SETTING.progress_file_path, "wb") as f:
61-
pickle.dump((trace, current_index), f)
62-
63-
64-
def load_progress():
65-
if Path(FACTOR_PROP_SETTING.progress_file_path).exists():
66-
with open(FACTOR_PROP_SETTING.progress_file_path, "rb") as f:
67-
return pickle.load(f)
68-
return Trace(scen=scen), 0
69-
70-
7153
def generate_hypothesis(factor_result: dict, report_content: str) -> str:
7254
system_prompt = (
7355
Environment(undefined=StrictUndefined).from_string(prompts["hypothesis_generation"]["system"]).render()
@@ -123,52 +105,95 @@ def extract_factors_and_implement(report_file_path: str) -> tuple:
123105
return exp, hypothesis
124106

125107

126-
trace, start_index = load_progress()
127-
128-
try:
129-
judge_pdf_data_items = list(judge_pdf_data.items())
130-
for index in range(start_index, len(judge_pdf_data_items)):
131-
if index > 1000:
132-
break
133-
file_path, attributes = judge_pdf_data_items[index]
134-
if attributes["class"] == 1:
135-
report_file_path = Path(
136-
file_path.replace(FACTOR_PROP_SETTING.origin_report_path, FACTOR_PROP_SETTING.local_report_path)
137-
)
138-
if report_file_path.exists():
139-
logger.info(f"Processing {report_file_path}")
140-
141-
with logger.tag("r"):
142-
exp, hypothesis = extract_factors_and_implement(str(report_file_path))
143-
if exp is None:
144-
continue
145-
exp.based_experiments = [t[1] for t in trace.hist if t[2]]
146-
if len(exp.based_experiments) == 0:
147-
exp.based_experiments.append(QlibFactorExperiment(sub_tasks=[]))
148-
logger.log_object(hypothesis, tag="hypothesis generation")
149-
logger.log_object(exp.sub_tasks, tag="experiment generation")
150-
151-
with logger.tag("d"):
152-
exp = qlib_factor_coder.develop(exp)
153-
logger.log_object(exp.sub_workspace_list)
154-
155-
with logger.tag("ef"):
156-
exp = qlib_factor_runner.develop(exp)
157-
if exp is None:
158-
logger.error(f"Factor extraction failed for {report_file_path}. Skipping to the next report.")
159-
continue
160-
logger.log_object(exp, tag="factor runner result")
161-
feedback = qlib_factor_summarizer.generate_feedback(exp, hypothesis, trace)
162-
logger.log_object(feedback, tag="feedback")
163-
164-
trace.hist.append((hypothesis, exp, feedback))
165-
logger.info(f"Processed {report_file_path}: Result: {exp}")
166-
167-
# Save progress after processing each report
168-
save_progress(trace, index + 1)
169-
else:
170-
logger.error(f"File not found: {report_file_path}")
171-
except Exception as e:
172-
logger.error(f"An error occurred: {e}")
173-
save_progress(trace, index)
174-
raise
108+
class FactorReportLoop(LoopBase, metaclass=LoopMeta):
109+
skip_loop_error = (FactorEmptyError,)
110+
111+
def __init__(self, PROP_SETTING: BasePropSetting):
112+
scen: Scenario = import_class(PROP_SETTING.scen)()
113+
114+
self.coder: Developer = import_class(PROP_SETTING.coder)(scen)
115+
self.runner: Developer = import_class(PROP_SETTING.runner)(scen)
116+
117+
self.summarizer: HypothesisExperiment2Feedback = import_class(PROP_SETTING.summarizer)(scen)
118+
self.trace = Trace(scen=scen)
119+
120+
self.judge_pdf_data_items = judge_pdf_data
121+
self.index = 0
122+
self.hypo_exp_cache = (
123+
pickle.load(open(FACTOR_PROP_SETTING.report_extract_result, "rb"))
124+
if Path(FACTOR_PROP_SETTING.report_extract_result).exists()
125+
else {}
126+
)
127+
super().__init__()
128+
129+
def propose_hypo_exp(self, prev_out: dict[str, Any]):
130+
with logger.tag("r"):
131+
while True:
132+
if self.index > 100:
133+
break
134+
report_file_path = self.judge_pdf_data_items[self.index]
135+
self.index += 1
136+
if report_file_path in self.hypo_exp_cache:
137+
hypothesis, exp = self.hypo_exp_cache[report_file_path]
138+
exp.based_experiments = [QlibFactorExperiment(sub_tasks=[])] + [
139+
t[1] for t in self.trace.hist if t[2]
140+
]
141+
else:
142+
continue
143+
# else:
144+
# exp, hypothesis = extract_factors_and_implement(str(report_file_path))
145+
# if exp is None:
146+
# continue
147+
# exp.based_experiments = [QlibFactorExperiment(sub_tasks=[])] + [t[1] for t in self.trace.hist if t[2]]
148+
# self.hypo_exp_cache[report_file_path] = (hypothesis, exp)
149+
# pickle.dump(self.hypo_exp_cache, open(FACTOR_PROP_SETTING.report_extract_result, "wb"))
150+
with logger.tag("extract_factors_and_implement"):
151+
with logger.tag("load_pdf_screenshot"):
152+
pdf_screenshot = extract_first_page_screenshot_from_pdf(report_file_path)
153+
logger.log_object(pdf_screenshot)
154+
exp.sub_workspace_list = exp.sub_workspace_list[: FACTOR_PROP_SETTING.max_factor_per_report]
155+
exp.sub_tasks = exp.sub_tasks[: FACTOR_PROP_SETTING.max_factor_per_report]
156+
logger.log_object(hypothesis, tag="hypothesis generation")
157+
logger.log_object(exp.sub_tasks, tag="experiment generation")
158+
return hypothesis, exp
159+
160+
def coding(self, prev_out: dict[str, Any]):
161+
with logger.tag("d"): # develop
162+
exp = self.coder.develop(prev_out["propose_hypo_exp"][1])
163+
logger.log_object(exp.sub_workspace_list, tag="coder result")
164+
return exp
165+
166+
def running(self, prev_out: dict[str, Any]):
167+
with logger.tag("ef"): # evaluate and feedback
168+
exp = self.runner.develop(prev_out["coding"])
169+
if exp is None:
170+
logger.error(f"Factor extraction failed.")
171+
raise FactorEmptyError("Factor extraction failed.")
172+
logger.log_object(exp, tag="runner result")
173+
return exp
174+
175+
def feedback(self, prev_out: dict[str, Any]):
176+
feedback = self.summarizer.generate_feedback(prev_out["running"], prev_out["propose_hypo_exp"][0], self.trace)
177+
with logger.tag("ef"): # evaluate and feedback
178+
logger.log_object(feedback, tag="feedback")
179+
self.trace.hist.append((prev_out["propose_hypo_exp"][0], prev_out["running"], feedback))
180+
181+
182+
def main(path=None, step_n=None):
183+
"""
184+
You can continue running session by
185+
186+
.. code-block:: python
187+
188+
dotenv run -- python rdagent/app/qlib_rd_loop/factor_from_report_sh.py $LOG_PATH/__session__/1/0_propose --step_n 1 # `step_n` is a optional paramter
189+
190+
"""
191+
if path is None:
192+
model_loop = FactorReportLoop(FACTOR_PROP_SETTING)
193+
else:
194+
model_loop = FactorReportLoop.load(path)
195+
model_loop.run(step_n=step_n)
196+
197+
198+
if __name__ == "__main__":
199+
fire.Fire(main)

rdagent/components/coder/factor_coder/CoSTEER/evaluators.py

+37-13
Original file line numberDiff line numberDiff line change
@@ -165,19 +165,43 @@ def evaluate(
165165
)
166166
.render(scenario=self.scen.get_scenario_all_desc() if self.scen is not None else "No scenario description.")
167167
)
168-
resp = APIBackend().build_messages_and_create_chat_completion(
169-
user_prompt=gen_df_info_str, system_prompt=system_prompt, json_mode=True
170-
)
171-
resp_dict = json.loads(resp)
172-
if isinstance(resp_dict["output_format_decision"], str) and resp_dict["output_format_decision"].lower() in (
173-
"true",
174-
"false",
175-
):
176-
resp_dict["output_format_decision"] = bool(resp_dict["output_format_decision"])
177-
return (
178-
resp_dict["output_format_feedback"],
179-
resp_dict["output_format_decision"],
180-
)
168+
169+
# TODO: with retry_context(retry_n=3, except_list=[KeyError]):
170+
max_attempts = 3
171+
attempts = 0
172+
final_evaluation_dict = None
173+
174+
while attempts < max_attempts:
175+
try:
176+
resp = APIBackend().build_messages_and_create_chat_completion(
177+
user_prompt=gen_df_info_str, system_prompt=system_prompt, json_mode=True
178+
)
179+
resp_dict = json.loads(resp)
180+
181+
if isinstance(resp_dict["output_format_decision"], str) and resp_dict[
182+
"output_format_decision"
183+
].lower() in (
184+
"true",
185+
"false",
186+
):
187+
resp_dict["output_format_decision"] = bool(resp_dict["output_format_decision"])
188+
189+
return (
190+
resp_dict["output_format_feedback"],
191+
resp_dict["output_format_decision"],
192+
)
193+
194+
except json.JSONDecodeError as e:
195+
raise ValueError("Failed to decode JSON response from API.") from e
196+
197+
except KeyError as e:
198+
attempts += 1
199+
if attempts >= max_attempts:
200+
raise KeyError(
201+
"Response from API is missing 'output_format_decision' or 'output_format_feedback' key after multiple attempts."
202+
) from e
203+
204+
return "Failed to evaluate output format after multiple attempts.", False
181205

182206

183207
class FactorDatetimeDailyEvaluator(FactorEvaluator):

rdagent/components/coder/factor_coder/CoSTEER/evolving_strategy.py

+20-22
Original file line numberDiff line numberDiff line change
@@ -66,29 +66,27 @@ def evolve(
6666

6767
# 2. 选择selection方法
6868
# if the number of factors to be implemented is larger than the limit, we need to select some of them
69-
if FACTOR_IMPLEMENT_SETTINGS.select_ratio < 1:
70-
# if the number of loops is equal to the select_loop, we need to select some of them
71-
implementation_factors_per_round = round(
72-
FACTOR_IMPLEMENT_SETTINGS.select_ratio * len(to_be_finished_task_index) + 0.5
73-
) # ceilling
74-
implementation_factors_per_round = min(
75-
implementation_factors_per_round, len(to_be_finished_task_index)
76-
) # but not exceed the total number of tasks
77-
78-
if FACTOR_IMPLEMENT_SETTINGS.select_method == "random":
79-
to_be_finished_task_index = RandomSelect(
80-
to_be_finished_task_index,
81-
implementation_factors_per_round,
82-
)
8369

84-
if FACTOR_IMPLEMENT_SETTINGS.select_method == "scheduler":
85-
to_be_finished_task_index = LLMSelect(
86-
to_be_finished_task_index,
87-
implementation_factors_per_round,
88-
evo,
89-
queried_knowledge.former_traces,
90-
self.scen,
91-
)
70+
if FACTOR_IMPLEMENT_SETTINGS.select_threshold < len(to_be_finished_task_index):
71+
# Select a fixed number of factors if the total exceeds the threshold
72+
implementation_factors_per_round = FACTOR_IMPLEMENT_SETTINGS.select_threshold
73+
else:
74+
implementation_factors_per_round = len(to_be_finished_task_index)
75+
76+
if FACTOR_IMPLEMENT_SETTINGS.select_method == "random":
77+
to_be_finished_task_index = RandomSelect(
78+
to_be_finished_task_index,
79+
implementation_factors_per_round,
80+
)
81+
82+
if FACTOR_IMPLEMENT_SETTINGS.select_method == "scheduler":
83+
to_be_finished_task_index = LLMSelect(
84+
to_be_finished_task_index,
85+
implementation_factors_per_round,
86+
evo,
87+
queried_knowledge.former_traces,
88+
self.scen,
89+
)
9290

9391
result = multiprocessing_wrapper(
9492
[

rdagent/components/coder/factor_coder/config.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ class Config:
3939
file_based_execution_timeout: int = 120 # seconds for each factor implementation execution
4040

4141
select_method: SELECT_METHOD = "random"
42-
select_ratio: float = 0.5
42+
select_threshold: int = 10
4343

4444
max_loop: int = 10
4545

rdagent/components/proposal/prompts.yaml

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ hypothesis_gen:
88
Please generate the output following the format and specifications below:
99
{{ hypothesis_output_format }}
1010
Here are the specifications: {{ hypothesis_specification }}
11+
1112
user_prompt: |-
1213
The user has made several hypothesis on this scenario and did several evaluation on them.
1314
The former hypothesis and the corresponding feedbacks are as follows (focus on the last one & the new hypothesis that it provides and reasoning to see if you agree):

rdagent/scenarios/qlib/developer/factor_runner.py

+1
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ def develop(self, exp: QlibFactorExperiment) -> QlibFactorExperiment:
104104

105105
# Sort and nest the combined factors under 'feature'
106106
combined_factors = combined_factors.sort_index()
107+
combined_factors = combined_factors.loc[:, ~combined_factors.columns.duplicated(keep="last")]
107108
new_columns = pd.MultiIndex.from_product([["feature"], combined_factors.columns])
108109
combined_factors.columns = new_columns
109110

0 commit comments

Comments
 (0)