1
0
mirror of https://github.com/microsoft/qlib.git synced 2026-07-14 08:16:54 +08:00

Add backtest and backforward task (#1568)

* * add TrainTask & BacktestTask;
* add BackForwardTask;
* adjust prompt_template.yaml which default config failed to backtest;
* run workflow in loop
* add update method to prompt_template.py

* remove debug code

* Adjust Learn Process
* add LearnManager class & use LearnManager to update system prompt;
* use qrun to replace recorder for training and backtesting;

* Adjust analyser
* analyser independent of recorder;
* rename analyser's workspace attribution;
* analyser load variable by recorder.

---------

Co-authored-by: Cadenza-Li <362237642@qq.com>
This commit is contained in:
Fivele-Li
2023-06-30 10:04:43 +08:00
committed by GitHub
parent 1326ac614d
commit 7e84f3aae2
8 changed files with 179 additions and 92 deletions

View File

@@ -3,10 +3,6 @@ import matplotlib.pyplot as plt
from pathlib import Path from pathlib import Path
import numpy as np import numpy as np
from qlib.utils import class_casting
from ..data.dataset import DatasetH
from ..data.dataset.handler import DataHandlerLP
from ..log import get_module_logger from ..log import get_module_logger
from ..contrib.eva.alpha import calc_ic, calc_long_short_return, calc_long_short_prec from ..contrib.eva.alpha import calc_ic, calc_long_short_return, calc_long_short_prec
@@ -14,8 +10,25 @@ logger = get_module_logger("analysis", logging.INFO)
class AnalyzerTemp: class AnalyzerTemp:
def __init__(self, workspace=None, **kwargs): def __init__(self, recorder, output_dir=None, **kwargs):
self.workspace = Path(workspace) if workspace else "./" self.recorder = recorder
self.output_dir = Path(output_dir) if output_dir else "./"
def load(self, name: str):
"""
It behaves the same as self.recorder.load_object.
But it is an easier interface because users don't have to care about `get_path` and `artifact_path`
Parameters
----------
name : str
the name for the file to be load.
Return
------
The stored records.
"""
return self.recorder.load_object(name)
def analyse(self, **kwargs): def analyse(self, **kwargs):
""" """
@@ -42,7 +55,10 @@ class HFAnalyzer(AnalyzerTemp):
def __init__(self, **kwargs): def __init__(self, **kwargs):
super().__init__(**kwargs) super().__init__(**kwargs)
def analyse(self, pred=None, label=None): def analyse(self):
pred = self.load("pred.pkl")
label = self.load("label.pkl")
long_pre, short_pre = calc_long_short_prec(pred.iloc[:, 0], label.iloc[:, 0], is_alpha=True) long_pre, short_pre = calc_long_short_prec(pred.iloc[:, 0], label.iloc[:, 0], is_alpha=True)
ic, ric = calc_ic(pred.iloc[:, 0], label.iloc[:, 0]) ic, ric = calc_ic(pred.iloc[:, 0], label.iloc[:, 0])
metrics = { metrics = {
@@ -65,13 +81,13 @@ class HFAnalyzer(AnalyzerTemp):
table = [[k, v] for (k, v) in metrics.items()] table = [[k, v] for (k, v) in metrics.items()]
plt.table(cellText=table, loc="center") plt.table(cellText=table, loc="center")
plt.axis("off") plt.axis("off")
plt.savefig(self.workspace.joinpath("HFAnalyzerTable.jpeg")) plt.savefig(self.output_dir.joinpath("HFAnalyzerTable.jpeg"))
plt.clf() plt.clf()
plt.scatter(np.arange(0, len(pred)), pred.iloc[:, 0]) plt.scatter(np.arange(0, len(pred)), pred.iloc[:, 0])
plt.scatter(np.arange(0, len(label)), label.iloc[:, 0]) plt.scatter(np.arange(0, len(label)), label.iloc[:, 0])
plt.title("HFAnalyzer") plt.title("HFAnalyzer")
plt.savefig(self.workspace.joinpath("HFAnalyzer.jpeg")) plt.savefig(self.output_dir.joinpath("HFAnalyzer.jpeg"))
return "HFAnalyzer.jpeg" return "HFAnalyzer.jpeg"
@@ -86,24 +102,10 @@ class SignalAnalyzer(AnalyzerTemp):
super().__init__(**kwargs) super().__init__(**kwargs)
def analyse(self, dataset=None, **kwargs): def analyse(self, dataset=None, **kwargs):
label = self.load("label.pkl")
with class_casting(dataset, DatasetH): plt.hist(label)
params = dict(segments="test", col_set="label", data_key=DataHandlerLP.DK_R)
try:
# Assume the backend handler is DataHandlerLP
raw_label = dataset.prepare(**params)
except TypeError:
# The argument number is not right
del params["data_key"]
# The backend handler should be DataHandler
raw_label = dataset.prepare(**params)
except AttributeError as e:
# The data handler is initialized with `drop_raw=True`...
# So raw_label is not available
logger.warning(f"Exception: {e}")
raw_label = None
plt.hist(raw_label)
plt.title("SignalAnalyzer") plt.title("SignalAnalyzer")
plt.savefig(self.workspace.joinpath("signalAnalysis.jpeg")) plt.savefig(self.output_dir.joinpath("signalAnalysis.jpeg"))
return "signalAnalysis.jpeg" return "signalAnalysis.jpeg"

15
qlib/finco/cli_learn.py Normal file
View File

@@ -0,0 +1,15 @@
import fire
from qlib.finco.workflow import LearnManager
from dotenv import load_dotenv
from qlib import auto_init
def main(prompt=None):
load_dotenv(verbose=True, override=True)
lm = LearnManager()
lm.run(prompt)
if __name__ == "__main__":
auto_init()
fire.Fire(main)

View File

@@ -1,13 +1,17 @@
from typing import Union
from pathlib import Path
from jinja2 import Template from jinja2 import Template
import yaml
from qlib.finco.utils import Singleton from qlib.finco.utils import Singleton
from qlib.finco import get_finco_path from qlib.finco import get_finco_path
import yaml
import os
class PormptTemplate(Singleton):
class PromptTemplate(Singleton):
def __init__(self) -> None: def __init__(self) -> None:
super().__init__() super().__init__()
_template = yaml.load(open(os.path.join(get_finco_path(), "prompt_template.yaml"), "r"), Loader=yaml.FullLoader) _template = yaml.load(open(Path.joinpath(get_finco_path(), "prompt_template.yaml"), "r"),
Loader=yaml.FullLoader)
for k, v in _template.items(): for k, v in _template.items():
if k == "mods": if k == "mods":
continue continue
@@ -15,4 +19,19 @@ class PormptTemplate(Singleton):
for target_name, module_to_render_params in _template["mods"].items(): for target_name, module_to_render_params in _template["mods"].items():
for module_name, params in module_to_render_params.items(): for module_name, params in module_to_render_params.items():
self.__setattr__(f"{target_name}_{module_name}", Template(self.__getattribute__(target_name).render(**params))) self.__setattr__(f"{target_name}_{module_name}",
Template(self.__getattribute__(target_name).render(**params)))
def get(self, key: str):
return self.__dict__.get(key, Template(""))
def update(self, key: str, value):
self.__setattr__(key, value)
def save(self, file_path: Union[str, Path]):
if isinstance(file_path, str):
file_path = Path(file_path)
Path.mkdir(file_path.parent, exist_ok=True)
with open(file_path, 'w') as f:
yaml.dump(self.__dict__, f)

View File

@@ -193,6 +193,13 @@ SummarizeTask_user : |-
Here is my information: '{{information}}' Here is my information: '{{information}}'
My intention is: {{user_prompt}}. Please provide me with a summary and recommendation based on my intention and the information I have provided. There are some figures which absolute path are: {{figure_path}}, You must display these images in markdown using the appropriate image format. My intention is: {{user_prompt}}. Please provide me with a summary and recommendation based on my intention and the information I have provided. There are some figures which absolute path are: {{figure_path}}, You must display these images in markdown using the appropriate image format.
BackForwardTask_system : |-
Your task is adjusting system prompt in each task to fulfill user's intention
BackForwardTask_user : |-
Here is the final summary: '{{summary}}'
Tasks I have run are: {{task_finished}}, {{task}}'s system prompt is: {{system}}. User's intention is: {{user_prompt}}. you will adjust it to:
mods: mods:
ConfigActionTask_system: ConfigActionTask_system:
Dataset: Dataset:

View File

@@ -11,10 +11,9 @@ import platform
from qlib.finco.llm import APIBackend from qlib.finco.llm import APIBackend
from qlib.finco.tpl import get_tpl_path from qlib.finco.tpl import get_tpl_path
from qlib.finco.prompt_template import PormptTemplate from qlib.finco.prompt_template import PromptTemplate
from qlib.workflow.record_temp import HFSignalRecord, SignalRecord from qlib.workflow.record_temp import HFSignalRecord, SignalRecord
from qlib.contrib.analyzer import HFAnalyzer, SignalAnalyzer from qlib.contrib.analyzer import HFAnalyzer, SignalAnalyzer
from qlib.utils import init_instance_by_config
from qlib.workflow import R from qlib.workflow import R
from qlib.finco.log import FinCoLog, LogColors from qlib.finco.log import FinCoLog, LogColors
from qlib.finco.conf import Config from qlib.finco.conf import Config
@@ -41,7 +40,7 @@ class Task:
def __init__(self) -> None: def __init__(self) -> None:
self._context_manager = None self._context_manager = None
self.prompt_template = PormptTemplate() self.prompt_template = PromptTemplate()
self.executed = False self.executed = False
self.continuous = Config().continuous_mode self.continuous = Config().continuous_mode
self.logger = FinCoLog() self.logger = FinCoLog()
@@ -96,13 +95,11 @@ class Task:
@property @property
def system(self): def system(self):
return self.prompt_template.__getattribute__( return self.prompt_template.get(self.__class__.__name__ + "_system")
self.__class__.__name__ + "_system"
)
@property @property
def user(self): def user(self):
return self.prompt_template.__getattribute__(self.__class__.__name__ + "_user") return self.prompt_template.get(self.__class__.__name__ + "_user")
def __str__(self): def __str__(self):
return self.__class__.__name__ return self.__class__.__name__
@@ -150,7 +147,7 @@ class PlanTask(Task):
class SLPlanTask(PlanTask): class SLPlanTask(PlanTask):
def __init__(self,) -> None: def __init__(self, ) -> None:
super().__init__() super().__init__()
def execute(self): def execute(self):
@@ -220,13 +217,14 @@ class RLPlanTask(PlanTask):
return [] return []
class RecorderTask(Task): class TrainTask(Task):
""" """
This Recorder task is responsible for analysing data such as index and distribution. This train task is responsible for training model configure by yaml file.
""" """
def __init__(self): def __init__(self):
super().__init__() super().__init__()
self._output = None
def execute(self): def execute(self):
workflow_config = ( workflow_config = (
@@ -234,6 +232,7 @@ class RecorderTask(Task):
if self._context_manager.get_context("workflow_config") if self._context_manager.get_context("workflow_config")
else "workflow_config.yaml" else "workflow_config.yaml"
) )
workspace = self._context_manager.get_context("workspace") workspace = self._context_manager.get_context("workspace")
workflow_path = workspace.joinpath(workflow_config) workflow_path = workspace.joinpath(workflow_config)
with workflow_path.open() as f: with workflow_path.open() as f:
@@ -246,24 +245,19 @@ class RecorderTask(Task):
if confirm is False: if confirm is False:
return [] return []
model = init_instance_by_config(workflow["task"]["model"]) command = f"qrun {workflow_path}"
dataset = init_instance_by_config(workflow["task"]["dataset"]) self._output = subprocess.check_output(command, shell=True, cwd=workspace)
with R.start(experiment_name="finCo"):
model.fit(dataset)
R.save_objects(trained_model=model)
# prediction
recorder = R.get_recorder()
sr = SignalRecord(model, dataset, recorder)
sr.generate()
self._context_manager.set_context("model", model)
self._context_manager.set_context("dataset", dataset)
self._context_manager.set_context("recorder", recorder)
return [AnalysisTask()] return [AnalysisTask()]
def summarize(self):
if self._output is not None:
# TODO: it will be overrides by later commands
# utf8 can't decode normally on Windows
self._context_manager.set_context(
self.__class__.__name__, self._output.decode("ANSI")
)
class AnalysisTask(Task): class AnalysisTask(Task):
""" """
@@ -271,8 +265,8 @@ class AnalysisTask(Task):
""" """
__ANALYZERS_PROJECT = { __ANALYZERS_PROJECT = {
HFAnalyzer.__name__: HFSignalRecord, HFAnalyzer.__name__: HFAnalyzer,
SignalAnalyzer.__name__: SignalRecord, SignalAnalyzer.__name__: SignalAnalyzer,
} }
__ANALYZERS_DOCS = { __ANALYZERS_DOCS = {
HFAnalyzer.__name__: HFAnalyzer.__doc__, HFAnalyzer.__name__: HFAnalyzer.__doc__,
@@ -303,7 +297,7 @@ class AnalysisTask(Task):
ANALYZERS_DOCS=self.__ANALYZERS_DOCS, ANALYZERS_DOCS=self.__ANALYZERS_DOCS,
), ),
) )
analysers = response.split(",") analysers = response.replace(" ", "").split(",")
confirm = self.interact(f"I select these analysers: {analysers}\n" confirm = self.interact(f"I select these analysers: {analysers}\n"
f"Are you sure you want to use? yes(Y/y), no(N/n) or prompt:") f"Are you sure you want to use? yes(Y/y), no(N/n) or prompt:")
if confirm is False: if confirm is False:
@@ -317,15 +311,26 @@ class AnalysisTask(Task):
if isinstance(analysers, list) and len(analysers): if isinstance(analysers, list) and len(analysers):
self.logger.info(f"selected analysers: {analysers}", plain=True) self.logger.info(f"selected analysers: {analysers}", plain=True)
workflow_config = (
self._context_manager.get_context("workflow_config")
if self._context_manager.get_context("workflow_config")
else "workflow_config.yaml"
)
workspace = self._context_manager.get_context("workspace")
workflow_path = workspace.joinpath(workflow_config)
with workflow_path.open() as f:
workflow = yaml.safe_load(f)
experiment_name = workflow["experiment_name"] if "experiment_name" in workflow else "workflow"
R.set_uri(Path.joinpath(workspace, 'mlruns').as_uri())
tasks = [] tasks = []
for analyser in analysers: for analyser in analysers:
if analyser in self.__ANALYZERS_PROJECT.keys(): if analyser in self.__ANALYZERS_PROJECT.keys():
tasks.append( tasks.append(
self.__ANALYZERS_PROJECT.get(analyser)( self.__ANALYZERS_PROJECT.get(analyser)(
workspace=self._context_manager.get_context("workspace"), recorder=R.get_recorder(experiment_name=experiment_name),
model=self._context_manager.get_context("model"), output_dir=workspace
dataset=self._context_manager.get_context("dataset"),
recorder=self._context_manager.get_context("recorder"),
) )
) )
@@ -575,11 +580,14 @@ class SummarizeTask(Task):
information=information, figure_path=figure_path, user_prompt=user_prompt information=information, figure_path=figure_path, user_prompt=user_prompt
) )
# todo: remove 'be' after test
be = APIBackend() be = APIBackend()
be.debug_mode = False be.debug_mode = False
response = be.build_messages_and_create_chat_completion( response = be.build_messages_and_create_chat_completion(
user_prompt=prompt_workflow_selection, system_prompt=self.system.render() user_prompt=prompt_workflow_selection, system_prompt=self.system.render()
) )
self._context_manager.set_context("summary", response)
self.save_markdown(content=response) self.save_markdown(content=response)
self.logger.info(f"Report has saved to {self.__DEFAULT_REPORT_NAME}", title="End") self.logger.info(f"Report has saved to {self.__DEFAULT_REPORT_NAME}", title="End")

