diff --git a/examples/workflow_by_code.py b/examples/workflow_by_code.py index 8fdb4332f..b8cf3f935 100644 --- a/examples/workflow_by_code.py +++ b/examples/workflow_by_code.py @@ -98,6 +98,7 @@ if __name__ == "__main__": "open_cost": 0.0005, "close_cost": 0.0015, "min_cost": 5, + "return_order": True, }, } diff --git a/examples/workflow_with_highfreq_backtest.py b/examples/workflow_with_highfreq_backtest.py deleted file mode 100644 index 682ec7a7f..000000000 --- a/examples/workflow_with_highfreq_backtest.py +++ /dev/null @@ -1,174 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. - -import sys -from pathlib import Path - -import qlib -import pandas as pd -from qlib.config import REG_CN -from qlib.contrib.model.gbdt import LGBModel -from qlib.contrib.data.handler import Alpha158 -from qlib.contrib.strategy.strategy import TopkDropoutStrategy -from qlib.contrib.evaluate import ( - backtest as normal_backtest, - risk_analysis, -) -from qlib.utils import exists_qlib_data, init_instance_by_config, flatten_dict -from qlib.workflow import R -from qlib.workflow.record_temp import SignalRecord, PortAnaRecord - - -if __name__ == "__main__": - - # use default data - provider_uri = "~/.qlib/qlib_data/cn_data" # target_dir - if not exists_qlib_data(provider_uri): - print(f"Qlib data is not found in {provider_uri}") - sys.path.append(str(Path(__file__).resolve().parent.parent.joinpath("scripts"))) - from get_data import GetData - - GetData().qlib_data(target_dir=provider_uri, region=REG_CN) - - qlib.init(provider_uri=provider_uri, region=REG_CN) - - market = "csi300" - benchmark = "SH000300" - - ################################### - # train model - ################################### - data_handler_config = { - "start_time": "2012-01-01", - "end_time": "2019-06-01", - "fit_start_time": "2012-01-01", - "fit_end_time": "2017-04-30", - "instruments": market, - } - - task = { - "model": { - "class": "LGBModel", - "module_path": "qlib.contrib.model.gbdt", - "kwargs": { - "loss": "mse", - "colsample_bytree": 0.8879, - "learning_rate": 0.0421, - "subsample": 0.8789, - "lambda_l1": 205.6999, - "lambda_l2": 580.9768, - "max_depth": 8, - "num_leaves": 210, - "num_threads": 20, - }, - }, - "dataset": { - "class": "DatasetH", - "module_path": "qlib.data.dataset", - "kwargs": { - "handler": { - "class": "Alpha158", - "module_path": "qlib.contrib.data.handler", - "kwargs": data_handler_config, - }, - "segments": { - "train": ("2012-01-01", "2017-04-30"), - "valid": ("2017-05-01", "2019-04-30"), - "test": ("2019-05-01", "2019-06-01"), - }, - }, - }, - } - - highfreq_executor_config = { - "log_dir": '/shared_data/data/v-xiabi/highfreq-exe/log/', - "is_multi": True, - "resources": { - "num_cpus": 48, - "num_gpus": 2, - 'device': 'cpu', - }, - "paths": { - "raw_dir": "/shared_data/data/v-xiabi/highfreq-exe/data/backtest_test_multi", - "feature_conf": "/shared_data/data/v-xiabi/highfreq-exe/code/rl4execution/config/test_feature_all1620.json", - }, - "env_conf": { - "name": "MARL_Accelerated", - "max_step_num": 237, - "limit": 10, - "time_interval": 30, - "interval_num": 8, - "features": "raw_30", - "max_agent_num": 49, - "log": True, - "obs": { - "name": "MultiTeacherObs", - "config": {} - }, - "action": { - "name": "Multi_Static", - "config": { - 'action_num':5, - 'action_map': [0, 0.25, 0.5, 0.75, 1], - } - }, - "reward": { - "name": "Multi_VP_Penalty_small", - "config": { - "action_penalty": 100, - "hit_penalty": 1., - } - }, - }, - "policy_conf": { - "name": "Multi_RL_backtest", - "config": { - "buy_policy": '/shared_data/data/v-xiabi/highfreq-exe/model/OPDS_buy/policy_best', - 'sell_policy': '/shared_data/data/v-xiabi/highfreq-exe/model/OPDS_sell/policy_best', - }, - }, - } - - port_analysis_config = { - "strategy": { - "class": "TopkDropoutStrategy", - "module_path": "qlib.contrib.strategy.strategy", - "kwargs": { - "topk": 50, - "n_drop": 5, - }, - }, - "backtest": { - "verbose": False, - "limit_threshold": 0.095, - "account": 100000000, - "benchmark": benchmark, - "deal_price": "close", - "open_cost": 0.0005, - "close_cost": 0.0015, - "min_cost": 5, - "highfreq_executor": { - "class": "Online_Executor", - "module_path": "/shared_data/data/v-xiabi/highfreq-exe/code/rl4execution/executor.py", - "kwargs": highfreq_executor_config, - } - }, - } - - # model initiaiton - model = init_instance_by_config(task["model"]) - dataset = init_instance_by_config(task["dataset"]) - - # start exp - with R.start(experiment_name="workflow"): - R.log_params(**flatten_dict(task)) - model.fit(dataset) - - # prediction - recorder = R.get_recorder() - sr = SignalRecord(model, dataset, recorder) - sr.generate() - - # backtest - par = PortAnaRecord(recorder, port_analysis_config) - par.generate() diff --git a/qlib/contrib/backtest/backtest.py b/qlib/contrib/backtest/backtest.py index c14699bac..b87d6afe3 100644 --- a/qlib/contrib/backtest/backtest.py +++ b/qlib/contrib/backtest/backtest.py @@ -5,7 +5,6 @@ import numpy as np import pandas as pd from ...utils import get_date_by_shift, get_date_range -from ..online.executor import SimulatorExecutor from ...data import D from .account import Account from ...config import C @@ -15,7 +14,7 @@ from ...data.dataset.utils import get_level_index LOG = get_module_logger("backtest") -def backtest(pred, strategy, trade_exchange, shift, verbose, account, benchmark, return_order): +def backtest(pred, strategy, executor, trade_exchange, shift, verbose, account, benchmark, return_order): """Parameters ---------- pred : pandas.DataFrame @@ -70,8 +69,8 @@ def backtest(pred, strategy, trade_exchange, shift, verbose, account, benchmark, bench = _temp_result.groupby(level="datetime")[_temp_result.columns.tolist()[0]].mean() trade_dates = np.append(predict_dates[shift:], get_date_range(predict_dates[-1], left_shift=1, right_shift=shift)) - executor = SimulatorExecutor(trade_exchange, verbose=verbose) - order_set = [] + if return_order: + multi_order_list = [] # trading apart for pred_date, trade_date in zip(predict_dates, trade_dates): # for loop predict date and trading date @@ -103,8 +102,8 @@ def backtest(pred, strategy, trade_exchange, shift, verbose, account, benchmark, ) else: order_list = [] - - order_set.append((trade_account, order_list, trade_date)) + if return_order: + multi_order_list.append((trade_account, order_list, trade_date)) # 4. Get result after executing order list # NOTE: The following operation will modify order.amount. # NOTE: If it is buy and the cash is insufficient, the tradable amount will be recalculated @@ -113,53 +112,16 @@ def backtest(pred, strategy, trade_exchange, shift, verbose, account, benchmark, # 5. Update account information according to transaction update_account(trade_account, trade_info, trade_exchange, trade_date) - if return_order: - return order_set - else: - # generate backtest report - report_df = trade_account.report.generate_report_dataframe() - report_df["bench"] = bench - positions = trade_account.get_positions() - return report_df, positions - -def backtest_highfreq(pred, executor, trade_exchange, shift, order_set, verbose, account, benchmark): - trade_account_highfreq = Account(init_cash=account) - _pred_dates = pred.index.get_level_values(level="datetime") - predict_dates = D.calendar(start_time=_pred_dates.min(), end_time=_pred_dates.max()) - - if isinstance(benchmark, pd.Series): - bench = benchmark - else: - _codes = benchmark if isinstance(benchmark, list) else [benchmark] - _temp_result = D.features( - _codes, - ["$close/Ref($close,1)-1"], - predict_dates[0], - get_date_by_shift(predict_dates[-1], shift=shift), - disk_cache=1, - ) - if len(_temp_result) == 0: - raise ValueError(f"The benchmark {_codes} does not exist. Please provide the right benchmark") - bench = _temp_result.groupby(level="datetime")[_temp_result.columns.tolist()[0]].mean() - - for trade_account, order_list, trade_date in order_set: - if verbose: - LOG.info("[I {:%Y-%m-%d}]: highfreq trade begin.".format(trade_date)) - ## TODO: kanren group need to merge code here - print(trade_account, order_list, trade_date) - executor.execute(trade_account, order_list, trade_date) - - for trade_account, order_list, trade_date in order_set: - trade_info = executor.get_res() - print(trade_info) - update_account(trade_account_highfreq, trade_info, trade_exchange, trade_date) - if verbose: - LOG.info("[I {:%Y-%m-%d}]: highfreq trade end.".format(trade_date)) - executor.close() - report_df = trade_account_highfreq.report.generate_report_dataframe() + # generate backtest report + report_df = trade_account.report.generate_report_dataframe() report_df["bench"] = bench - positions = trade_account_highfreq.get_positions() - return report_df, positions + positions = trade_account.get_positions() + + report_dict = {"report_df": report_df, "positions": positions} + if return_order: + report_dict.update({"order_list": multi_order_list}) + return report_dict + def update_account(trade_account, trade_info, trade_exchange, trade_date): """Update the account and strategy diff --git a/qlib/contrib/evaluate.py b/qlib/contrib/evaluate.py index 7232c3854..44627eef1 100644 --- a/qlib/contrib/evaluate.py +++ b/qlib/contrib/evaluate.py @@ -11,7 +11,8 @@ from ..log import get_module_logger from . import strategy as strategy_pool from .strategy.strategy import BaseStrategy from .backtest.exchange import Exchange -from .backtest.backtest import backtest as backtest_func, get_date_range, backtest_highfreq as backtest_highfreq_func +from .backtest.backtest import backtest as backtest_func, get_date_range +from .online.executor import BaseExecutor, SimulatorExecutor from ..data import D from ..config import C @@ -100,7 +101,7 @@ def get_strategy( "weight": "TopkWeightStrategy", "dropout": "TopkDropoutStrategy", } - logger.info("Create new streategy ") + logger.info("Create new strategy ") str_cls = getattr(strategy_pool, str_cls_dict.get(str_type)) strategy = str_cls( topk=topk, @@ -111,6 +112,7 @@ def get_strategy( ) elif isinstance(strategy, (dict, str)): # 2) create strategy with init_instance_by_config + logger.info("Create new strategy ") strategy = init_instance_by_config(strategy) # else: nothing happens. 3) Use the strategy directly @@ -196,8 +198,48 @@ def get_exchange( return exchange +def get_executor( + executor=None, + trade_exchange=None, + verbose=True, +): + """get_executor + + There will be 3 ways to return a executor. Please follow the code. + + Parameters + ---------- + + executor : BaseExecutor + executor used in backtest. + trade_exchange : Exchange + exchange used in executor + verbose : bool + whether to print log. + + Returns + ------- + :class: BaseExecutor + an initialized BaseExecutor object + """ + # There will be 3 ways to return a executor. + if executor is None: + # 1) create executor with param `executor` + logger.info("Create new executor ") + executor = SimulatorExecutor(trade_exchange=trade_exchange, verbose=verbose) + elif isinstance(executor, (dict, str)): + # 2) create executor with config + logger.info("Create new executor ") + executor = init_instance_by_config(executor) + + # 3) Use the executor directly + if not isinstance(executor, BaseExecutor): + raise TypeError("Executor not supported") + return executor + + # This is the API for compatibility for legacy code -def backtest(pred, account=1e9, shift=1, benchmark="SH000905", verbose=True, **kwargs): +def backtest(pred, account=1e9, shift=1, benchmark="SH000905", verbose=True, return_order=False, **kwargs): """This function will help you set a reasonable Exchange and provide default value for strategy Parameters ---------- @@ -214,6 +256,8 @@ def backtest(pred, account=1e9, shift=1, benchmark="SH000905", verbose=True, **k benchmark code, default is SH000905 CSI 500. verbose : bool whether to print log. + return_order : bool + whther to return order list - **strategy related arguments** @@ -261,6 +305,14 @@ def backtest(pred, account=1e9, shift=1, benchmark="SH000905", verbose=True, **k will we pass the codes extracted from the pred to the exchange. .. note:: This will be faster with offline qlib. + + - **executor related arguments** + + executor : BaseExecutor() + executor used in backtest. + verbose : bool + whether to print log. + """ # check strategy: spec = inspect.getfullargspec(get_strategy) @@ -271,45 +323,27 @@ def backtest(pred, account=1e9, shift=1, benchmark="SH000905", verbose=True, **k spec = inspect.getfullargspec(get_exchange) ex_args = {k: v for k, v in kwargs.items() if k in spec.args} trade_exchange = get_exchange(pred, **ex_args) - if kwargs.get('highfreq_executor', False): - order_set = backtest_func( - pred=pred, - strategy=strategy, - trade_exchange=trade_exchange, - shift=shift, - verbose=verbose, - account=account, - benchmark=benchmark, - return_order=True, - ) - executor = init_instance_by_config(kwargs.get('highfreq_executor')) - report_df, positions = backtest_highfreq_func( - pred=pred, - executor=executor, - trade_exchange=trade_exchange, - shift=shift, - order_set=order_set, - verbose=verbose, - account=account, - benchmark=benchmark - ) - positions = {k: p.position for k, p in positions.items()} - return report_df, positions - else: - # run backtest - report_df, positions = backtest_func( - pred=pred, - strategy=strategy, - trade_exchange=trade_exchange, - shift=shift, - verbose=verbose, - account=account, - benchmark=benchmark, - return_order=False, - ) - # for compatibility of the old API. return the dict positions - positions = {k: p.position for k, p in positions.items()} - return report_df, positions + + # init executor: + executor = get_executor(executor=kwargs.get("executor"), trade_exchange=trade_exchange, verbose=verbose) + + # run backtest + report_dict = backtest_func( + pred=pred, + strategy=strategy, + executor=executor, + trade_exchange=trade_exchange, + shift=shift, + verbose=verbose, + account=account, + benchmark=benchmark, + return_order=return_order, + ) + # for compatibility of the old API. return the dict positions + + positions = report_dict.get("positions") + report_dict.update({"positions": {k: p.position for k, p in positions.items()}}) + return report_dict def long_short_backtest( diff --git a/qlib/workflow/record_temp.py b/qlib/workflow/record_temp.py index bcbcd3cb4..188857e86 100644 --- a/qlib/workflow/record_temp.py +++ b/qlib/workflow/record_temp.py @@ -241,9 +241,14 @@ class PortAnaRecord(SignalRecord): # custom strategy and get backtest pred_score = super().load() - report_normal, positions_normal = normal_backtest(pred_score, strategy=self.strategy, **self.backtest_config) + report_dict = normal_backtest(pred_score, strategy=self.strategy, **self.backtest_config) + report_normal = report_dict.get("report_df") + positions_normal = report_dict.get("positions") self.recorder.save_objects(**{"report_normal.pkl": report_normal}, artifact_path=PortAnaRecord.get_path()) self.recorder.save_objects(**{"positions_normal.pkl": positions_normal}, artifact_path=PortAnaRecord.get_path()) + order_normal = report_dict.get("order_list") + if order_normal: + self.recorder.save_objects(**{"order_normal.pkl": order_normal}, artifact_path=PortAnaRecord.get_path()) # analysis analysis = dict()