mirror of
https://github.com/NicolasBohn/NexQuant.git
synced 2026-08-08 12:37:44 +00:00
feat: add a web UI server (#1345)
* update rdagent cmd * fix log error message * use multiProcessing.Process instead of subprocess.Popen * add traces to gitignore * add user interactor in RDLoop (finance scenarios) * add interactor (feedback, hypothesis) for quant scens * fix the test_end in qlib conf * add features init config, general instruction to qlib scenarios * set base features for based exp * fix bug when combine factors * move traces folder to git_ignore_folder * fix bug in features init * fix quant interact bug * fix logger warning error * bug fixes * modify rdagent logger, now it can set file output * adjust cli functions and fix logger bug * fix server port transport problem * update server_ui in cli * add web code * fix CI problem * black fix * update web ui README * update README * update readme
This commit is contained in:
@@ -4,6 +4,9 @@ It is from `rdagent/app/qlib_rd_loop/model.py` and try to replace `rdagent/app/q
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
from multiprocessing import Queue
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from rdagent.components.workflow.conf import BasePropSetting
|
||||
@@ -11,6 +14,7 @@ from rdagent.core.conf import RD_AGENT_SETTINGS
|
||||
from rdagent.core.developer import Developer
|
||||
from rdagent.core.proposal import (
|
||||
Experiment2Feedback,
|
||||
ExperimentPlan,
|
||||
Hypothesis,
|
||||
Hypothesis2Experiment,
|
||||
HypothesisFeedback,
|
||||
@@ -20,6 +24,7 @@ from rdagent.core.proposal import (
|
||||
from rdagent.core.scenario import Scenario
|
||||
from rdagent.core.utils import import_class
|
||||
from rdagent.log import rdagent_logger as logger
|
||||
from rdagent.utils.qlib import ALPHA20, validate_qlib_features
|
||||
from rdagent.utils.workflow import LoopBase, LoopMeta
|
||||
|
||||
|
||||
@@ -36,6 +41,11 @@ class RDLoop(LoopBase, metaclass=LoopMeta):
|
||||
else None
|
||||
)
|
||||
|
||||
self.plan: ExperimentPlan = {
|
||||
"features": ALPHA20,
|
||||
"feature_codes": {},
|
||||
} # for user interaction
|
||||
|
||||
self.hypothesis2experiment: Hypothesis2Experiment = (
|
||||
import_class(PROP_SETTING.hypothesis2experiment)()
|
||||
if hasattr(PROP_SETTING, "hypothesis2experiment") and PROP_SETTING.hypothesis2experiment
|
||||
@@ -58,8 +68,125 @@ class RDLoop(LoopBase, metaclass=LoopMeta):
|
||||
super().__init__()
|
||||
|
||||
# excluded steps
|
||||
def _set_interactor(self, user_request_q: Queue, user_response_q: Queue):
|
||||
self.user_request_q = user_request_q
|
||||
self.user_response_q = user_response_q
|
||||
|
||||
def _init_base_features(self, base_features_path: str | None):
|
||||
if base_features_path is not None:
|
||||
try:
|
||||
base_dir = Path(base_features_path)
|
||||
base_factors_file = base_dir / "base_factors.json"
|
||||
|
||||
feature_codes: dict[str, str] = {}
|
||||
for py_file in sorted(base_dir.glob("*.py")):
|
||||
feature_codes[py_file.name] = py_file.read_text()
|
||||
self.plan["feature_codes"] = feature_codes
|
||||
|
||||
if not base_factors_file.exists():
|
||||
logger.info(f"No base_factors.json found under {base_dir}. Keeping default base features.")
|
||||
logger.info(f"{len(feature_codes)} feature code files loaded from {base_dir}.")
|
||||
else:
|
||||
with base_factors_file.open("r") as f:
|
||||
features = json.load(f)
|
||||
|
||||
if not isinstance(features, dict):
|
||||
raise ValueError(
|
||||
"`base_factors.json` must contain a JSON object of feature_name -> expression."
|
||||
)
|
||||
|
||||
if validate_qlib_features(list(features.values())):
|
||||
self.plan["features"] = features
|
||||
logger.info(
|
||||
f"Loaded base features from {base_factors_file}. {len(features)} features loaded and {len(feature_codes)} feature code files loaded."
|
||||
)
|
||||
else:
|
||||
logger.warning(
|
||||
f"Base feature validation failed for features loaded from {base_factors_file}. Using default features."
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to load base features from {base_features_path}: {e}. Using default features.")
|
||||
else:
|
||||
logger.info("No base features path provided. Using default features.")
|
||||
|
||||
def _interact_init_params(self) -> None:
|
||||
if not (hasattr(self, "user_request_q") and hasattr(self, "user_response_q")):
|
||||
return
|
||||
|
||||
logger.info("Waiting for user interaction on initial parameters...")
|
||||
try:
|
||||
self.user_request_q.put(
|
||||
{
|
||||
"user_instruction": None,
|
||||
}
|
||||
)
|
||||
res_dict = self.user_response_q.get()
|
||||
logger.info("Received user instruction response.")
|
||||
self.plan.update(res_dict)
|
||||
|
||||
if "feature_codes" not in self.plan:
|
||||
self.plan[
|
||||
"user_instruction"
|
||||
] += f"\n\n{str(list(self.plan['feature_codes'].keys()))} has been configured as the base factor; do not generate duplicate factors."
|
||||
fea_valid_msg = ""
|
||||
while True:
|
||||
logger.info("Requesting base feature configuration from user.")
|
||||
self.user_request_q.put(
|
||||
{
|
||||
"features": self.plan["features"],
|
||||
"feature_validation_msg": fea_valid_msg,
|
||||
}
|
||||
)
|
||||
self.plan["features"] = self.user_response_q.get()
|
||||
logger.info("Received base feature configuration response.")
|
||||
if validate_qlib_features(list(self.plan["features"].values())):
|
||||
logger.info(f"Base feature validation passed. {len(self.plan['features'])} features selected.")
|
||||
break
|
||||
else:
|
||||
logger.info("Base feature validation failed. Asking user to revise.")
|
||||
fea_valid_msg = "Some features are invalid, please revise."
|
||||
|
||||
except (EOFError, OSError):
|
||||
logger.info("User interaction failed, using default initial parameters.")
|
||||
return
|
||||
logger.info("Received user interaction on initial parameters.")
|
||||
|
||||
def _interact_hypo(self, hypo: Hypothesis) -> Hypothesis:
|
||||
if not (hasattr(self, "user_request_q") and hasattr(self, "user_response_q")):
|
||||
return hypo
|
||||
|
||||
logger.info("Waiting for user interaction on hypothesis...")
|
||||
try:
|
||||
self.user_request_q.put(hypo.__dict__)
|
||||
res_dict = self.user_response_q.get()
|
||||
modified_hypo = type(hypo)(**res_dict)
|
||||
except (EOFError, OSError, TypeError):
|
||||
logger.info("User interaction failed, using original hypothesis.")
|
||||
return hypo
|
||||
logger.info("Received user interaction on hypothesis.")
|
||||
return modified_hypo
|
||||
|
||||
def _interact_feedback(self, feedback: HypothesisFeedback) -> HypothesisFeedback:
|
||||
if not (hasattr(self, "user_request_q") and hasattr(self, "user_response_q")):
|
||||
return feedback
|
||||
|
||||
logger.info("Waiting for user interaction on feedback...")
|
||||
try:
|
||||
self.user_request_q.put(feedback.__dict__)
|
||||
res_dict = self.user_response_q.get()
|
||||
modified_feedback = HypothesisFeedback(**res_dict)
|
||||
except (EOFError, OSError, TypeError):
|
||||
logger.info("User interaction failed, using original feedback.")
|
||||
return feedback
|
||||
logger.info("Received user interaction on feedback.")
|
||||
return modified_feedback
|
||||
|
||||
def _propose(self):
|
||||
hypothesis = self.hypothesis_gen.gen(self.trace)
|
||||
hypothesis = self.hypothesis_gen.gen(self.trace, self.plan)
|
||||
|
||||
# user can change the hypothesis here
|
||||
hypothesis = self._interact_hypo(hypothesis)
|
||||
|
||||
logger.log_object(hypothesis, tag="hypothesis generation")
|
||||
return hypothesis
|
||||
|
||||
@@ -74,6 +201,11 @@ class RDLoop(LoopBase, metaclass=LoopMeta):
|
||||
if self.get_unfinished_loop_cnt(self.loop_idx) < RD_AGENT_SETTINGS.get_max_parallel():
|
||||
hypo = self._propose()
|
||||
exp = self._exp_gen(hypo)
|
||||
exp.base_features = self.plan["features"]
|
||||
exp.base_feature_codes = self.plan["feature_codes"]
|
||||
if exp.based_experiments:
|
||||
exp.based_experiments[-1].base_features = self.plan["features"]
|
||||
exp.based_experiments[-1].base_feature_codes = self.plan["feature_codes"]
|
||||
return {"propose": hypo, "exp_gen": exp}
|
||||
await asyncio.sleep(1)
|
||||
|
||||
@@ -99,6 +231,7 @@ class RDLoop(LoopBase, metaclass=LoopMeta):
|
||||
)
|
||||
else:
|
||||
feedback = self.summarizer.generate_feedback(prev_out["running"], self.trace)
|
||||
feedback = self._interact_feedback(feedback)
|
||||
logger.log_object(feedback, tag="feedback")
|
||||
return feedback
|
||||
|
||||
|
||||
Reference in New Issue
Block a user