View File

@@ -1,6 +1,7 @@
qlib_init: qlib_init:
provider_uri: "~/.qlib/qlib_data/cn_data" provider_uri: "~/.qlib/qlib_data/cn_data"
region: cn region: cn
experiment_name: finCo
market: &market csi300 market: &market csi300
benchmark: &benchmark SH000300 benchmark: &benchmark SH000300
data_handler_config: &data_handler_config data_handler_config: &data_handler_config

View File

@@ -3,9 +3,11 @@ import copy
import shutil import shutil
from pathlib import Path from pathlib import Path
from qlib.finco.task import WorkflowTask, PlanTask, ActionTask, SummarizeTask, RecorderTask, AnalysisTask from qlib.finco.task import WorkflowTask, SummarizeTask, TrainTask
from qlib.finco.prompt_template import PromptTemplate, Template
from qlib.finco.log import FinCoLog, LogColors from qlib.finco.log import FinCoLog, LogColors
from qlib.finco.utils import similarity from qlib.finco.utils import similarity
from qlib.finco.llm import APIBackend
class WorkflowContextManager: class WorkflowContextManager:
@@ -51,9 +53,16 @@ class WorkflowContextManager:
max_score_key = max(scores, key=scores.get) max_score_key = max(scores, key=scores.get)
return {max_score_key: self.context.get(max_score_key)} return {max_score_key: self.context.get(max_score_key)}
def clear(self, reserve: list = None):
if reserve is None:
reserve = []
_context = {k: self.get_context(k) for k in reserve}
self.context = _context
class WorkflowManager: class WorkflowManager:
"""This manange the whole task automation workflow including tasks and actions""" """This manage the whole task automation workflow including tasks and actions"""
def __init__(self, workspace=None) -> None: def __init__(self, workspace=None) -> None:
self.logger = FinCoLog() self.logger = FinCoLog()
@@ -63,8 +72,10 @@ class WorkflowManager:
else: else:
self._workspace = Path(workspace) self._workspace = Path(workspace)
self._confirm_and_rm() self._confirm_and_rm()
self._context = WorkflowContextManager()
self._context.set_context("workspace", self._workspace) self.prompt_template = PromptTemplate()
self.context = WorkflowContextManager()
self.context.set_context("workspace", self._workspace)
self.default_user_prompt = "Please help me build a low turnover strategy that focus more on longterm return in China a stock market. Please help to pick one third of the factors in Alpha360 and use lightGBM model." self.default_user_prompt = "Please help me build a low turnover strategy that focus more on longterm return in China a stock market. Please help to pick one third of the factors in Alpha360 and use lightGBM model."
def _confirm_and_rm(self): def _confirm_and_rm(self):
@@ -87,10 +98,10 @@ class WorkflowManager:
def set_context(self, key, value): def set_context(self, key, value):
"""Direct call set_context method of the context manager""" """Direct call set_context method of the context manager"""
self._context.set_context(key, value) self.context.set_context(key, value)
def get_context(self) -> WorkflowContextManager: def get_context(self) -> WorkflowContextManager:
return self._context return self.context
def run(self, prompt: str) -> Path: def run(self, prompt: str) -> Path:
""" """
@@ -124,7 +135,7 @@ class WorkflowManager:
self.logger.info(f"user_prompt: {self.get_context().get_context('user_prompt')}", title="Start") self.logger.info(f"user_prompt: {self.get_context().get_context('user_prompt')}", title="Start")
# NOTE: list may not be enough for general task list # NOTE: list may not be enough for general task list
task_list = [WorkflowTask(), RecorderTask(), SummarizeTask()] task_list = [WorkflowTask(), TrainTask(), SummarizeTask()]
task_finished = [] task_finished = []
while len(task_list): while len(task_list):
task_list_info = [str(task) for task in task_list] task_list_info = [str(task) for task in task_list]
@@ -138,15 +149,51 @@ class WorkflowManager:
f"Executing task: {str(t)}", f"Executing task: {str(t)}",
title="Task") title="Task")
t.assign_context_manager(self._context) t.assign_context_manager(self.context)
res = t.execute() res = t.execute()
t.summarize() t.summarize()
task_finished.append(t) task_finished.append(t)
self.context.set_context("task_finished", task_finished)
self.logger.plain_info(f"{str(t)} finished.\n\n\n") self.logger.plain_info(f"{str(t)} finished.\n\n\n")
for _ in res:
if not isinstance(t, (WorkflowTask, PlanTask, ActionTask, RecorderTask, AnalysisTask, SummarizeTask)):
raise NotImplementedError(f"Unsupported Task type {_}")
task_list = res + task_list task_list = res + task_list
return self._workspace return self._workspace
class LearnManager:
def __init__(self):
self.epoch = 0
self.wm = WorkflowManager()
def run(self, prompt):
# todo: add early stop condition
for i in range(10):
self.wm.run(prompt)
self.learn()
self.epoch += 1
def learn(self):
workspace = self.wm.context.get_context("workspace")
task_finished = self.wm.context.get_context("task_finished")
user_prompt = self.wm.context.get_context("user_prompt")
summary = self.wm.context.get_context("summary")
for task in task_finished:
prompt_workflow_selection = task.user.render(
summary=summary, task_finished=[str(task) for task in task_finished],
task=task.__class__, system=task.system, user_prompt=user_prompt
)
response = APIBackend().build_messages_and_create_chat_completion(
user_prompt=prompt_workflow_selection, system_prompt=task.system.render()
)
# todo: response assertion
task.prompt_template.update(key=f"{task.__class__.__name__}_system", value=Template(response))
self.wm.prompt_template.save(Path.joinpath(workspace, f"prompts/checkpoint_{self.epoch}.yml"))
self.wm.context.clear(reserve=["workspace"])

