From d297a493b870294d1f47e496b6664b64ed19705d Mon Sep 17 00:00:00 2001 From: bxdd Date: Fri, 30 Apr 2021 22:56:21 +0800 Subject: [PATCH] fix bugs --- examples/highfreq/backtest/workflow.py | 18 +--- qlib/contrib/backtest/account.py | 2 +- qlib/contrib/backtest/env.py | 34 +++++-- qlib/contrib/strategy/model_strategy.py | 2 +- qlib/workflow/record_temp.py | 116 +++++++++++++++--------- 5 files changed, 102 insertions(+), 70 deletions(-) diff --git a/examples/highfreq/backtest/workflow.py b/examples/highfreq/backtest/workflow.py index a4d163ce5..bbe00ed5c 100644 --- a/examples/highfreq/backtest/workflow.py +++ b/examples/highfreq/backtest/workflow.py @@ -7,8 +7,7 @@ from pathlib import Path import qlib import pandas as pd from qlib.config import REG_CN -from qlib.contrib.strategy import TopkDropoutStrategy -from qlib.contrib.backtest import backtest + from qlib.utils import exists_qlib_data, init_instance_by_config, flatten_dict from qlib.workflow import R from qlib.workflow.record_temp import PortAnaRecord @@ -130,20 +129,9 @@ if __name__ == "__main__": "min_cost": 5, }, } - - #report_dict = backtest( - # start_time=trade_start_time, - # end_time=trade_end_time, - # **backtest_config, - # account=1e8, - # benchmark=benchmark, - # deal_price="$close", - # verbose=False, - #) - with R.start(experiment_name="highfreq_backtest"): # backtest. If users want to use backtest based on their own prediction, # please refer to https://qlib.readthedocs.io/en/latest/component/recorder.html#record-template. recorder = R.get_recorder() - par = PortAnaRecord(recorder, port_analysis_config, 1) - par.generate() \ No newline at end of file + par = PortAnaRecord(recorder, port_analysis_config, "day") + par.generate() diff --git a/qlib/contrib/backtest/account.py b/qlib/contrib/backtest/account.py index 5a35ffc08..88a695f8f 100644 --- a/qlib/contrib/backtest/account.py +++ b/qlib/contrib/backtest/account.py @@ -179,7 +179,7 @@ class Account: bar_close = trade_exchange.get_close(code, trade_start_time, trade_end_time) self.current.update_stock_price(stock_id=code, price=bar_close) # update holding day count - + # update value self.val = self.current.calculate_value() # update earning diff --git a/qlib/contrib/backtest/env.py b/qlib/contrib/backtest/env.py index ea2618977..f5c84169d 100644 --- a/qlib/contrib/backtest/env.py +++ b/qlib/contrib/backtest/env.py @@ -6,7 +6,7 @@ import pathlib import numpy as np import pandas as pd from ...data.data import Cal -from ...utils import get_sample_freq_calendar +from ...utils import get_sample_freq_calendar, parse_freq from .position import Position from .report import Report from .order import Order @@ -151,16 +151,25 @@ class SplitEnv(BaseEnv): trade_state, trade_info = self.sub_env.execute(order_list=_order_list) self.trade_account.update_bar_end( - trade_start_time=trade_start_time, trade_end_time=trade_end_time, trade_exchange=self.trade_exchange, update_report=self.generate_report + trade_start_time=trade_start_time, + trade_end_time=trade_end_time, + trade_exchange=self.trade_exchange, + update_report=self.generate_report, ) _obs = {"current": self.trade_account.current} _info = {} return _obs, _info def get_report(self): - _report = self.trade_account.report.generate_report_dataframe() if self.generate_report else None - _positions = self.trade_account.get_positions() if self.generate_report else None - return [(_report, _positions), *self.sub_env.get_report()] + sub_env_report_dict = self.sub_env.get_report() + if self.generate_report: + _report = self.trade_account.report.generate_report_dataframe() + _positions = self.trade_account.get_positions() + _count, _freq = parse_freq(self.step_bar) + sub_env_report_dict.update({f"{_count}{_freq}": (_report, _positions)}) + return sub_env_report_dict + else: + return sub_env_report_dict class SimulatorEnv(BaseEnv): @@ -235,13 +244,20 @@ class SimulatorEnv(BaseEnv): # do nothing pass self.trade_account.update_bar_end( - trade_start_time=trade_start_time, trade_end_time=trade_end_time, trade_exchange=self.trade_exchange, update_report=self.generate_report + trade_start_time=trade_start_time, + trade_end_time=trade_end_time, + trade_exchange=self.trade_exchange, + update_report=self.generate_report, ) _obs = {"current": self.trade_account.current} _info = {"trade_info": trade_info} return _obs, _info def get_report(self): - _report = self.trade_account.report.generate_report_dataframe() if self.generate_report else None - _positions = self.trade_account.get_positions() if self.generate_report else None - return [(_report, _positions)] + if self.generate_report: + _report = self.trade_account.report.generate_report_dataframe() + _positions = self.trade_account.get_positions() + _count, _freq = parse_freq(self.step_bar) + return {f"{_count}{_freq}": (_report, _positions)} + else: + return {} diff --git a/qlib/contrib/strategy/model_strategy.py b/qlib/contrib/strategy/model_strategy.py index 0bd0b9e0c..4d471cf89 100644 --- a/qlib/contrib/strategy/model_strategy.py +++ b/qlib/contrib/strategy/model_strategy.py @@ -163,7 +163,7 @@ class TopkDropoutStrategy(ModelStrategy): # Get the stock list we really want to buy buy = today[: len(sell) + self.topk - len(last)] - #print("flag", len(sell), len(buy), self.topk, len(last)) + # print("flag", len(sell), len(buy), self.topk, len(last)) for code in current_stock_list: if not self.trade_exchange.is_stock_tradable( stock_id=code, start_time=trade_start_time, end_time=trade_end_time diff --git a/qlib/workflow/record_temp.py b/qlib/workflow/record_temp.py index 546fb5a60..8ed1e724f 100644 --- a/qlib/workflow/record_temp.py +++ b/qlib/workflow/record_temp.py @@ -13,7 +13,7 @@ from ..data.dataset import DatasetH from ..data.dataset.handler import DataHandlerLP from ..utils import init_instance_by_config, get_module_by_module_path from ..log import get_module_logger -from ..utils import flatten_dict +from ..utils import flatten_dict, parse_freq from ..strategy.base import BaseStrategy from ..contrib.eva.alpha import calc_ic, calc_long_short_return @@ -225,7 +225,7 @@ class PortAnaRecord(RecordTemp): artifact_path = "portfolio_analysis" - def __init__(self, recorder, config, risk_analysis_dep, **kwargs): + def __init__(self, recorder, config, risk_analysis_freq, **kwargs): """ config["strategy"] : dict define the strategy class as well as the kwargs. @@ -233,59 +233,87 @@ class PortAnaRecord(RecordTemp): define the env class as well as the kwargs. config["backtest"] : dict define the backtest kwargs. - risk_analysis_dep : int - risk analyze the dep'th env report + risk_analysis_freq : int + risk analysis freq of report """ super().__init__(recorder=recorder, **kwargs) self.strategy_config = config["strategy"] self.env_config = config["env"] self.backtest_config = config["backtest"] - self.risk_analysis_dep = risk_analysis_dep + _count, _freq = parse_freq(risk_analysis_freq) + self.risk_analysis_freq = f"{_count}{_freq}" + self.report_freq = self._get_report_freq(self.env_config) + + def _get_report_freq(self, env_config): + ret_freq = [] + if env_config["kwargs"].get("generate_report", False): + _count, _freq = parse_freq(env_config["kwargs"]["step_bar"]) + ret_freq.append(f"{_count}{_freq}") + if "sub_env" in env_config["kwargs"]: + ret_freq.extend(self._get_report_freq(env_config["kwargs"]["sub_env"])) + return ret_freq + + def _cal_risk_analysis_scaler(self, freq): + _count, _freq = parse_freq(freq) + _freq_scaler = { + "minute": 240 * 250, + "day": 250, + "week": 50, + "month": 12, + } + return _count * _freq_scaler[_freq] def generate(self, **kwargs): # custom strategy and get backtest - report_list = normal_backtest(env=self.env_config, strategy=self.strategy_config, **self.backtest_config) - for report_dep, (report_normal, positions_normal) in enumerate(report_list): - if report_normal is None: - if self.risk_analysis_dep == report_dep: - warnings.warn( - f"the report in dep {risk_analysis_dep} is None, please set the corresponding env with `generate_report==True`" - ) - continue + report_dict = normal_backtest(env=self.env_config, strategy=self.strategy_config, **self.backtest_config) + for report_freq, (report_normal, positions_normal) in report_dict.items(): + self.recorder.save_objects( + **{f"report_normal_{report_freq}.pkl": report_normal}, artifact_path=PortAnaRecord.get_path() + ) + self.recorder.save_objects( + **{f"positions_normal_{report_freq}.pkl": positions_normal}, artifact_path=PortAnaRecord.get_path() + ) - self.recorder.save_objects( - **{f"report_normal_{report_dep}.pkl": report_normal}, artifact_path=PortAnaRecord.get_path() + if self.risk_analysis_freq not in report_dict: + warnings.warn( + f"the freq {self.risk_analysis_freq} report is not found, please set the corresponding env with `generate_report==True`" ) - self.recorder.save_objects( - **{f"positions_norma_{report_dep}l.pkl": positions_normal}, artifact_path=PortAnaRecord.get_path() + else: + report_normal, _ = report_dict.get(self.risk_analysis_freq) + analysis = dict() + risk_analysis_scaler = self._cal_risk_analysis_scaler(self.risk_analysis_freq) + analysis["excess_return_without_cost"] = risk_analysis( + report_normal["return"] - report_normal["bench"], risk_analysis_scaler ) - # analysis - if self.risk_analysis_dep == report_dep: - analysis = dict() - analysis["excess_return_without_cost"] = risk_analysis(report_normal["return"] - report_normal["bench"]) - analysis["excess_return_with_cost"] = risk_analysis( - report_normal["return"] - report_normal["bench"] - report_normal["cost"] - ) - analysis_df = pd.concat(analysis) # type: pd.DataFrame - # log metrics - self.recorder.log_metrics(**flatten_dict(analysis_df["risk"].unstack().T.to_dict())) - # save results - self.recorder.save_objects( - **{f"port_analysis.pkl_{report_dep}": analysis_df}, artifact_path=PortAnaRecord.get_path() - ) - logger.info( - f"Portfolio analysis record 'port_analysis_{report_dep}.pkl' has been saved as the artifact of the Experiment {self.recorder.experiment_id}" - ) - # print out results - pprint("The following are analysis results of the excess return without cost.") - pprint(analysis["excess_return_without_cost"]) - pprint("The following are analysis results of the excess return with cost.") - pprint(analysis["excess_return_with_cost"]) + analysis["excess_return_with_cost"] = risk_analysis( + report_normal["return"] - report_normal["bench"] - report_normal["cost"], risk_analysis_scaler + ) + analysis_df = pd.concat(analysis) # type: pd.DataFrame + # log metrics + self.recorder.log_metrics(**flatten_dict(analysis_df["risk"].unstack().T.to_dict())) + # save results + self.recorder.save_objects( + **{f"port_analysis_{report_freq}.pkl": analysis_df}, artifact_path=PortAnaRecord.get_path() + ) + logger.info( + f"Portfolio analysis record 'port_analysis_{report_freq}.pkl' has been saved as the artifact of the Experiment {self.recorder.experiment_id}" + ) + # print out results + pprint("The following are analysis results of the excess return without cost.") + pprint(analysis["excess_return_without_cost"]) + pprint("The following are analysis results of the excess return with cost.") + pprint(analysis["excess_return_with_cost"]) def list(self): - return [ - PortAnaRecord.get_path("report_normal.pkl"), - PortAnaRecord.get_path("positions_normal.pkl"), - PortAnaRecord.get_path("port_analysis.pkl"), - ] + list_path = [] + for _freq in self.report_freq: + list_path.extend( + [ + PortAnaRecord.get_path(f"report_normal_{_freq}.pkl"), + PortAnaRecord.get_path(f"positions_normal_{_freq}.pkl"), + ] + ) + if _freq == self.risk_analysis_freq: + list_path.append(PortAnaRecord.get_path(f"port_analysis_{_freq}.pkl")) + return list_path