View File

@@ -165,11 +165,10 @@ class SignalRecord(RecordTemp):
This is the Signal Record class that generates the signal prediction. This class inherits the ``RecordTemp`` class. This is the Signal Record class that generates the signal prediction. This class inherits the ``RecordTemp`` class.
""" """
def __init__(self, model=None, dataset=None, recorder=None, workspace=None): def __init__(self, model=None, dataset=None, recorder=None):
super().__init__(recorder=recorder) super().__init__(recorder=recorder)
self.model = model self.model = model
self.dataset = dataset self.dataset = dataset
self.workspace = workspace
@staticmethod @staticmethod
def generate_label(dataset): def generate_label(dataset):
@@ -208,10 +207,6 @@ class SignalRecord(RecordTemp):
raw_label = self.generate_label(self.dataset) raw_label = self.generate_label(self.dataset)
self.save(**{"label.pkl": raw_label}) self.save(**{"label.pkl": raw_label})
def analyse(self):
res = SignalAnalyzer(workspace=self.workspace).analyse(dataset=self.dataset)
return res
def list(self): def list(self):
return ["pred.pkl", "label.pkl"] return ["pred.pkl", "label.pkl"]
@@ -253,9 +248,8 @@ class HFSignalRecord(SignalRecord):
artifact_path = "hg_sig_analysis" artifact_path = "hg_sig_analysis"
depend_cls = SignalRecord depend_cls = SignalRecord
def __init__(self, recorder, workspace=None, **kwargs): def __init__(self, recorder, **kwargs):
super().__init__(recorder=recorder) super().__init__(recorder=recorder)
self.workspace = workspace
def generate(self): def generate(self):
pred = self.load("pred.pkl") pred = self.load("pred.pkl")
@@ -289,12 +283,6 @@ class HFSignalRecord(SignalRecord):
self.save(**objects) self.save(**objects)
pprint(metrics) pprint(metrics)
def analyse(self):
pred = self.load("pred.pkl")
raw_label = self.load("label.pkl")
res = HFAnalyzer(workspace=self.workspace).analyse(pred=pred, label=raw_label)
return res
def list(self): def list(self):
return ["ic.pkl", "ric.pkl", "long_pre.pkl", "short_pre.pkl", "long_short_r.pkl", "long_avg_r.pkl"] return ["ic.pkl", "ric.pkl", "long_pre.pkl", "short_pre.pkl", "long_short_r.pkl", "long_avg_r.pkl"]