From 54d5e1fbdcecf4dbe99162a4dab92a05d4d08406 Mon Sep 17 00:00:00 2001 From: Young Date: Sat, 10 Oct 2020 09:27:18 +0000 Subject: [PATCH 001/241] change the init order --- qlib/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qlib/__init__.py b/qlib/__init__.py index f63aa26cc..8d0b322b1 100644 --- a/qlib/__init__.py +++ b/qlib/__init__.py @@ -38,13 +38,13 @@ def init(default_conf="client", **kwargs): LOG.info(f"default_conf: {default_conf}.") C.set_mode(default_conf) + C.set_region(kwargs.get('region', C['region'] if 'region' in C else REG_CN )) for k, v in kwargs.items(): C[k] = v if k not in C: LOG.warning("Unrecognized config %s" % k) - C.set_region(kwargs.get("region", C["region"] if "region" in C else REG_CN)) C.resolve_path() if not (C["expression_cache"] is None and C["dataset_cache"] is None): From 77e2f25f7b9f716a3fb685565613459b8c5e4645 Mon Sep 17 00:00:00 2001 From: Young Date: Sat, 10 Oct 2020 09:35:10 +0000 Subject: [PATCH 002/241] rename modules --- qlib/{contrib/estimator => data/dataset}/handler.py | 0 qlib/{contrib => }/model/__init__.py | 0 qlib/{contrib => }/model/base.py | 0 qlib/{contrib => }/model/gbdt.py | 0 qlib/{contrib => }/model/pytorch_nn.py | 0 5 files changed, 0 insertions(+), 0 deletions(-) rename qlib/{contrib/estimator => data/dataset}/handler.py (100%) rename qlib/{contrib => }/model/__init__.py (100%) rename qlib/{contrib => }/model/base.py (100%) rename qlib/{contrib => }/model/gbdt.py (100%) rename qlib/{contrib => }/model/pytorch_nn.py (100%) diff --git a/qlib/contrib/estimator/handler.py b/qlib/data/dataset/handler.py similarity index 100% rename from qlib/contrib/estimator/handler.py rename to qlib/data/dataset/handler.py diff --git a/qlib/contrib/model/__init__.py b/qlib/model/__init__.py similarity index 100% rename from qlib/contrib/model/__init__.py rename to qlib/model/__init__.py diff --git a/qlib/contrib/model/base.py b/qlib/model/base.py similarity index 100% rename from qlib/contrib/model/base.py rename to qlib/model/base.py diff --git a/qlib/contrib/model/gbdt.py b/qlib/model/gbdt.py similarity index 100% rename from qlib/contrib/model/gbdt.py rename to qlib/model/gbdt.py diff --git a/qlib/contrib/model/pytorch_nn.py b/qlib/model/pytorch_nn.py similarity index 100% rename from qlib/contrib/model/pytorch_nn.py rename to qlib/model/pytorch_nn.py From d4091a87112401255da035c8800b30e0a4407ba9 Mon Sep 17 00:00:00 2001 From: Young Date: Mon, 12 Oct 2020 02:12:27 +0000 Subject: [PATCH 003/241] split code into core and contrib for data&model --- README.md | 4 +- docs/advanced/alpha.rst | 2 +- docs/component/data.rst | 10 +- docs/component/estimator.rst | 10 +- docs/component/model.rst | 8 +- docs/reference/api.rst | 4 +- docs/start/integration.rst | 8 +- examples/estimator/estimator_config.yaml | 2 +- examples/estimator/estimator_config_dnn.yaml | 2 +- examples/train_and_backtest.py | 2 +- qlib/contrib/data/handler.py | 63 +++++ qlib/contrib/estimator/config.py | 4 +- qlib/contrib/model/__init__.py | 0 qlib/{ => contrib}/model/gbdt.py | 2 +- qlib/{ => contrib}/model/pytorch_nn.py | 2 +- qlib/data/dataset/__init__.py | 0 qlib/data/dataset/handler.py | 70 ------ qlib/data/dataset/processor.py | 249 +++++++++++++++++++ qlib/log.py | 6 +- tests/test_all_pipeline.py | 2 +- 20 files changed, 346 insertions(+), 104 deletions(-) create mode 100644 qlib/contrib/data/handler.py create mode 100644 qlib/contrib/model/__init__.py rename qlib/{ => contrib}/model/gbdt.py (98%) rename qlib/{ => contrib}/model/pytorch_nn.py (99%) create mode 100644 qlib/data/dataset/__init__.py create mode 100644 qlib/data/dataset/processor.py diff --git a/README.md b/README.md index 988a8a385..44b2da388 100644 --- a/README.md +++ b/README.md @@ -195,8 +195,8 @@ Your PR of new Quant models is highly welcomed. # Quant Dataset Zoo Dataset plays a very important role in Quant. Here is a list of the datasets built on `Qlib`. -- [Alpha360](./qlib/contrib/estimator/handler.py) -- [Alpha158](./qlib/contrib/estimator/handler.py) +- [Alpha360](./qlib/contrib/data/handler.py) +- [Alpha158](./qlib/contrib/data/handler.py) [Here](https://qlib.readthedocs.io/en/latest/advanced/alpha.html) is a tutorial to build dataset with `Qlib`. Your PR to build new Quant dataset is highly welcomed. diff --git a/docs/advanced/alpha.rst b/docs/advanced/alpha.rst index be30ea8a7..bba6c3980 100644 --- a/docs/advanced/alpha.rst +++ b/docs/advanced/alpha.rst @@ -49,7 +49,7 @@ Users can use ``Data Handler`` to build formulaic alphas `MACD` in qlib: .. code-block:: python - >> from qlib.contrib.estimator.handler import QLibDataHandler + >> from qlib.data.dataset.handler import QLibDataHandler >> MACD_EXP = '(EMA($close, 12) - EMA($close, 26))/$close - EMA((EMA($close, 12) - EMA($close, 26))/$close, 9)/$close' >> fields = [MACD_EXP] # MACD >> names = ['MACD'] diff --git a/docs/component/data.rst b/docs/component/data.rst index 7c374f1dd..eb9673e92 100644 --- a/docs/component/data.rst +++ b/docs/component/data.rst @@ -156,12 +156,12 @@ Data Handler Users can use ``Data Handler`` in an automatic workflow by ``Estimator``, refer to `Estimator: Workflow Management `_ for more details. -Also, ``Data Handler`` can be used as an independent module, by which users can easily preprocess data(standardization, remove NaN, etc.) and build datasets. It is a subclass of ``qlib.contrib.estimator.handler.BaseDataHandler``, which provides some interfaces as follows. +Also, ``Data Handler`` can be used as an independent module, by which users can easily preprocess data(standardization, remove NaN, etc.) and build datasets. It is a subclass of ``qlib.data.dataset.handler.BaseDataHandler``, which provides some interfaces as follows. Base Class & Interface ---------------------- -Qlib provides a base class `qlib.contrib.estimator.BaseDataHandler <../reference/api.html#qlib.contrib.estimator.handler.BaseDataHandler>`_, which provides the following interfaces: +Qlib provides a base class `qlib.data.dataset.BaseDataHandler <../reference/api.html#qlib.data.dataset.handler.BaseDataHandler>`_, which provides the following interfaces: - `setup_feature` Implement the interface to load the data features. @@ -182,7 +182,7 @@ Qlib also provides two functions to help users init the data handler, users can Users can init the raw df, feature names, and label names of data handler in this function. If the index of feature df and label df are not the same, users need to override this method to merge them (e.g. inner, left, right merge). -If users want to load features and labels by config, users can inherit ``qlib.contrib.estimator.handler.ConfigDataHandler``, ``Qlib`` also provides some preprocess method in this subclass. +If users want to load features and labels by config, users can inherit ``qlib.data.dataset.handler.ConfigDataHandler``, ``Qlib`` also provides some preprocess method in this subclass. If users want to use qlib data, `QLibDataHandler` is recommended. Users can inherit their custom class from `QLibDataHandler`, which is also a subclass of `ConfigDataHandler`. @@ -214,7 +214,7 @@ Qlib provides implemented data handler `Alpha158`. The following example shows h .. code-block:: Python - from qlib.contrib.estimator.handler import Alpha158 + from qlib.contrib.data.handler import Alpha158 from qlib.contrib.model.gbdt import LGBModel DATA_HANDLER_CONFIG = { @@ -251,7 +251,7 @@ Also, the above example has been given in ``examples.estimator.train_backtest_an API --------- -To know more about ``Data Handler``, please refer to `Data Handler API <../reference/api.html#module-qlib.contrib.estimator.handler>`_. +To know more about ``Data Handler``, please refer to `Data Handler API <../reference/api.html#module-qlib.data.dataset.handler>`_. Cache ========== diff --git a/docs/component/estimator.rst b/docs/component/estimator.rst index 917d73c13..e306c2f25 100644 --- a/docs/component/estimator.rst +++ b/docs/component/estimator.rst @@ -266,7 +266,7 @@ Users can use a specified model by configuration with hyper-parameters. Custom Models ~~~~~~~~~~~~~~~~~ -Qlib supports custom models, but it must be a subclass of the `qlib.contrib.model.Model`, the config for a custom model may be as following. +Qlib supports custom models, but it must be a subclass of the `qlib.model.Model`, the config for a custom model may be as following. .. code-block:: YAML @@ -284,7 +284,7 @@ To know more about ``Interday Model``, please refer to `Interday Model: Training Data Section ----------------- -``Data Handler`` can be used to load raw data, prepare features and label columns, preprocess data (standardization, remove NaN, etc.), split training, validation, and test sets. It is a subclass of `qlib.contrib.estimator.handler.BaseDataHandler`. +``Data Handler`` can be used to load raw data, prepare features and label columns, preprocess data (standardization, remove NaN, etc.), split training, validation, and test sets. It is a subclass of `qlib.data.dataset.handler.BaseDataHandler`. Users can use the specified data handler by config as follows. @@ -315,10 +315,10 @@ Users can use the specified data handler by config as follows. fend_time: 2018-12-11 - `class` - Data handler class, str type, which should be a subclass of `qlib.contrib.estimator.handler.BaseDataHandler`, and implements 5 important interfaces for loading features, loading raw data, preprocessing raw data, slicing train, validation, and test data. The default value is `ALPHA360`. If users want to write a data handler to retrieve the data in ``Qlib``, `QlibDataHandler` is suggested. + Data handler class, str type, which should be a subclass of `qlib.data.dataset.handler.BaseDataHandler`, and implements 5 important interfaces for loading features, loading raw data, preprocessing raw data, slicing train, validation, and test data. The default value is `ALPHA360`. If users want to write a data handler to retrieve the data in ``Qlib``, `QlibDataHandler` is suggested. - `module_path` - The module path, str type, absolute url is also supported, indicates the path of the `class` implementation of the data processor class. The default value is `qlib.contrib.estimator.handler`. + The module path, str type, absolute url is also supported, indicates the path of the `class` implementation of the data processor class. The default value is `qlib.data.dataset.handler`. - `args` Parameters used for ``Data Handler`` initialization. @@ -376,7 +376,7 @@ Qlib support custom data handler, but it must be a subclass of the ``qlib.contri The class `SomeDataHandler` should be in the module `custom_data_handler`, and ``Qlib`` could parse the `module_path` to load the class. -If users want to load features and labels by config, they can inherit ``qlib.contrib.estimator.handler.ConfigDataHandler``, ``Qlib`` also has provided some preprocess methods in this subclass. +If users want to load features and labels by config, they can inherit ``qlib.data.dataset.handler.ConfigDataHandler``, ``Qlib`` also has provided some preprocess methods in this subclass. If users want to use qlib data, `QLibDataHandler` is recommended, from which users can inherit the custom class. `QLibDataHandler` is also a subclass of `ConfigDataHandler`. To know more about ``Data Handler``, please refer to `Data Framework&Usage `_. diff --git a/docs/component/model.rst b/docs/component/model.rst index 0cd375a24..52cda79e7 100644 --- a/docs/component/model.rst +++ b/docs/component/model.rst @@ -13,7 +13,7 @@ Because the components in ``Qlib`` are designed in a loosely-coupled way, ``Inte Base Class & Interface ====================== -``Qlib`` provides a base class `qlib.contrib.model.base.Model <../reference/api.html#module-qlib.contrib.model.base>`_ from which all models should inherit. +``Qlib`` provides a base class `qlib.model.base.Model <../reference/api.html#module-qlib.model.base>`_ from which all models should inherit. The base class provides the following interfaces: @@ -110,7 +110,7 @@ The base class provides the following interfaces: The format of `w_test` is same as `w_train` in `fit` method. - Return: float type, evaluation score -For other interfaces such as `save`, `load`, `finetune`, please refer to `Model API <../reference/api.html#module-qlib.contrib.model.base>`_. +For other interfaces such as `save`, `load`, `finetune`, please refer to `Model API <../reference/api.html#module-qlib.model.base>`_. Example ================== @@ -121,7 +121,7 @@ Example - Run the following code to get the `prediction score` `pred_score` .. code-block:: Python - from qlib.contrib.estimator.handler import Alpha158 + from qlib.contrib.data.handler import Alpha158 from qlib.contrib.model.gbdt import LGBModel DATA_HANDLER_CONFIG = { @@ -175,4 +175,4 @@ Qlib supports custom models. If users are interested in customizing their own mo API =================== -Please refer to `Model API <../reference/api.html#module-qlib.contrib.model.base>`_. +Please refer to `Model API <../reference/api.html#module-qlib.model.base>`_. diff --git a/docs/reference/api.rst b/docs/reference/api.rst index ea1a545e2..637a32053 100644 --- a/docs/reference/api.rst +++ b/docs/reference/api.rst @@ -63,12 +63,12 @@ Contrib Data Handler --------------- -.. automodule:: qlib.contrib.estimator.handler +.. automodule:: qlib.data.dataset.handler :members: Model -------------------- -.. automodule:: qlib.contrib.model.base +.. automodule:: qlib.model.base :members: Strategy diff --git a/docs/start/integration.rst b/docs/start/integration.rst index 2732f61df..5276729b5 100644 --- a/docs/start/integration.rst +++ b/docs/start/integration.rst @@ -9,13 +9,13 @@ Introduction Users can integrate their own custom models according to the following steps. -- Define a custom model class, which should be a subclass of the `qlib.contrib.model.base.Model <../reference/api.html#module-qlib.contrib.model.base>`_. +- Define a custom model class, which should be a subclass of the `qlib.model.base.Model <../reference/api.html#module-qlib.model.base>`_. - Write a configuration file that describes the path and parameters of the custom model. - Test the custom model. Custom Model Class =========================== -The Custom models need to inherit `qlib.contrib.model.base.Model <../reference/api.html#module-qlib.contrib.model.base>`_ and override the methods in it. +The Custom models need to inherit `qlib.model.base.Model <../reference/api.html#module-qlib.model.base>`_ and override the methods in it. - Override the `__init__` method - ``Qlib`` passes the initialized parameters to the \_\_init\_\_ method. @@ -63,7 +63,7 @@ The Custom models need to inherit `qlib.contrib.model.base.Model <../reference/a - Override the `predict` method - The parameters include the test features. - Return the `prediction score`. - - Please refer to `Model API <../reference/api.html#module-qlib.contrib.model.base>`_ for the parameter types of the fit method. + - Please refer to `Model API <../reference/api.html#module-qlib.model.base>`_ for the parameter types of the fit method. - Code Example: In the following example, users need to use dnn to predict the label(such as `preds`) of test data `x_test` and return it. .. code-block:: Python @@ -143,4 +143,4 @@ Also, ``Model`` can also be tested as a single module. An example has been given Reference ===================== -To know more about ``Interday Model``, please refer to `Interday Model: Model Training & Prediction <../component/model.html>`_ and `Model API <../reference/api.html#module-qlib.contrib.model.base>`_. +To know more about ``Interday Model``, please refer to `Interday Model: Model Training & Prediction <../component/model.html>`_ and `Model API <../reference/api.html#module-qlib.model.base>`_. diff --git a/examples/estimator/estimator_config.yaml b/examples/estimator/estimator_config.yaml index 7b532ca40..eaffc181b 100644 --- a/examples/estimator/estimator_config.yaml +++ b/examples/estimator/estimator_config.yaml @@ -5,7 +5,7 @@ experiment: model: class: LGBModel - module_path: qlib.contrib.model.gbdt + module_path: qlib.gbdt.model.gbdt args: loss: mse colsample_bytree: 0.8879 diff --git a/examples/estimator/estimator_config_dnn.yaml b/examples/estimator/estimator_config_dnn.yaml index a4a9d18ff..1aa122313 100644 --- a/examples/estimator/estimator_config_dnn.yaml +++ b/examples/estimator/estimator_config_dnn.yaml @@ -4,7 +4,7 @@ experiment: mode: train model: - module_path: qlib.contrib.model.pytorch_nn + module_path: qlib.model.pytorch_nn class: DNNModelPytorch args: loss: mse diff --git a/examples/train_and_backtest.py b/examples/train_and_backtest.py index 39cae20b1..def50b75a 100644 --- a/examples/train_and_backtest.py +++ b/examples/train_and_backtest.py @@ -8,7 +8,7 @@ import qlib import pandas as pd from qlib.config import REG_CN from qlib.contrib.model.gbdt import LGBModel -from qlib.contrib.estimator.handler import Alpha158 +from qlib.contrib.data.handler import Alpha158 from qlib.contrib.strategy.strategy import TopkDropoutStrategy from qlib.contrib.evaluate import ( backtest as normal_backtest, diff --git a/qlib/contrib/data/handler.py b/qlib/contrib/data/handler.py new file mode 100644 index 000000000..23dab22ee --- /dev/null +++ b/qlib/contrib/data/handler.py @@ -0,0 +1,63 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +from ...data.dataset.handler import ConfigQLibDataHandler +from ...log import TimeInspector + + +class ALPHA360(ConfigQLibDataHandler): + config_template = { + "price": {"windows": range(60)}, + "volume": {"windows": range(60)}, + } + + +class QLibDataHandlerV1(ConfigQLibDataHandler): + config_template = { + "kbar": {}, + "price": { + "windows": [0], + "feature": ["OPEN", "HIGH", "LOW", "VWAP"], + }, + "rolling": {}, + } + + def __init__(self, start_date, end_date, processors=None, **kwargs): + if processors is None: + processors = ["PanelProcessor"] # V1 default processor + super().__init__(start_date, end_date, processors, **kwargs) + + def setup_label(self): + """ + load the labels df + :return: df_labels + """ + TimeInspector.set_time_mark() + + df_labels = super().setup_label() + + ## calculate new labels + df_labels["LABEL1"] = df_labels["LABEL0"].groupby(level="datetime").apply(lambda x: (x - x.mean()) / x.std()) + + df_labels = df_labels.drop(["LABEL0"], axis=1) + + TimeInspector.log_cost_time("Finished loading labels.") + + return df_labels + + +class Alpha158(QLibDataHandlerV1): + config_template = { + "kbar": {}, + "price": { + "windows": [0], + "feature": ["OPEN", "HIGH", "LOW", "CLOSE"], + }, + "rolling": {}, + } + + def _init_kwargs(self, **kwargs): + kwargs["labels"] = ["Ref($close, -2)/Ref($close, -1) - 1"] + super(Alpha158, self)._init_kwargs(**kwargs) + + diff --git a/qlib/contrib/estimator/config.py b/qlib/contrib/estimator/config.py index 5a4a31613..0d782c412 100644 --- a/qlib/contrib/estimator/config.py +++ b/qlib/contrib/estimator/config.py @@ -103,7 +103,7 @@ class DataConfig(object): :param config: The config dict for data :param CONFIG_MANAGER: The estimator config manager """ - self.handler_module_path = config.get("module_path", "qlib.contrib.estimator.handler") + self.handler_module_path = config.get("module_path", "qlib.contrib.data.handler") self.handler_class = config.get("class", "ALPHA360") self.handler_parameters = config.get("args", dict()) self.handler_filter = config.get("filter", dict()) @@ -118,7 +118,7 @@ class ModelConfig(object): :param CONFIG_MANAGER: The estimator config manager """ self.model_class = config.get("class", "Model") - self.model_module_path = config.get("module_path", "qlib.contrib.model") + self.model_module_path = config.get("module_path", "qlib.model") self.save_dir = os.path.join(CONFIG_MANAGER.ex_config.tmp_run_dir, "model") self.save_path = config.get("save_path", os.path.join(self.save_dir, "model.bin")) self.parameters = config.get("args", dict()) diff --git a/qlib/contrib/model/__init__.py b/qlib/contrib/model/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/qlib/model/gbdt.py b/qlib/contrib/model/gbdt.py similarity index 98% rename from qlib/model/gbdt.py rename to qlib/contrib/model/gbdt.py index 61b902995..b0c52edcb 100644 --- a/qlib/model/gbdt.py +++ b/qlib/contrib/model/gbdt.py @@ -9,7 +9,7 @@ import numpy as np import lightgbm as lgb from sklearn.metrics import roc_auc_score, mean_squared_error -from .base import Model +from ...model.base import Model from ...utils import drop_nan_by_y_index diff --git a/qlib/model/pytorch_nn.py b/qlib/contrib/model/pytorch_nn.py similarity index 99% rename from qlib/model/pytorch_nn.py rename to qlib/contrib/model/pytorch_nn.py index 48b643bf8..b5bf91472 100644 --- a/qlib/model/pytorch_nn.py +++ b/qlib/contrib/model/pytorch_nn.py @@ -17,7 +17,7 @@ import torch import torch.nn as nn import torch.optim as optim -from .base import Model +from ...model.base import Model class DNNModelPytorch(Model): diff --git a/qlib/data/dataset/__init__.py b/qlib/data/dataset/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/qlib/data/dataset/handler.py b/qlib/data/dataset/handler.py index 3c30b01d8..5aa9e81a5 100644 --- a/qlib/data/dataset/handler.py +++ b/qlib/data/dataset/handler.py @@ -513,73 +513,3 @@ class ConfigQLibDataHandler(QLibDataHandler): if "labels" not in kwargs: kwargs["labels"] = ["Ref($vwap, -2)/Ref($vwap, -1) - 1"] super()._init_kwargs(**kwargs) - - -class ALPHA360(ConfigQLibDataHandler): - config_template = { - "price": {"windows": range(60)}, - "volume": {"windows": range(60)}, - } - - -class QLibDataHandlerV1(ConfigQLibDataHandler): - config_template = { - "kbar": {}, - "price": { - "windows": [0], - "feature": ["OPEN", "HIGH", "LOW", "VWAP"], - }, - "rolling": {}, - } - - def __init__(self, start_date, end_date, processors=None, **kwargs): - if processors is None: - processors = ["PanelProcessor"] # V1 default processor - super().__init__(start_date, end_date, processors, **kwargs) - - def setup_label(self): - """ - load the labels df - :return: df_labels - """ - TimeInspector.set_time_mark() - - df_labels = super().setup_label() - - ## calculate new labels - df_labels["LABEL1"] = df_labels["LABEL0"].groupby(level="datetime").apply(lambda x: (x - x.mean()) / x.std()) - - df_labels = df_labels.drop(["LABEL0"], axis=1) - - TimeInspector.log_cost_time("Finished loading labels.") - - return df_labels - - -class Alpha158(QLibDataHandlerV1): - config_template = { - "kbar": {}, - "price": { - "windows": [0], - "feature": ["OPEN", "HIGH", "LOW", "CLOSE"], - }, - "rolling": {}, - } - - def _init_kwargs(self, **kwargs): - kwargs["labels"] = ["Ref($close, -2)/Ref($close, -1) - 1"] - super(Alpha158, self)._init_kwargs(**kwargs) - - -# if __name__ == '__main__': -# import qlib -# -# qlib.init() -# -# handler = ALPHA80('2010-01-01', '2018-12-31') -# data = handler.get_split_data( -# pd.Timestamp('2010-01-01'), pd.Timestamp('2014-01-01'), -# pd.Timestamp('2015-01-01'), pd.Timestamp('2016-01-01'), -# pd.Timestamp('2017-01-01'), pd.Timestamp('2018-01-01')) -# print(data[0]) -# data[0].to_pickle('alpha80.pkl') diff --git a/qlib/data/dataset/processor.py b/qlib/data/dataset/processor.py new file mode 100644 index 000000000..b67170397 --- /dev/null +++ b/qlib/data/dataset/processor.py @@ -0,0 +1,249 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +import abc +import numpy as np +import pandas as pd + +from ...log import TimeInspector + +EPS = 1e-12 + + +class Processor(abc.ABC): + def __init__(self, feature_names, label_names, **kwargs): + self.feature_names = feature_names + self.label_names = label_names + + @abc.abstractmethod + def __call__(self, df_train, df_valid, df_test): + pass + + +class PanelProcessor(Processor): + """Panel Preprocessor""" + + STD_NORM = "Std" + MINMAX_NORM = "MinMax" + + def __init__(self, feature_names, label_names, **kwargs): + super().__init__(feature_names, label_names) + # Options. + self.dropna_label = kwargs.get("dropna_label", True) + self.dropna_feature = kwargs.get("dropna_feature", False) + self.normalize_method = kwargs.get("normalize_method", None) + self.replace_inf = kwargs.get("replace_inf_feature", False) + + def __call__(self, df_train, df_valid, df_test): + """ + Preprocess the data + :param df: the dataframe to process data. + """ + # Drop null labels. + if self.dropna_label: + df_train, df_valid, df_test = self._process_drop_null_label(df_train, df_valid, df_test) + + # Dropna if need. + if self.dropna_feature: + df_train, df_valid, df_test = self._process_drop_null_feature(df_train, df_valid, df_test) + + # replace the 'inf' with the mean the corresponding dimension + if self.replace_inf: + df_train, df_valid, df_test = self._process_replace_inf_feature(df_train, df_valid, df_test) + + # normalize data in given method. + if self.normalize_method is not None: + df_train, df_valid, df_test = self._process_normalize_feature(df_train, df_valid, df_test) + + return df_train, df_valid, df_test + + def _process_drop_null_label(self, df_train, df_valid, df_test): + """ + Drop null labels. + """ + TimeInspector.set_time_mark() + df_train = df_train.dropna(subset=self.label_names) + df_valid = df_valid.dropna(subset=self.label_names) + # The test data's label is Unkown. They can not be seen when preprocessing + TimeInspector.log_cost_time("Finished dropping null labels.") + + return df_train, df_valid, df_test + + def _process_drop_null_feature(self, df_train, df_valid, df_test): + """ + Drop data which contain null features if needed. + """ + # TODO - `Pandas.dropna` is a low performance method. + TimeInspector.set_time_mark() + df_train = df_train.dropna(subset=self.feature_names) + df_valid = df_valid.dropna(subset=self.feature_names) + df_test = df_test.dropna(subset=self.feature_names) + TimeInspector.log_cost_time("Finished dropping nan.") + + return df_train, df_valid, df_test + + def _process_replace_inf_feature(self, df_train, df_valid, df_test): + """ + replace the 'inf' in feature with the mean of this dimension. + """ + TimeInspector.set_time_mark() + + def replace_inf(data): + def process_inf(df): + for col in df.columns: + df[col] = df[col].replace([np.inf, -np.inf], df[col][~np.isinf(df[col])].mean()) + return df + + data = data.groupby("datetime").apply(process_inf) + data.sort_index(inplace=True) + return data + + df_train = replace_inf(df_train) + df_valid = replace_inf(df_valid) + df_test = replace_inf(df_test) + TimeInspector.log_cost_time("Finished replace inf.") + + return df_train, df_valid, df_test + + def _process_normalize_feature(self, df_train, df_valid, df_test): + """ + Normalize data if needed, we provide two method now: min-max normalization and standard normalization. + """ + TimeInspector.set_time_mark() + + if self.normalize_method == self.MINMAX_NORM: + min_train = np.nanmin(df_train[self.feature_names].values, axis=0) + max_train = np.nanmax(df_train[self.feature_names].values, axis=0) + ignore = min_train == max_train + + def normalize(x, min_train=min_train, max_train=max_train, ignore=ignore): + if (~ignore).all(): + return (x - min_train) / (max_train - min_train) + for i in range(ignore.size): + if not ignore[i]: + x[i] = (x[i] - min_train) / (max_train - min_train) + return x + + elif self.normalize_method == self.STD_NORM: + mean_train = np.nanmean(df_train[self.feature_names].values, axis=0) + std_train = np.nanstd(df_train[self.feature_names].values, axis=0) + ignore = std_train == 0 + + def normalize(x, mean_train=mean_train, std_train=std_train, ignore=ignore): + if (~ignore).all(): + return (x - mean_train) / std_train + for i in range(ignore.size): + if not ignore[i]: + x[i] = (x[i] - mean_train) / std_train + return x + + else: + raise ValueError("Normalize method {} is not allowed".format(self.normalize_method)) + + df_train.loc(axis=1)[self.feature_names] = normalize(df_train[self.feature_names].values) + df_valid.loc(axis=1)[self.feature_names] = normalize(df_valid[self.feature_names].values) + df_test.loc(axis=1)[self.feature_names] = normalize(df_test[self.feature_names].values) + + TimeInspector.log_cost_time("Finished normalizing data.") + + return df_train, df_valid, df_test + + +class ConfigSectionProcessor(Processor): + def __init__(self, feature_names, label_names, **kwargs): + super().__init__(feature_names, label_names) + # Options + self.fillna_feature = kwargs.get("fillna_feature", True) + self.fillna_label = kwargs.get("fillna_label", True) + self.clip_feature_outlier = kwargs.get("clip_feature_outlier", False) + self.shrink_feature_outlier = kwargs.get("shrink_feature_outlier", True) + self.clip_label_outlier = kwargs.get("clip_label_outlier", False) + + def __call__(self, *args): + return [self._transform(x) for x in args] + + def _transform(self, df): + def _label_norm(x): + x = x - x.mean() # copy + x /= x.std() + if self.clip_label_outlier: + x.clip(-3, 3, inplace=True) + if self.fillna_label: + x.fillna(0, inplace=True) + return x + + def _feature_norm(x): + x = x - x.median() # copy + x /= x.abs().median() * 1.4826 + if self.clip_feature_outlier: + x.clip(-3, 3, inplace=True) + if self.shrink_feature_outlier: + x.where(x <= 3, 3 + (x - 3).div(x.max() - 3) * 0.5, inplace=True) + x.where(x >= -3, -3 - (x + 3).div(x.min() + 3) * 0.5, inplace=True) + if self.fillna_feature: + x.fillna(0, inplace=True) + return x + + TimeInspector.set_time_mark() + + # Copy + df_new = df.copy() + + # Label + cols = df.columns[df.columns.str.contains("^LABEL")] + df_new[cols] = df[cols].groupby(level="datetime").apply(_label_norm) + + # Features + cols = df.columns[df.columns.str.contains("^KLEN|^KLOW|^KUP")] + df_new[cols] = df[cols].apply(lambda x: x ** 0.25).groupby(level="datetime").apply(_feature_norm) + + cols = df.columns[df.columns.str.contains("^KLOW2|^KUP2")] + df_new[cols] = df[cols].apply(lambda x: x ** 0.5).groupby(level="datetime").apply(_feature_norm) + + _cols = [ + "KMID", + "KSFT", + "OPEN", + "HIGH", + "LOW", + "CLOSE", + "VWAP", + "ROC", + "MA", + "BETA", + "RESI", + "QTLU", + "QTLD", + "RSV", + "SUMP", + "SUMN", + "SUMD", + "VSUMP", + "VSUMN", + "VSUMD", + ] + pat = "|".join(["^" + x for x in _cols]) + cols = df.columns[df.columns.str.contains(pat) & (~df.columns.isin(["HIGH0", "LOW0"]))] + df_new[cols] = df[cols].groupby(level="datetime").apply(_feature_norm) + + cols = df.columns[df.columns.str.contains("^STD|^VOLUME|^VMA|^VSTD")] + df_new[cols] = df[cols].apply(np.log).groupby(level="datetime").apply(_feature_norm) + + cols = df.columns[df.columns.str.contains("^RSQR")] + df_new[cols] = df[cols].fillna(0).groupby(level="datetime").apply(_feature_norm) + + cols = df.columns[df.columns.str.contains("^MAX|^HIGH0")] + df_new[cols] = df[cols].apply(lambda x: (x - 1) ** 0.5).groupby(level="datetime").apply(_feature_norm) + + cols = df.columns[df.columns.str.contains("^MIN|^LOW0")] + df_new[cols] = df[cols].apply(lambda x: (1 - x) ** 0.5).groupby(level="datetime").apply(_feature_norm) + + cols = df.columns[df.columns.str.contains("^CORR|^CORD")] + df_new[cols] = df[cols].apply(np.exp).groupby(level="datetime").apply(_feature_norm) + + cols = df.columns[df.columns.str.contains("^WVMA")] + df_new[cols] = df[cols].apply(np.log1p).groupby(level="datetime").apply(_feature_norm) + + TimeInspector.log_cost_time("Finished preprocessing data.") + + return df_new diff --git a/qlib/log.py b/qlib/log.py index bc87fc579..7db9ea92d 100644 --- a/qlib/log.py +++ b/qlib/log.py @@ -2,12 +2,12 @@ # Licensed under the MIT License. +import logging +import logging.handlers import os import re -import logging -from time import time -import logging.handlers from logging import config as logging_config +from time import time from .config import C diff --git a/tests/test_all_pipeline.py b/tests/test_all_pipeline.py index e3ede382b..ff80f8520 100644 --- a/tests/test_all_pipeline.py +++ b/tests/test_all_pipeline.py @@ -13,7 +13,7 @@ import qlib from qlib.config import REG_CN from qlib.utils import drop_nan_by_y_index from qlib.contrib.model.gbdt import LGBModel -from qlib.contrib.estimator.handler import Alpha158 +from qlib.contrib.data.handler import Alpha158 from qlib.contrib.strategy.strategy import TopkDropoutStrategy from qlib.contrib.evaluate import ( backtest as normal_backtest, From 10066ecf7976de22e5f03042335e95d20065084d Mon Sep 17 00:00:00 2001 From: Young Date: Sat, 17 Oct 2020 09:16:43 +0000 Subject: [PATCH 004/241] Draft version of refactoring handler --- docs/component/data.rst | 14 +- docs/component/estimator.rst | 6 +- examples/workflow_by_code.py | 131 +++++++ qlib/contrib/data/__init__.py | 0 qlib/contrib/data/handler.py | 37 +- qlib/contrib/estimator/processor.py | 249 ------------- qlib/contrib/estimator/trainer.py | 4 +- qlib/contrib/strategy/strategy.py | 1 + qlib/data/dataset/handler.py | 504 +++++++++++++++------------ qlib/data/dataset/processor.py | 289 +++++++++------ qlib/model/task.py | 142 ++++++++ qlib/{utils.py => utils/__init__.py} | 10 +- qlib/utils/objm.py | 130 +++++++ qlib/utils/serial.py | 22 ++ 14 files changed, 929 insertions(+), 610 deletions(-) create mode 100644 examples/workflow_by_code.py create mode 100644 qlib/contrib/data/__init__.py delete mode 100644 qlib/contrib/estimator/processor.py create mode 100644 qlib/model/task.py rename qlib/{utils.py => utils/__init__.py} (99%) create mode 100644 qlib/utils/objm.py create mode 100644 qlib/utils/serial.py diff --git a/docs/component/data.rst b/docs/component/data.rst index eb9673e92..6b813b39e 100644 --- a/docs/component/data.rst +++ b/docs/component/data.rst @@ -156,17 +156,17 @@ Data Handler Users can use ``Data Handler`` in an automatic workflow by ``Estimator``, refer to `Estimator: Workflow Management `_ for more details. -Also, ``Data Handler`` can be used as an independent module, by which users can easily preprocess data(standardization, remove NaN, etc.) and build datasets. It is a subclass of ``qlib.data.dataset.handler.BaseDataHandler``, which provides some interfaces as follows. +Also, ``Data Handler`` can be used as an independent module, by which users can easily preprocess data(standardization, remove NaN, etc.) and build datasets. It is a subclass of ``qlib.data.dataset.handler.DataHandlerLP``, which provides some interfaces as follows. Base Class & Interface ---------------------- -Qlib provides a base class `qlib.data.dataset.BaseDataHandler <../reference/api.html#qlib.data.dataset.handler.BaseDataHandler>`_, which provides the following interfaces: +Qlib provides a base class `qlib.data.dataset.DataHandlerLP <../reference/api.html#qlib.data.dataset.handler.DataHandlerLP>`_, which provides the following interfaces: -- `setup_feature` +- `load_feature` Implement the interface to load the data features. -- `setup_label` +- `load_label` Implement the interface to load the data labels and calculate the users' labels. - `setup_processed_data` @@ -174,11 +174,7 @@ Qlib provides a base class `qlib.data.dataset.BaseDataHandler <../reference/api. Qlib also provides two functions to help users init the data handler, users can override them for users' needs. -- `_init_kwargs` - Users can init the kwargs of the data handler in this function, some kwargs may be used when init the raw df. - Kwargs are the other attributes in data.args, like dropna_label, dropna_feature - -- `_init_raw_df` +- `_init_raw_data` Users can init the raw df, feature names, and label names of data handler in this function. If the index of feature df and label df are not the same, users need to override this method to merge them (e.g. inner, left, right merge). diff --git a/docs/component/estimator.rst b/docs/component/estimator.rst index e306c2f25..df59b75b9 100644 --- a/docs/component/estimator.rst +++ b/docs/component/estimator.rst @@ -284,7 +284,7 @@ To know more about ``Interday Model``, please refer to `Interday Model: Training Data Section ----------------- -``Data Handler`` can be used to load raw data, prepare features and label columns, preprocess data (standardization, remove NaN, etc.), split training, validation, and test sets. It is a subclass of `qlib.data.dataset.handler.BaseDataHandler`. +``Data Handler`` can be used to load raw data, prepare features and label columns, preprocess data (standardization, remove NaN, etc.), split training, validation, and test sets. It is a subclass of `qlib.data.dataset.handler.DataHandlerLP`. Users can use the specified data handler by config as follows. @@ -315,7 +315,7 @@ Users can use the specified data handler by config as follows. fend_time: 2018-12-11 - `class` - Data handler class, str type, which should be a subclass of `qlib.data.dataset.handler.BaseDataHandler`, and implements 5 important interfaces for loading features, loading raw data, preprocessing raw data, slicing train, validation, and test data. The default value is `ALPHA360`. If users want to write a data handler to retrieve the data in ``Qlib``, `QlibDataHandler` is suggested. + Data handler class, str type, which should be a subclass of `qlib.data.dataset.handler.DataHandlerLP`, and implements 5 important interfaces for loading features, loading raw data, preprocessing raw data, slicing train, validation, and test data. The default value is `ALPHA360`. If users want to write a data handler to retrieve the data in ``Qlib``, `QlibDataHandler` is suggested. - `module_path` The module path, str type, absolute url is also supported, indicates the path of the `class` implementation of the data processor class. The default value is `qlib.data.dataset.handler`. @@ -363,7 +363,7 @@ Users can use the specified data handler by config as follows. Custom Data Handler ~~~~~~~~~~~~~~~~~~~~~~ -Qlib support custom data handler, but it must be a subclass of the ``qlib.contrib.estimator.handler.BaseDataHandler``, the config for custom data handler may be as follows. +Qlib support custom data handler, but it must be a subclass of the ``qlib.data.dataset.handler.DataHandlerLP``, the config for custom data handler may be as follows. .. code-block:: YAML diff --git a/examples/workflow_by_code.py b/examples/workflow_by_code.py new file mode 100644 index 000000000..3179cbab3 --- /dev/null +++ b/examples/workflow_by_code.py @@ -0,0 +1,131 @@ +# 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 + + +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_cn(target_dir=provider_uri) + + qlib.init(provider_uri=provider_uri, region=REG_CN) + + MARKET = "CSI300" + BENCHMARK = "SH000300" + + + ################################### + # train model + ################################### + DATA_HANDLER_CONFIG = { + "start_date": "2008-01-01", + "end_date": "2020-08-01", + "fit_start_time":"2008-01-01", + "fit_end_time":"2014-12-31", + "market": MARKET, + } + + TRAINER_CONFIG = { + "train_start_date": "2008-01-01", + "train_end_date": "2014-12-31", + "validate_start_date": "2015-01-01", + "validate_end_date": "2016-12-31", + "test_start_date": "2017-01-01", + "test_end_date": "2020-08-01", + } + + # use default DataHandler + # custom DataHandler, refer to: TODO: DataHandler API url + handler = Alpha158(**DATA_HANDLER_CONFIG) + + data = handler.fetch(slice('2008-01-01', '2014-12-31'), key=handler.DK_I) + print(data) + + sys.exit(0) # I have tested the code above --------------------------------------------- + + x_train, y_train, x_validate, y_validate, x_test, y_test = Alpha158(**DATA_HANDLER_CONFIG).get_split_data( + **TRAINER_CONFIG + ) + + MODEL_CONFIG = { + "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, + } + # use default model + # custom Model, refer to: TODO: Model API url + model = LGBModel(**MODEL_CONFIG) + model.fit(x_train, y_train, x_validate, y_validate) + _pred = model.predict(x_test) + _pred = pd.DataFrame(_pred, index=x_test.index, columns=y_test.columns) + + # backtest requires pred_score + pred_score = pd.DataFrame(index=_pred.index) + pred_score["score"] = _pred.iloc(axis=1)[0] + + # save pred_score to file + pred_score_path = Path("~/tmp/qlib/pred_score.pkl").expanduser() + pred_score_path.parent.mkdir(exist_ok=True, parents=True) + pred_score.to_pickle(pred_score_path) + + ################################### + # backtest + ################################### + STRATEGY_CONFIG = { + "topk": 50, + "n_drop": 5, + } + BACKTEST_CONFIG = { + "verbose": False, + "limit_threshold": 0.095, + "account": 100000000, + "benchmark": BENCHMARK, + "deal_price": "close", + "open_cost": 0.0005, + "close_cost": 0.0015, + "min_cost": 5, + } + + # use default strategy + # custom Strategy, refer to: TODO: Strategy API url + strategy = TopkDropoutStrategy(**STRATEGY_CONFIG) + report_normal, positions_normal = normal_backtest(pred_score, strategy=strategy, **BACKTEST_CONFIG) + + ################################### + # analyze + # If need a more detailed analysis, refer to: examples/train_and_bakctest.ipynb + ################################### + 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 + print(analysis_df) diff --git a/qlib/contrib/data/__init__.py b/qlib/contrib/data/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/qlib/contrib/data/handler.py b/qlib/contrib/data/handler.py index 23dab22ee..6f53670dd 100644 --- a/qlib/contrib/data/handler.py +++ b/qlib/contrib/data/handler.py @@ -2,7 +2,9 @@ # Licensed under the MIT License. from ...data.dataset.handler import ConfigQLibDataHandler +from ...data.dataset.processor import Processor, MinMaxNorm, ZscoreNorm, get_cls_kwargs from ...log import TimeInspector +import copy class ALPHA360(ConfigQLibDataHandler): @@ -22,19 +24,36 @@ class QLibDataHandlerV1(ConfigQLibDataHandler): "rolling": {}, } - def __init__(self, start_date, end_date, processors=None, **kwargs): - if processors is None: - processors = ["PanelProcessor"] # V1 default processor - super().__init__(start_date, end_date, processors, **kwargs) + def __init__(self, start_date, end_date, infer_processors=[], learn_processors=["DropnaLabel"], fit_start_time=None, fit_end_time=None, **kwargs): + def check_transform_proc(proc_l): + new_l = [] + for p in proc_l: + if not isinstance(p, Processor): + klass, pkwargs = get_cls_kwargs(p) + if isinstance(klass, (MinMaxNorm, ZscoreNorm)): + assert(fit_start_time is not None and fit_end_time is not None) + pkwargs.update({ + "fit_start_time": fit_start_time, + "fit_end_time": fit_end_time, + }) + new_l.append({"class": klass.__name__, "kwargs": pkwargs}) + else: + new_l.append(p) + return new_l - def setup_label(self): + infer_processors = check_transform_proc(infer_processors) + learn_processors = check_transform_proc(learn_processors) + + super().__init__(start_date, end_date, infer_processors=infer_processors, learn_processors=learn_processors, **kwargs) + + def load_label(self): """ load the labels df :return: df_labels """ TimeInspector.set_time_mark() - df_labels = super().setup_label() + df_labels = super().load_label() ## calculate new labels df_labels["LABEL1"] = df_labels["LABEL0"].groupby(level="datetime").apply(lambda x: (x - x.mean()) / x.std()) @@ -56,8 +75,6 @@ class Alpha158(QLibDataHandlerV1): "rolling": {}, } - def _init_kwargs(self, **kwargs): + def __init__(self, *args, **kwargs): kwargs["labels"] = ["Ref($close, -2)/Ref($close, -1) - 1"] - super(Alpha158, self)._init_kwargs(**kwargs) - - + super().__init__(*args, **kwargs) diff --git a/qlib/contrib/estimator/processor.py b/qlib/contrib/estimator/processor.py deleted file mode 100644 index b67170397..000000000 --- a/qlib/contrib/estimator/processor.py +++ /dev/null @@ -1,249 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. - -import abc -import numpy as np -import pandas as pd - -from ...log import TimeInspector - -EPS = 1e-12 - - -class Processor(abc.ABC): - def __init__(self, feature_names, label_names, **kwargs): - self.feature_names = feature_names - self.label_names = label_names - - @abc.abstractmethod - def __call__(self, df_train, df_valid, df_test): - pass - - -class PanelProcessor(Processor): - """Panel Preprocessor""" - - STD_NORM = "Std" - MINMAX_NORM = "MinMax" - - def __init__(self, feature_names, label_names, **kwargs): - super().__init__(feature_names, label_names) - # Options. - self.dropna_label = kwargs.get("dropna_label", True) - self.dropna_feature = kwargs.get("dropna_feature", False) - self.normalize_method = kwargs.get("normalize_method", None) - self.replace_inf = kwargs.get("replace_inf_feature", False) - - def __call__(self, df_train, df_valid, df_test): - """ - Preprocess the data - :param df: the dataframe to process data. - """ - # Drop null labels. - if self.dropna_label: - df_train, df_valid, df_test = self._process_drop_null_label(df_train, df_valid, df_test) - - # Dropna if need. - if self.dropna_feature: - df_train, df_valid, df_test = self._process_drop_null_feature(df_train, df_valid, df_test) - - # replace the 'inf' with the mean the corresponding dimension - if self.replace_inf: - df_train, df_valid, df_test = self._process_replace_inf_feature(df_train, df_valid, df_test) - - # normalize data in given method. - if self.normalize_method is not None: - df_train, df_valid, df_test = self._process_normalize_feature(df_train, df_valid, df_test) - - return df_train, df_valid, df_test - - def _process_drop_null_label(self, df_train, df_valid, df_test): - """ - Drop null labels. - """ - TimeInspector.set_time_mark() - df_train = df_train.dropna(subset=self.label_names) - df_valid = df_valid.dropna(subset=self.label_names) - # The test data's label is Unkown. They can not be seen when preprocessing - TimeInspector.log_cost_time("Finished dropping null labels.") - - return df_train, df_valid, df_test - - def _process_drop_null_feature(self, df_train, df_valid, df_test): - """ - Drop data which contain null features if needed. - """ - # TODO - `Pandas.dropna` is a low performance method. - TimeInspector.set_time_mark() - df_train = df_train.dropna(subset=self.feature_names) - df_valid = df_valid.dropna(subset=self.feature_names) - df_test = df_test.dropna(subset=self.feature_names) - TimeInspector.log_cost_time("Finished dropping nan.") - - return df_train, df_valid, df_test - - def _process_replace_inf_feature(self, df_train, df_valid, df_test): - """ - replace the 'inf' in feature with the mean of this dimension. - """ - TimeInspector.set_time_mark() - - def replace_inf(data): - def process_inf(df): - for col in df.columns: - df[col] = df[col].replace([np.inf, -np.inf], df[col][~np.isinf(df[col])].mean()) - return df - - data = data.groupby("datetime").apply(process_inf) - data.sort_index(inplace=True) - return data - - df_train = replace_inf(df_train) - df_valid = replace_inf(df_valid) - df_test = replace_inf(df_test) - TimeInspector.log_cost_time("Finished replace inf.") - - return df_train, df_valid, df_test - - def _process_normalize_feature(self, df_train, df_valid, df_test): - """ - Normalize data if needed, we provide two method now: min-max normalization and standard normalization. - """ - TimeInspector.set_time_mark() - - if self.normalize_method == self.MINMAX_NORM: - min_train = np.nanmin(df_train[self.feature_names].values, axis=0) - max_train = np.nanmax(df_train[self.feature_names].values, axis=0) - ignore = min_train == max_train - - def normalize(x, min_train=min_train, max_train=max_train, ignore=ignore): - if (~ignore).all(): - return (x - min_train) / (max_train - min_train) - for i in range(ignore.size): - if not ignore[i]: - x[i] = (x[i] - min_train) / (max_train - min_train) - return x - - elif self.normalize_method == self.STD_NORM: - mean_train = np.nanmean(df_train[self.feature_names].values, axis=0) - std_train = np.nanstd(df_train[self.feature_names].values, axis=0) - ignore = std_train == 0 - - def normalize(x, mean_train=mean_train, std_train=std_train, ignore=ignore): - if (~ignore).all(): - return (x - mean_train) / std_train - for i in range(ignore.size): - if not ignore[i]: - x[i] = (x[i] - mean_train) / std_train - return x - - else: - raise ValueError("Normalize method {} is not allowed".format(self.normalize_method)) - - df_train.loc(axis=1)[self.feature_names] = normalize(df_train[self.feature_names].values) - df_valid.loc(axis=1)[self.feature_names] = normalize(df_valid[self.feature_names].values) - df_test.loc(axis=1)[self.feature_names] = normalize(df_test[self.feature_names].values) - - TimeInspector.log_cost_time("Finished normalizing data.") - - return df_train, df_valid, df_test - - -class ConfigSectionProcessor(Processor): - def __init__(self, feature_names, label_names, **kwargs): - super().__init__(feature_names, label_names) - # Options - self.fillna_feature = kwargs.get("fillna_feature", True) - self.fillna_label = kwargs.get("fillna_label", True) - self.clip_feature_outlier = kwargs.get("clip_feature_outlier", False) - self.shrink_feature_outlier = kwargs.get("shrink_feature_outlier", True) - self.clip_label_outlier = kwargs.get("clip_label_outlier", False) - - def __call__(self, *args): - return [self._transform(x) for x in args] - - def _transform(self, df): - def _label_norm(x): - x = x - x.mean() # copy - x /= x.std() - if self.clip_label_outlier: - x.clip(-3, 3, inplace=True) - if self.fillna_label: - x.fillna(0, inplace=True) - return x - - def _feature_norm(x): - x = x - x.median() # copy - x /= x.abs().median() * 1.4826 - if self.clip_feature_outlier: - x.clip(-3, 3, inplace=True) - if self.shrink_feature_outlier: - x.where(x <= 3, 3 + (x - 3).div(x.max() - 3) * 0.5, inplace=True) - x.where(x >= -3, -3 - (x + 3).div(x.min() + 3) * 0.5, inplace=True) - if self.fillna_feature: - x.fillna(0, inplace=True) - return x - - TimeInspector.set_time_mark() - - # Copy - df_new = df.copy() - - # Label - cols = df.columns[df.columns.str.contains("^LABEL")] - df_new[cols] = df[cols].groupby(level="datetime").apply(_label_norm) - - # Features - cols = df.columns[df.columns.str.contains("^KLEN|^KLOW|^KUP")] - df_new[cols] = df[cols].apply(lambda x: x ** 0.25).groupby(level="datetime").apply(_feature_norm) - - cols = df.columns[df.columns.str.contains("^KLOW2|^KUP2")] - df_new[cols] = df[cols].apply(lambda x: x ** 0.5).groupby(level="datetime").apply(_feature_norm) - - _cols = [ - "KMID", - "KSFT", - "OPEN", - "HIGH", - "LOW", - "CLOSE", - "VWAP", - "ROC", - "MA", - "BETA", - "RESI", - "QTLU", - "QTLD", - "RSV", - "SUMP", - "SUMN", - "SUMD", - "VSUMP", - "VSUMN", - "VSUMD", - ] - pat = "|".join(["^" + x for x in _cols]) - cols = df.columns[df.columns.str.contains(pat) & (~df.columns.isin(["HIGH0", "LOW0"]))] - df_new[cols] = df[cols].groupby(level="datetime").apply(_feature_norm) - - cols = df.columns[df.columns.str.contains("^STD|^VOLUME|^VMA|^VSTD")] - df_new[cols] = df[cols].apply(np.log).groupby(level="datetime").apply(_feature_norm) - - cols = df.columns[df.columns.str.contains("^RSQR")] - df_new[cols] = df[cols].fillna(0).groupby(level="datetime").apply(_feature_norm) - - cols = df.columns[df.columns.str.contains("^MAX|^HIGH0")] - df_new[cols] = df[cols].apply(lambda x: (x - 1) ** 0.5).groupby(level="datetime").apply(_feature_norm) - - cols = df.columns[df.columns.str.contains("^MIN|^LOW0")] - df_new[cols] = df[cols].apply(lambda x: (1 - x) ** 0.5).groupby(level="datetime").apply(_feature_norm) - - cols = df.columns[df.columns.str.contains("^CORR|^CORD")] - df_new[cols] = df[cols].apply(np.exp).groupby(level="datetime").apply(_feature_norm) - - cols = df.columns[df.columns.str.contains("^WVMA")] - df_new[cols] = df[cols].apply(np.log1p).groupby(level="datetime").apply(_feature_norm) - - TimeInspector.log_cost_time("Finished preprocessing data.") - - return df_new diff --git a/qlib/contrib/estimator/trainer.py b/qlib/contrib/estimator/trainer.py index 6cb57f702..84f387d67 100644 --- a/qlib/contrib/estimator/trainer.py +++ b/qlib/contrib/estimator/trainer.py @@ -10,14 +10,14 @@ import numpy as np from scipy.stats import pearsonr from ...log import get_module_logger, TimeInspector -from .handler import BaseDataHandler +from ...data.dataset.handler import DataHandlerLP from .launcher import CONFIG_MANAGER from .fetcher import create_fetcher_with_config from ...utils import drop_nan_by_y_index, transform_end_date class BaseTrainer(object): - def __init__(self, model_class, model_save_path, model_args, data_handler: BaseDataHandler, sacred_ex, **kwargs): + def __init__(self, model_class, model_save_path, model_args, data_handler: DataHandlerLP, sacred_ex, **kwargs): # 1. Model. self.model_class = model_class self.model_save_path = model_save_path diff --git a/qlib/contrib/strategy/strategy.py b/qlib/contrib/strategy/strategy.py index a43aa61cf..3030e428a 100644 --- a/qlib/contrib/strategy/strategy.py +++ b/qlib/contrib/strategy/strategy.py @@ -11,6 +11,7 @@ from ...utils import get_pre_trading_date from .order_generator import OrderGenWInteract +# TODO: The base strategies will be moved out of contrib to core code class BaseStrategy: def __init__(self): pass diff --git a/qlib/data/dataset/handler.py b/qlib/data/dataset/handler.py index 5aa9e81a5..e523fbfef 100644 --- a/qlib/data/dataset/handler.py +++ b/qlib/data/dataset/handler.py @@ -5,270 +5,342 @@ import abc import bisect import logging +from typing import Union import pandas as pd import numpy as np from ...log import get_module_logger, TimeInspector from ...data import D +from ...config import C from ...utils import parse_config, transform_end_date +from ...utils.serial import Serializable +from pathlib import Path from . import processor as processor_module -class BaseDataHandler(abc.ABC): - def __init__(self, processors=[], **kwargs): - """ - :param start_date: - :param end_date: - :param kwargs: - """ +# TODO: A more general handler interface which does not relies on internal pd.DataFrame is needed. +class DataHandler(Serializable): + ''' + The steps to using a handler + 1. initialized data handler (call by `init`). + 2. use the data + + + The data handler try to maintain a handler with 2 level. + `datetime` & `instruments`. + + Any order of the index level can be suported(The order will implied in the data). + The order <`datetime`, `instruments`> will be used when the dataframe index name is missed. + + Example of the data: + + $close $volume Ref($close, 1) Mean($close, 3) $high-$low + datetime instrument + 2010-01-04 SH600000 81.807068 17145150.0 83.737389 83.016739 2.741058 + SH600004 13.313329 11800983.0 13.313329 13.317701 0.183632 + SH600005 37.796539 12231662.0 38.258602 37.919757 0.970325 + SH600006 22.672380 7095624.0 22.508326 22.573947 0.557785 + + ''' + def __init__(self, init_data=True): # Set logger self.logger = get_module_logger("DataHandler") - # init data using kwargs - self._init_kwargs(**kwargs) - # Setup data. - self.raw_df, self.feature_names, self.label_names = self._init_raw_df() + self._data = {} + if init_data: + self.init() + super().__init__() - # Setup preprocessor - self.processors = [] - for klass in processors: - if isinstance(klass, str): - try: - klass = getattr(processor_module, klass) - except: - raise ValueError("unknown Processor %s" % klass) - self.processors.append(klass(self.feature_names, self.label_names, **kwargs)) - - def _init_kwargs(self, **kwargs): + def init(self, force_reload: bool=True): """ - init the kwargs of DataHandler + initialize the data. + In case of running intialization for multiple time, it will do nothing for the second time. + + Parameters + ---------- + force_reload : bool + force to reload the data even if the data have been initialized """ pass + # if force_reload or hasattr(self, '_initialized', False): - def _init_raw_df(self): + def get_level_index(self, df: pd.DataFrame, level=Union[str, int]) -> int: + """ + + get the level index of `df` given `level` + + Parameters + ---------- + df : pd.DataFrame + data + level : Union[str, int] + index level + + Returns + ------- + int: + The level index in the multiple index + """ + if isinstance(level, str): + try: + return df.index.names.index(level) + except (AttributeError, ValueError): + # NOTE: If level index is not given in the data, the default level index will be ('datetime', 'instrument') + return ('datetime', 'instrument').index(level) + elif isinstance(level, int): + return level + else: + raise NotImplementedError(f"This type of input is not supported") + + def _fetch_df(self, df: pd.DataFrame, selector: Union[pd.Timestamp, slice, str, list], level: Union[str, int]): + """ + fetch data from `data` with `selector` and `level` + + Parameters + ---------- + df : pd.DataFrame + the data frame to be selected + selector : Union[pd.Timestamp, slice, str, list] + selector + level : Union[pd.Timestamp, slice, str] + the level to use the selector + """ + # Try to get the right index + idx_slc = (selector, slice(None, None)) + if self.get_level_index(df, level) == 1: + idx_slc = idx_slc[1], idx_slc[0] + return df.loc(axis=0)[idx_slc] + + def fetch(self, selector: Union[pd.Timestamp, slice, str], level='datetime', key=None) -> Union[pd.DataFrame, dict]: + if key is None: + res = {} + for k, df in self._data.items(): + res[k] = self._fetch_df(df, selector, level) + else: + res = self._fetch_df(self._data[key], selector, level) + return res + + +class DataHandlerLP(DataHandler): + ''' + DataHandler with **(L)earnable (P)rocessor** + ''' + # data key + DK_R = 'raw' + DK_I = 'infer' + DK_L = 'learn' + + # process type + PTYPE_I = 'independent' + # - _proc_infer_df will processed by infer_processors + # - _proc_learn_df will be processed by learn_processors + PTYPE_A = 'append' + # - _proc_infer_df will processed by infer_processors + # - _proc_learn_df will be processed by infer_processors + learn_processors + # - (e.g. _proc_infer_df processed by learn_processors ) + + def __init__(self, infer_processors=[], learn_processors=[], process_type=PTYPE_A, **kwargs): + """ + + Parameters + ---------- + infer_processors : list + list of of processors to generate data for inference + example of : + 1) classname & kwargs: + { + "class": "MinMaxNorm", + "kwargs": { + "fit_start_time": "20080101", + "fit_end_time": "20121231" + } + } + 2) Only classname: + "DropnaFeature" + 3) object instance of Processor + + learn_processors : list + similar to infer_processors, but for generating data for learning models + + process_type: str + PTYPE_I = 'independent' + - _proc_infer_df will processed by infer_processors + - _proc_learn_df will be processed by learn_processors + PTYPE_A = 'append' + - _proc_infer_df will processed by infer_processors + - _proc_learn_df will be processed by infer_processors + learn_processors + - (e.g. _proc_infer_df processed by learn_processors ) + """ + + # Setup preprocessor + self.infer_processors = [] # for lint + self.learn_processors = [] # for lint + for pname in 'infer_processors', 'learn_processors': + for proc in locals()[pname]: + getattr(self, pname).append(processor_module.init_proc_obj(proc)) + + self.process_type = process_type + super().__init__(**kwargs) + + def get_all_processors(self): + return self.infer_processors + self.learn_processors + + def _init_raw_data(self): + """ + initialize the raw data + the raw data will be saved in to `self._data['raw']` + """ + raise NotImplementedError(f"Please implement the `_init_raw_data` method") + + def fit(self): + for proc in self.get_all_processors(): + proc.fit(self) + + def fit_process_data(self): + """ + fit and process data + + The input of the `fit` will be the output of the previous processor + """ + self.process_data(with_fit=True) + + + def process_data(self, with_fit: bool=False): + """ + process_data data. Fun `processor.fit` if necessary + + Parameters + ---------- + with_fit : bool + The input of the `fit` will be the output of the previous processor + """ + # data for inference + _infer_df = self._data[DataHandlerLP.DK_R] + for proc in self.infer_processors: + if not proc.is_for_infer(): + raise TypeError("Only processors usable for inference can be used in `infer_processors` ") + if with_fit: + proc.fit(self, _infer_df) + _infer_df = proc(_infer_df) + + # data for learning + if self.process_type == DataHandlerLP.PTYPE_I: + _learn_df = self._data[DataHandlerLP.DK_R] + elif self.process_type == DataHandlerLP.PTYPE_A: + # based on `infer_df` and append the processor + _learn_df = _infer_df + else: + raise NotImplementedError(f"This type of input is not supported") + + for proc in self.learn_processors: + if with_fit: + proc.fit(self, _learn_df) + _learn_df = proc(_learn_df) + + self._data.update({ + DataHandlerLP.DK_I: _infer_df, + DataHandlerLP.DK_L: _learn_df, + }) + + # init type + IT_FIT_SEQ = 'fit_seq' # the input of `fit` will be the output of the previous processor + IT_FIT_IND = 'fit_ind' # the input of `fit` will be the original df + IT_LS = 'load_state' # The state of the object has been load by pickle + + def init(self, init_type: str=IT_FIT_SEQ, path: Path=None): + """ + Initialize the data of Qlib + + Parameters + ---------- + init_type : str + 'fit' or 'load_state' + path : path + if `init_type` == 'load_state': `path` will be used to load_state + """ + self._init_raw_data() + + if init_type == DataHandlerLP.IT_FIT_IND: + self.fit() + self.process_data() + elif init_type == DataHandlerLP.IT_LS: + self.process_data() + elif init_type == DataHandlerLP.IT_FIT_SEQ: + self.fit_process_data() + else: + raise NotImplementedError(f"This type of input is not supported") + + # TODO: Be able to cache handler data. Save the memory for data processing + + +class DataHandlerLPWL(DataHandlerLP): + ''' + DataHandler with (L)earnable (P)rocessor with (L)abel + ''' + + def _init_raw_data(self): """ init raw_df, feature_names, label_names of DataHandler if the index of df_feature and df_label are not same, user need to overload this method to merge (e.g. inner, left, right merge). - """ - df_features = self.setup_feature() + df_features = self.load_feature() feature_names = df_features.columns - df_labels = self.setup_label() + df_labels = self.load_label() label_names = df_labels.columns raw_df = df_features.merge(df_labels, left_index=True, right_index=True, how="left") + self.feature_names = feature_names + self.label_names = label_names + self._data['raw'] = raw_df - return raw_df, feature_names, label_names - - def reset_label(self, df_labels): - for col in self.label_names: - del self.raw_df[col] - self.label_names = df_labels.columns - self.raw_df = self.raw_df.merge(df_labels, left_index=True, right_index=True, how="left") - - def split_rolling_periods( - self, - train_start_date, - train_end_date, - validate_start_date, - validate_end_date, - test_start_date, - test_end_date, - rolling_period, - calendar_freq="day", - ): - """ - Calculating the Rolling split periods, the period rolling on market calendar. - :param train_start_date: - :param train_end_date: - :param validate_start_date: - :param validate_end_date: - :param test_start_date: - :param test_end_date: - :param rolling_period: The market period of rolling - :param calendar_freq: The frequence of the market calendar - :yield: Rolling split periods - """ - - def get_start_index(calendar, start_date): - start_index = bisect.bisect_left(calendar, start_date) - return start_index - - def get_end_index(calendar, end_date): - end_index = bisect.bisect_right(calendar, end_date) - return end_index - 1 - - calendar = self.raw_df.index.get_level_values("datetime").unique() - - train_start_index = get_start_index(calendar, pd.Timestamp(train_start_date)) - train_end_index = get_end_index(calendar, pd.Timestamp(train_end_date)) - valid_start_index = get_start_index(calendar, pd.Timestamp(validate_start_date)) - valid_end_index = get_end_index(calendar, pd.Timestamp(validate_end_date)) - test_start_index = get_start_index(calendar, pd.Timestamp(test_start_date)) - test_end_index = test_start_index + rolling_period - 1 - - need_stop_split = False - - bound_test_end_index = get_end_index(calendar, pd.Timestamp(test_end_date)) - - while not need_stop_split: - - if test_end_index > bound_test_end_index: - test_end_index = bound_test_end_index - need_stop_split = True - - yield ( - calendar[train_start_index], - calendar[train_end_index], - calendar[valid_start_index], - calendar[valid_end_index], - calendar[test_start_index], - calendar[test_end_index], - ) - - train_start_index += rolling_period - train_end_index += rolling_period - valid_start_index += rolling_period - valid_end_index += rolling_period - test_start_index += rolling_period - test_end_index += rolling_period - - def get_rolling_data( - self, - train_start_date, - train_end_date, - validate_start_date, - validate_end_date, - test_start_date, - test_end_date, - rolling_period, - calendar_freq="day", - ): - # Set generator. - for period in self.split_rolling_periods( - train_start_date, - train_end_date, - validate_start_date, - validate_end_date, - test_start_date, - test_end_date, - rolling_period, - calendar_freq, - ): - ( - x_train, - y_train, - x_validate, - y_validate, - x_test, - y_test, - ) = self.get_split_data(*period) - yield x_train, y_train, x_validate, y_validate, x_test, y_test - - def get_split_data( - self, - train_start_date, - train_end_date, - validate_start_date, - validate_end_date, - test_start_date, - test_end_date, - ): - """ - all return types are DataFrame - """ - ## TODO: loc can be slow, expecially when we put it at the second level index. - if self.raw_df.index.names[0] == "instrument": - df_train = self.raw_df.loc(axis=0)[:, train_start_date:train_end_date] - df_validate = self.raw_df.loc(axis=0)[:, validate_start_date:validate_end_date] - df_test = self.raw_df.loc(axis=0)[:, test_start_date:test_end_date] - else: - df_train = self.raw_df.loc[train_start_date:train_end_date] - df_validate = self.raw_df.loc[validate_start_date:validate_end_date] - df_test = self.raw_df.loc[test_start_date:test_end_date] - - TimeInspector.set_time_mark() - df_train, df_validate, df_test = self.setup_process_data(df_train, df_validate, df_test) - TimeInspector.log_cost_time("Finished setup processed data.") - - x_train = df_train[self.feature_names] - y_train = df_train[self.label_names] - - x_validate = df_validate[self.feature_names] - y_validate = df_validate[self.label_names] - - x_test = df_test[self.feature_names] - y_test = df_test[self.label_names] - - return x_train, y_train, x_validate, y_validate, x_test, y_test - - def setup_process_data(self, df_train, df_valid, df_test): - """ - process the train, valid and test data - :return: the processed train, valid and test data. - """ - for processor in self.processors: - df_train, df_valid, df_test = processor(df_train, df_valid, df_test) - return df_train, df_valid, df_test - - def get_origin_test_label_with_date(self, test_start_date, test_end_date, freq="day"): - """Get origin test label - - :param test_start_date: test start date - :param test_end_date: test end date - :param freq: freq - :return: pd.DataFrame - """ - test_end_date = transform_end_date(test_end_date, freq=freq) - return self.raw_df.loc[(slice(None), slice(test_start_date, test_end_date)), self.label_names] - - @abc.abstractmethod - def setup_feature(self): + def load_feature(self): """ Implement this method to load raw feature. the format of the feature is below return: df_features """ - pass + raise NotImplementedError(f"Please implement `load_feature`") - @abc.abstractmethod - def setup_label(self): + def load_label(self): """ Implement this method to load and calculate label. the format of the label is below return: df_label """ - pass + raise NotImplementedError(f"Please implement `load_label`") + + def get_feature_names(self): + return self.feature_names + + def get_label_names(self): + return self.label_names -class QLibDataHandler(BaseDataHandler): +class QLibDataHandler(DataHandlerLPWL): def __init__(self, start_date, end_date, *args, **kwargs): # Dates. self.start_date = start_date self.end_date = end_date - super().__init__(*args, **kwargs) - - def _init_kwargs(self, **kwargs): # Instruments - instruments = kwargs.get("instruments", None) + instruments = kwargs.pop("instruments", None) if instruments is None: - market = kwargs.get("market", "csi500").lower() - data_filter_list = kwargs.get("data_filter_list", list()) + market = kwargs.pop("market", "csi500").lower() + data_filter_list = kwargs.pop("data_filter_list", list()) self.instruments = D.instruments(market, filter_pipe=data_filter_list) else: self.instruments = instruments # Config of features and labels - self._fields = kwargs.get("fields", []) - self._names = kwargs.get("names", []) - self._labels = kwargs.get("labels", []) - self._label_names = kwargs.get("label_names", []) + self._fields = kwargs.pop("fields", []) + self._names = kwargs.pop("names", []) + self._labels = kwargs.pop("labels", []) + self._label_names = kwargs.pop("label_names", []) # Check arguments assert len(self._fields) > 0, "features list is empty" @@ -278,7 +350,9 @@ class QLibDataHandler(BaseDataHandler): # If test_end_date is -1 or greater than the last date, the last date is used self.end_date = transform_end_date(self.end_date) - def setup_feature(self): + super().__init__(*args, **kwargs) + + def load_feature(self): """ Load the raw data. return: df_features @@ -297,7 +371,7 @@ class QLibDataHandler(BaseDataHandler): return df_features - def setup_label(self): + def load_label(self): """ Build up labels in df through users' method :return: df_labels @@ -498,12 +572,7 @@ def parse_config_to_fields(config): class ConfigQLibDataHandler(QLibDataHandler): config_template = {} # template - def __init__(self, start_date, end_date, processors=None, **kwargs): - if processors is None: - processors = ["ConfigSectionProcessor"] # default processor - super().__init__(start_date, end_date, processors, **kwargs) - - def _init_kwargs(self, **kwargs): + def __init__(self, start_date, end_date, infer_processors=["ConfigSectionProcessor"], learn_processors=[], **kwargs): config = self.config_template.copy() if "config_update" in kwargs: config.update(kwargs["config_update"]) @@ -512,4 +581,5 @@ class ConfigQLibDataHandler(QLibDataHandler): kwargs["names"] = names if "labels" not in kwargs: kwargs["labels"] = ["Ref($vwap, -2)/Ref($vwap, -1) - 1"] - super()._init_kwargs(**kwargs) + + super().__init__(start_date, end_date, infer_processors=infer_processors, learn_processors=learn_processors, **kwargs) diff --git a/qlib/data/dataset/processor.py b/qlib/data/dataset/processor.py index b67170397..d9de408bc 100644 --- a/qlib/data/dataset/processor.py +++ b/qlib/data/dataset/processor.py @@ -4,154 +4,209 @@ import abc import numpy as np import pandas as pd +import copy from ...log import TimeInspector +from ...utils.serial import Serializable EPS = 1e-12 -class Processor(abc.ABC): - def __init__(self, feature_names, label_names, **kwargs): - self.feature_names = feature_names - self.label_names = label_names +class Processor(Serializable): + + def fit(self, handler, df: pd.DataFrame=None): + """ + learn data processing parameters + + Parameters + ---------- + handler : DataHandlerLP + The data handler to processing data + df : pd.DataFrame + When we fit and process data with processor one by one. The fit function reiles on the output of previous + processor, i.e. `df`. + + """ + pass @abc.abstractmethod - def __call__(self, df_train, df_valid, df_test): + def __call__(self, df: pd.DataFrame): + """ + process the data + + NOTE: The processor should not change the content of `df` + + Parameters + ---------- + df : pd.DataFrame + The raw_df of handler or result from previous processor + """ pass -class PanelProcessor(Processor): - """Panel Preprocessor""" +def get_cls_kwargs(processor: [dict, str]) -> (type, dict): + """ + extract class and kwargs from processor info - STD_NORM = "Std" - MINMAX_NORM = "MinMax" + Parameters + ---------- + processor : [dict, str] + similar to processor - def __init__(self, feature_names, label_names, **kwargs): - super().__init__(feature_names, label_names) - # Options. - self.dropna_label = kwargs.get("dropna_label", True) - self.dropna_feature = kwargs.get("dropna_feature", False) - self.normalize_method = kwargs.get("normalize_method", None) - self.replace_inf = kwargs.get("replace_inf_feature", False) + Returns + ------- + (type, dict): + the class object and it's arguments. + """ + if isinstance(processor, dict): + # raise AttributeError + klass = globals()[processor['class']] + kwargs = processor['kwargs'] + elif isinstance(processor, str): + klass = globals()[processor] + kwargs = {} + else: + raise NotImplementedError(f"This type of input is not supported") + return klass, kwargs - def __call__(self, df_train, df_valid, df_test): + +# Place the function here to be able to reference the Processor +def init_proc_obj(processor: [dict, str, Processor]) -> Processor: + """ + Initialize Processor Object + + Parameters + ---------- + processor : [dict, str, Processor] + The info to initialize processor + + Returns + ------- + Processor: + initialized Processor + """ + if not isinstance(processor, Processor): + klass, pkwargs = get_cls_kwargs(processor) + processor = klass(**pkwargs) + return processor + + +class InferProcessor(Processor): + '''This processor is usable for inference''' + def is_for_infer(self) -> bool: """ - Preprocess the data - :param df: the dataframe to process data. + Is this processor usable for inference + + Returns + ------- + bool: + if it is usable for infenrece """ - # Drop null labels. - if self.dropna_label: - df_train, df_valid, df_test = self._process_drop_null_label(df_train, df_valid, df_test) + return True - # Dropna if need. - if self.dropna_feature: - df_train, df_valid, df_test = self._process_drop_null_feature(df_train, df_valid, df_test) - # replace the 'inf' with the mean the corresponding dimension - if self.replace_inf: - df_train, df_valid, df_test = self._process_replace_inf_feature(df_train, df_valid, df_test) - - # normalize data in given method. - if self.normalize_method is not None: - df_train, df_valid, df_test = self._process_normalize_feature(df_train, df_valid, df_test) - - return df_train, df_valid, df_test - - def _process_drop_null_label(self, df_train, df_valid, df_test): +class NInferProcessor(Processor): + '''This processor is not usable for inference''' + def is_for_infer(self) -> bool: """ - Drop null labels. - """ - TimeInspector.set_time_mark() - df_train = df_train.dropna(subset=self.label_names) - df_valid = df_valid.dropna(subset=self.label_names) - # The test data's label is Unkown. They can not be seen when preprocessing - TimeInspector.log_cost_time("Finished dropping null labels.") + Is this processor usable for inference - return df_train, df_valid, df_test - - def _process_drop_null_feature(self, df_train, df_valid, df_test): + Returns + ------- + bool: + if it is usable for infenrece """ - Drop data which contain null features if needed. - """ - # TODO - `Pandas.dropna` is a low performance method. - TimeInspector.set_time_mark() - df_train = df_train.dropna(subset=self.feature_names) - df_valid = df_valid.dropna(subset=self.feature_names) - df_test = df_test.dropna(subset=self.feature_names) - TimeInspector.log_cost_time("Finished dropping nan.") + return False - return df_train, df_valid, df_test - def _process_replace_inf_feature(self, df_train, df_valid, df_test): - """ - replace the 'inf' in feature with the mean of this dimension. - """ - TimeInspector.set_time_mark() +class DropnaFeature(InferProcessor): + def fit(self, handler, df=None): + self.feature_names = copy.deepcopy(handler.get_feature_names()) + def __call__(self, df): + return df.dropna(subset=self.feature_names) + + +class DropnaLabel(InferProcessor): + def fit(self, handler, df=None): + self.label_names = copy.deepcopy(handler.get_label_names()) + + def __call__(self, df): + return df.dropna(subset=self.label_names) + + +class ProcessInf(InferProcessor): + '''Process infinity ''' + def __call__(self, df): def replace_inf(data): def process_inf(df): for col in df.columns: + # FIXME: Such behavior is very weird df[col] = df[col].replace([np.inf, -np.inf], df[col][~np.isinf(df[col])].mean()) return df data = data.groupby("datetime").apply(process_inf) data.sort_index(inplace=True) return data - - df_train = replace_inf(df_train) - df_valid = replace_inf(df_valid) - df_test = replace_inf(df_test) - TimeInspector.log_cost_time("Finished replace inf.") - - return df_train, df_valid, df_test - - def _process_normalize_feature(self, df_train, df_valid, df_test): - """ - Normalize data if needed, we provide two method now: min-max normalization and standard normalization. - """ - TimeInspector.set_time_mark() - - if self.normalize_method == self.MINMAX_NORM: - min_train = np.nanmin(df_train[self.feature_names].values, axis=0) - max_train = np.nanmax(df_train[self.feature_names].values, axis=0) - ignore = min_train == max_train - - def normalize(x, min_train=min_train, max_train=max_train, ignore=ignore): - if (~ignore).all(): - return (x - min_train) / (max_train - min_train) - for i in range(ignore.size): - if not ignore[i]: - x[i] = (x[i] - min_train) / (max_train - min_train) - return x - - elif self.normalize_method == self.STD_NORM: - mean_train = np.nanmean(df_train[self.feature_names].values, axis=0) - std_train = np.nanstd(df_train[self.feature_names].values, axis=0) - ignore = std_train == 0 - - def normalize(x, mean_train=mean_train, std_train=std_train, ignore=ignore): - if (~ignore).all(): - return (x - mean_train) / std_train - for i in range(ignore.size): - if not ignore[i]: - x[i] = (x[i] - mean_train) / std_train - return x - - else: - raise ValueError("Normalize method {} is not allowed".format(self.normalize_method)) - - df_train.loc(axis=1)[self.feature_names] = normalize(df_train[self.feature_names].values) - df_valid.loc(axis=1)[self.feature_names] = normalize(df_valid[self.feature_names].values) - df_test.loc(axis=1)[self.feature_names] = normalize(df_test[self.feature_names].values) - - TimeInspector.log_cost_time("Finished normalizing data.") - - return df_train, df_valid, df_test + return replace_inf(df) -class ConfigSectionProcessor(Processor): - def __init__(self, feature_names, label_names, **kwargs): - super().__init__(feature_names, label_names) +class MinMaxNorm(InferProcessor): + def __init__(self, fit_start_time, fit_end_time): + self.fit_start_time = fit_start_time + self.fit_end_time = fit_end_time + + def fit(self, handler, df): + # TODO: 看看这里怎么取数据 + self.min_val = np.nanmin(df[handler.get_feature_names()].values, axis=0) + self.max_val = np.nanmax(df[handler.get_feature_names()].values, axis=0) + self.ignore = self.min_val == self.max_val + self.feature_names = copy.deepcopy(handler.get_feature_names()) + + def __call__(self, df): + # FIXME: The df will be changed inplace. It's very dangerous + # The code below is ugly + df = df.copy() # currently copy is used + def normalize(x, min_val=self.min_val, max_val=self.max_val, ignore=self.ignore): + if (~ignore).all(): + return (x - min_val) / (max_val - min_val) + for i in range(ignore.size): + if not ignore[i]: + x[i] = (x[i] - min_val) / (max_val - min_val) + return x + df.loc(axis=1)[self.feature_names] = normalize(df[self.feature_names].values) + return df + + +class ZscoreNorm(InferProcessor): + def __init__(self, fit_start_time, fit_end_time): + self.fit_start_time = fit_start_time + self.fit_end_time = fit_end_time + + def fit(self, handler, df): + self.mean_train = np.nanmean(df[handler.get_feature_names()].values, axis=0) + self.std_train = np.nanstd(df[handler.get_feature_names()].values, axis=0) + self.ignore = self.std_train == 0 + self.feature_names = handler.get_feature_names() + + def __call__(self, df): + # FIXME: The df will be changed inplace. It's very dangerous + # The code below is ugly + df = df.copy() # currently copy is used + def normalize(x, mean_train=self.mean_train, std_train=self.std_train, ignore=self.ignore): + if (~ignore).all(): + return (x - mean_train) / std_train + for i in range(ignore.size): + if not ignore[i]: + x[i] = (x[i] - mean_train) / std_train + return x + df.loc(axis=1)[self.feature_names] = normalize(df[self.feature_names].values) + return df + + +class ConfigSectionProcessor(InferProcessor): + def __init__(self, **kwargs): + super().__init__() # Options self.fillna_feature = kwargs.get("fillna_feature", True) self.fillna_label = kwargs.get("fillna_label", True) @@ -159,8 +214,12 @@ class ConfigSectionProcessor(Processor): self.shrink_feature_outlier = kwargs.get("shrink_feature_outlier", True) self.clip_label_outlier = kwargs.get("clip_label_outlier", False) - def __call__(self, *args): - return [self._transform(x) for x in args] + def fit(self, handler, df=None): + self.feature_names = handler.get_feature_names() + self.label_names = handler.get_label_names() + + def __call__(self, df): + return self._transform(df) def _transform(self, df): def _label_norm(x): diff --git a/qlib/model/task.py b/qlib/model/task.py new file mode 100644 index 000000000..e66159233 --- /dev/null +++ b/qlib/model/task.py @@ -0,0 +1,142 @@ +''' +Please implement similar function here + +# Rolling relealted + + def split_rolling_periods( + self, + train_start_date, + train_end_date, + validate_start_date, + validate_end_date, + test_start_date, + test_end_date, + rolling_period, + calendar_freq="day", + ): + """ + Calculating the Rolling split periods, the period rolling on market calendar. + :param train_start_date: + :param train_end_date: + :param validate_start_date: + :param validate_end_date: + :param test_start_date: + :param test_end_date: + :param rolling_period: The market period of rolling + :param calendar_freq: The frequence of the market calendar + :yield: Rolling split periods + """ + + def get_start_index(calendar, start_date): + start_index = bisect.bisect_left(calendar, start_date) + return start_index + + def get_end_index(calendar, end_date): + end_index = bisect.bisect_right(calendar, end_date) + return end_index - 1 + + calendar = self.raw_df.index.get_level_values("datetime").unique() + + train_start_index = get_start_index(calendar, pd.Timestamp(train_start_date)) + train_end_index = get_end_index(calendar, pd.Timestamp(train_end_date)) + valid_start_index = get_start_index(calendar, pd.Timestamp(validate_start_date)) + valid_end_index = get_end_index(calendar, pd.Timestamp(validate_end_date)) + test_start_index = get_start_index(calendar, pd.Timestamp(test_start_date)) + test_end_index = test_start_index + rolling_period - 1 + + need_stop_split = False + + bound_test_end_index = get_end_index(calendar, pd.Timestamp(test_end_date)) + + while not need_stop_split: + + if test_end_index > bound_test_end_index: + test_end_index = bound_test_end_index + need_stop_split = True + + yield ( + calendar[train_start_index], + calendar[train_end_index], + calendar[valid_start_index], + calendar[valid_end_index], + calendar[test_start_index], + calendar[test_end_index], + ) + + train_start_index += rolling_period + train_end_index += rolling_period + valid_start_index += rolling_period + valid_end_index += rolling_period + test_start_index += rolling_period + test_end_index += rolling_period + + def get_rolling_data( + self, + train_start_date, + train_end_date, + validate_start_date, + validate_end_date, + test_start_date, + test_end_date, + rolling_period, + calendar_freq="day", + ): + # Set generator. + for period in self.split_rolling_periods( + train_start_date, + train_end_date, + validate_start_date, + validate_end_date, + test_start_date, + test_end_date, + rolling_period, + calendar_freq, + ): + ( + x_train, + y_train, + x_validate, + y_validate, + x_test, + y_test, + ) = self.get_split_data(*period) + yield x_train, y_train, x_validate, y_validate, x_test, y_test + + def get_split_data( + self, + train_start_date, + train_end_date, + validate_start_date, + validate_end_date, + test_start_date, + test_end_date, + ): + """ + all return types are DataFrame + """ + ## TODO: loc can be slow, expecially when we put it at the second level index. + if self.raw_df.index.names[0] == "instrument": + df_train = self.raw_df.loc(axis=0)[:, train_start_date:train_end_date] + df_validate = self.raw_df.loc(axis=0)[:, validate_start_date:validate_end_date] + df_test = self.raw_df.loc(axis=0)[:, test_start_date:test_end_date] + else: + df_train = self.raw_df.loc[train_start_date:train_end_date] + df_validate = self.raw_df.loc[validate_start_date:validate_end_date] + df_test = self.raw_df.loc[test_start_date:test_end_date] + + TimeInspector.set_time_mark() + df_train, df_validate, df_test = self.process_data(df_train, df_validate, df_test) + TimeInspector.log_cost_time("Finished setup processed data.") + + x_train = df_train[self.feature_names] + y_train = df_train[self.label_names] + + x_validate = df_validate[self.feature_names] + y_validate = df_validate[self.label_names] + + x_test = df_test[self.feature_names] + y_test = df_test[self.label_names] + + return x_train, y_train, x_validate, y_validate, x_test, y_test + +''' diff --git a/qlib/utils.py b/qlib/utils/__init__.py similarity index 99% rename from qlib/utils.py rename to qlib/utils/__init__.py index f45b171de..0e0b76e1c 100644 --- a/qlib/utils.py +++ b/qlib/utils/__init__.py @@ -24,8 +24,8 @@ import numpy as np import pandas as pd from pathlib import Path -from .config import C -from .log import get_module_logger +from ..config import C +from ..log import get_module_logger log = get_module_logger("utils") @@ -377,7 +377,7 @@ def is_tradable_date(cur_date): date : pandas.Timestamp current date """ - from .data import D + from ..data import D return str(cur_date.date()) == str(D.calendar(start_time=cur_date, future=True)[0].date()) @@ -390,7 +390,7 @@ def get_date_range(trading_date, shift, future=False): :param future: bool :return: """ - from .data import D + from ..data import D calendar = D.calendar(future=future) if pd.to_datetime(trading_date) not in list(calendar): @@ -445,7 +445,7 @@ def transform_end_date(end_date=None, freq="day"): date : pandas.Timestamp current date """ - from .data import D + from ..data import D last_date = D.calendar(freq=freq)[-1] if end_date is None or (str(end_date) == "-1") or (pd.Timestamp(last_date) < pd.Timestamp(end_date)): diff --git a/qlib/utils/objm.py b/qlib/utils/objm.py new file mode 100644 index 000000000..d7c4f4cb1 --- /dev/null +++ b/qlib/utils/objm.py @@ -0,0 +1,130 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +import os +import pickle +import tempfile +from pathlib import Path + +from qlib.config import C + + +class ObjManager: + def save_obj(self, obj: object, name: str): + """ + save obj as name + + Parameters + ---------- + obj : object + object to be saved + name : str + name of the object + """ + raise NotImplementedError(f"Please implement `save_obj`") + + def save_objs(self, obj_name_l): + """ + save objects + + Parameters + ---------- + obj_name_l : list of + """ + raise NotImplementedError(f"Please implement the `save_objs` method") + + def load_obj(self, name: str) -> object: + """ + load object by name + + Parameters + ---------- + name : str + the name of the object + + Returns + ------- + object: + loaded object + """ + raise NotImplementedError(f"Please implement the `load_obj` method") + + def exists(self, name: str) -> bool: + """ + if the object named `name` exists + + Parameters + ---------- + name : str + name of the objecT + + Returns + ------- + bool: + If the object exists + """ + raise NotImplementedError(f"Please implement the `exists` method") + + def list(self) -> list: + """ + list the objects + + Returns + ------- + list: + the list of returned objects + """ + raise NotImplementedError(f"Please implement the `list` method") + + def remove(self, fname=None): + """remove. + + Parameters + ---------- + fname : + if file name is provided. specific file is removed + otherwise, The all the objects will be removed. + """ + raise NotImplementedError(f"Please implement the `remove` method") + + +class FileManager(ObjManager): + ''' + Use file system to manage objects + ''' + def __init__(self, path=None): + if path is None: + self.path = Path(self.create_path()) + else: + self.path = Path(path).resolve() + + def create_path(self) -> str: + try: + return tempfile.mkdtemp(prefix=str(C['file_manager_path']) + os.sep) + except AttributeError: + raise NotImplementedError(f"If path is not given, the `create_path` function should be implemented") + + def save_obj(self, obj, name): + with (self.path / name).open('wb') as f: + pickle.dump(obj, f) + + def save_objs(self, obj_name_l): + for obj, name in obj_name_l: + self.save_obj(obj, name) + + def load_obj(self, name): + with (self.path / name).open('rb') as f: + return pickle.load(f) + + def exists(self, name): + return (self.path / name).exists() + + def list(self): + return list(self.path.iterdir()) + + def remove(self, fname=None): + if fname is None: + for fp in self.path.glob('*'): + fp.unlink() + self.path.rmdir() + else: + (self.path / fname).unlink() diff --git a/qlib/utils/serial.py b/qlib/utils/serial.py new file mode 100644 index 000000000..a4825615f --- /dev/null +++ b/qlib/utils/serial.py @@ -0,0 +1,22 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +from pathlib import Path +import pickle + + +class Serializable: + ''' + Serializable behaves like pickle. + But it only save the state whose name starts with `_` + ''' + + def __getstate__(self) -> dict: + return {k: v for k, v in self.__dict__.items() if k.startswith('_') } + + def __setstate__(self, state: dict): + self.__dict__.update(state) + + def to_pickle(self, path: [Path, str]): + with Path(path).open('wb') as f: + pickle.dump(self, f) From 393584e535e3b9104199cddb20626619ce261cfe Mon Sep 17 00:00:00 2001 From: Young Date: Fri, 23 Oct 2020 03:37:10 +0000 Subject: [PATCH 005/241] Update handler interface round2 --- examples/workflow_by_code.py | 22 +- qlib/contrib/data/handler.py | 132 +++++---- qlib/contrib/online/manager.py | 2 +- qlib/contrib/online/utils.py | 17 +- qlib/data/dataset/handler.py | 522 ++++++++++----------------------- qlib/data/dataset/loader.py | 274 +++++++++++++++++ qlib/data/dataset/processor.py | 229 +++++++-------- qlib/log.py | 23 ++ qlib/utils/__init__.py | 66 +++++ 9 files changed, 715 insertions(+), 572 deletions(-) create mode 100644 qlib/data/dataset/loader.py diff --git a/examples/workflow_by_code.py b/examples/workflow_by_code.py index 3179cbab3..9f0d5b02f 100644 --- a/examples/workflow_by_code.py +++ b/examples/workflow_by_code.py @@ -31,7 +31,7 @@ if __name__ == "__main__": qlib.init(provider_uri=provider_uri, region=REG_CN) - MARKET = "CSI300" + MARKET = "csi300" BENCHMARK = "SH000300" @@ -39,27 +39,27 @@ if __name__ == "__main__": # train model ################################### DATA_HANDLER_CONFIG = { - "start_date": "2008-01-01", - "end_date": "2020-08-01", + "start_time": "2008-01-01", + "end_time": "2020-08-01", "fit_start_time":"2008-01-01", "fit_end_time":"2014-12-31", - "market": MARKET, + "instruments": MARKET, } TRAINER_CONFIG = { - "train_start_date": "2008-01-01", - "train_end_date": "2014-12-31", - "validate_start_date": "2015-01-01", - "validate_end_date": "2016-12-31", - "test_start_date": "2017-01-01", - "test_end_date": "2020-08-01", + "train_start_time": "2008-01-01", + "train_end_time": "2014-12-31", + "validate_start_time": "2015-01-01", + "validate_end_time": "2016-12-31", + "test_start_time": "2017-01-01", + "test_end_time": "2020-08-01", } # use default DataHandler # custom DataHandler, refer to: TODO: DataHandler API url handler = Alpha158(**DATA_HANDLER_CONFIG) - data = handler.fetch(slice('2008-01-01', '2014-12-31'), key=handler.DK_I) + data = handler.fetch(slice('2008-01-01', '2014-12-31'), data_key=handler.DK_I) print(data) sys.exit(0) # I have tested the code above --------------------------------------------- diff --git a/qlib/contrib/data/handler.py b/qlib/contrib/data/handler.py index 6f53670dd..c9959535a 100644 --- a/qlib/contrib/data/handler.py +++ b/qlib/contrib/data/handler.py @@ -1,41 +1,73 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. -from ...data.dataset.handler import ConfigQLibDataHandler -from ...data.dataset.processor import Processor, MinMaxNorm, ZscoreNorm, get_cls_kwargs +from ...data.dataset.handler import DataHandlerLP +from ...data.dataset.processor import Processor, MinMaxNorm, ZscoreNorm +from ...utils import get_cls_kwargs +from ...data.dataset import processor as processor_module from ...log import TimeInspector import copy -class ALPHA360(ConfigQLibDataHandler): - config_template = { - "price": {"windows": range(60)}, - "volume": {"windows": range(60)}, - } +class ALPHA360(DataHandlerLP): + def __init__(self, instruments="csi500", start_time=None, end_time=None): + data_loader = { + "class": "QlibDataLoader", + "kwargs": { + "config": { + "feature": { + "price": { + "windows": range(60) + }, + "volume": { + "windows": range(60) + }, + }, + "label": self.get_label_config() + }, + "group_fields": True, + } + } + infer_processors = ["ConfigSectionProcessor"] # ConfigSectionProcessor will normalize LABEL0 + super().__init__(instruments, start_time, end_time, data_loader=data_loader, infer_processors=infer_processors) + + def get_label_config(self): + return (["Ref($close, -2)/Ref($close, -1) - 1"], ["LABEL0"]) -class QLibDataHandlerV1(ConfigQLibDataHandler): - config_template = { - "kbar": {}, - "price": { - "windows": [0], - "feature": ["OPEN", "HIGH", "LOW", "VWAP"], - }, - "rolling": {}, - } +class ALPHA360vwap(ALPHA360): + def get_label_config(self): + return (["Ref($vwap, -2)/Ref($vwap, -1) - 1"], ["LABEL0"]) - def __init__(self, start_date, end_date, infer_processors=[], learn_processors=["DropnaLabel"], fit_start_time=None, fit_end_time=None, **kwargs): + +class Alpha158(DataHandlerLP): + def __init__( + self, + instruments="csi500", + start_time=None, + end_time=None, + infer_processors=[], + learn_processors=["DropnaLabel", { + "class": "CSZScoreNorm", + "kwargs": { + "fields_group": "label" + } + }], + fit_start_time=None, + fit_end_time=None, + ): def check_transform_proc(proc_l): new_l = [] for p in proc_l: if not isinstance(p, Processor): - klass, pkwargs = get_cls_kwargs(p) + klass, pkwargs = get_cls_kwargs(p, processor_module) + # FIXME: It's hard code here!!!!! if isinstance(klass, (MinMaxNorm, ZscoreNorm)): - assert(fit_start_time is not None and fit_end_time is not None) + assert (fit_start_time is not None and fit_end_time is not None) pkwargs.update({ "fit_start_time": fit_start_time, "fit_end_time": fit_end_time, - }) + }) new_l.append({"class": klass.__name__, "kwargs": pkwargs}) else: new_l.append(p) @@ -44,37 +76,37 @@ class QLibDataHandlerV1(ConfigQLibDataHandler): infer_processors = check_transform_proc(infer_processors) learn_processors = check_transform_proc(learn_processors) - super().__init__(start_date, end_date, infer_processors=infer_processors, learn_processors=learn_processors, **kwargs) + data_loader = { + "class": "QlibDataLoader", + "kwargs": { + "config": { + "feature": self.get_feature_config(), + "label": self.get_label_config() + }, + "group_fields": True, + } + } + super().__init__(instruments, + start_time, + end_time, + data_loader=data_loader, + infer_processors=infer_processors, + learn_processors=learn_processors) - def load_label(self): - """ - load the labels df - :return: df_labels - """ - TimeInspector.set_time_mark() + def get_feature_config(self): + return { + "kbar": {}, + "price": { + "windows": [0], + "feature": ["OPEN", "HIGH", "LOW", "VWAP"], + }, + "rolling": {}, + } - df_labels = super().load_label() - - ## calculate new labels - df_labels["LABEL1"] = df_labels["LABEL0"].groupby(level="datetime").apply(lambda x: (x - x.mean()) / x.std()) - - df_labels = df_labels.drop(["LABEL0"], axis=1) - - TimeInspector.log_cost_time("Finished loading labels.") - - return df_labels + def get_label_config(self): + return (["Ref($close, -2)/Ref($close, -1) - 1"], ["LABEL0"]) -class Alpha158(QLibDataHandlerV1): - config_template = { - "kbar": {}, - "price": { - "windows": [0], - "feature": ["OPEN", "HIGH", "LOW", "CLOSE"], - }, - "rolling": {}, - } - - def __init__(self, *args, **kwargs): - kwargs["labels"] = ["Ref($close, -2)/Ref($close, -1) - 1"] - super().__init__(*args, **kwargs) +class Alpha158vwap(Alpha158): + def get_label_config(self): + return (["Ref($vwap, -2)/Ref($vwap, -1) - 1"], ["LABEL0"]) diff --git a/qlib/contrib/online/manager.py b/qlib/contrib/online/manager.py index 7e9c766e8..cf850b9da 100644 --- a/qlib/contrib/online/manager.py +++ b/qlib/contrib/online/manager.py @@ -11,7 +11,7 @@ from ..backtest.account import Account from ..backtest.exchange import Exchange from .user import User from .utils import load_instance -from .utils import save_instance, init_instance_by_config +from ...utils import save_instance, init_instance_by_config class UserManager: diff --git a/qlib/contrib/online/utils.py b/qlib/contrib/online/utils.py index cf08e4dbe..611af63e4 100644 --- a/qlib/contrib/online/utils.py +++ b/qlib/contrib/online/utils.py @@ -7,7 +7,7 @@ import yaml import pandas as pd from ...data import D from ...log import get_module_logger -from ...utils import get_module_by_module_path +from ...utils import get_module_by_module_path, init_instance_by_config from ...utils import get_next_trading_date from ..backtest.exchange import Exchange @@ -45,21 +45,6 @@ def save_instance(instance, file_path): pickle.dump(instance, fr) -def init_instance_by_config(config): - """ - generate an instance with settings in config - Parameter - config : dict - python dict indicate a init parameters to create an item - :return - An instance - """ - module = get_module_by_module_path(config["module_path"]) - instance_class = getattr(module, config["class"]) - instance = instance_class(**config["args"]) - return instance - - def create_user_folder(path): path = pathlib.Path(path) if path.exists(): diff --git a/qlib/data/dataset/handler.py b/qlib/data/dataset/handler.py index e523fbfef..7cc7995ea 100644 --- a/qlib/data/dataset/handler.py +++ b/qlib/data/dataset/handler.py @@ -5,7 +5,7 @@ import abc import bisect import logging -from typing import Union +from typing import Union, Tuple import pandas as pd import numpy as np @@ -13,11 +13,13 @@ import numpy as np from ...log import get_module_logger, TimeInspector from ...data import D from ...config import C -from ...utils import parse_config, transform_end_date +from ...utils import parse_config, transform_end_date, init_instance_by_config from ...utils.serial import Serializable from pathlib import Path +from .loader import DataLoader from . import processor as processor_module +from . import loader as data_loader_module # TODO: A more general handler interface which does not relies on internal pd.DataFrame is needed. @@ -30,44 +32,57 @@ class DataHandler(Serializable): The data handler try to maintain a handler with 2 level. `datetime` & `instruments`. - + Any order of the index level can be suported(The order will implied in the data). The order <`datetime`, `instruments`> will be used when the dataframe index name is missed. Example of the data: - - $close $volume Ref($close, 1) Mean($close, 3) $high-$low + The multi-index of the columns is optional. + feature label + $close $volume Ref($close, 1) Mean($close, 3) $high-$low LABEL0 datetime instrument - 2010-01-04 SH600000 81.807068 17145150.0 83.737389 83.016739 2.741058 - SH600004 13.313329 11800983.0 13.313329 13.317701 0.183632 - SH600005 37.796539 12231662.0 38.258602 37.919757 0.970325 - SH600006 22.672380 7095624.0 22.508326 22.573947 0.557785 + 2010-01-04 SH600000 81.807068 17145150.0 83.737389 83.016739 2.741058 0.0032 + SH600004 13.313329 11800983.0 13.313329 13.317701 0.183632 0.0042 + SH600005 37.796539 12231662.0 38.258602 37.919757 0.970325 0.0289 ''' - def __init__(self, init_data=True): + def __init__(self, instruments, start_time=None, end_time=None, data_loader: Tuple[dict, str, DataLoader]=None, init_data=True): # Set logger self.logger = get_module_logger("DataHandler") - # Setup data. - self._data = {} + # Setup data loader + assert(data_loader is not None) # to make start_time end_time could have None default value + self.data_loader = init_instance_by_config(data_loader, data_loader_module, accept_types=DataLoader) + + self.instruments = instruments + self.start_time = start_time + self.end_time = end_time if init_data: self.init() super().__init__() - def init(self, force_reload: bool=True): + def init(self, enable_cache: bool=True): """ initialize the data. In case of running intialization for multiple time, it will do nothing for the second time. + It is responsible for maintaining following variable + 1) self._data + Parameters ---------- - force_reload : bool - force to reload the data even if the data have been initialized + enable_cache : bool + default value is false + if `enable_cache` == True + the processed data will be saved on disk, and handler will load the cached data from the disk directly + when we call `init` next time """ - pass - # if force_reload or hasattr(self, '_initialized', False): + # Setup data. + # _data may be with multiple column index level. The outer level indicates the feature set name + self._data = self.data_loader.load(self.instruments, self.start_time, self.end_time) + # TODO: cache - def get_level_index(self, df: pd.DataFrame, level=Union[str, int]) -> int: + def _get_level_index(self, df: pd.DataFrame, level=Union[str, int]) -> int: """ get the level index of `df` given `level` @@ -88,40 +103,78 @@ class DataHandler(Serializable): try: return df.index.names.index(level) except (AttributeError, ValueError): - # NOTE: If level index is not given in the data, the default level index will be ('datetime', 'instrument') + # NOTE: If level index is not given in the data, the default level index will be ('datetime', 'instrument') return ('datetime', 'instrument').index(level) elif isinstance(level, int): return level else: raise NotImplementedError(f"This type of input is not supported") - def _fetch_df(self, df: pd.DataFrame, selector: Union[pd.Timestamp, slice, str, list], level: Union[str, int]): + def _fetch_df_by_index(self, df: pd.DataFrame, selector: Union[pd.Timestamp, slice, str, list], level: Union[str, int]) -> pd.DataFrame: """ fetch data from `data` with `selector` and `level` Parameters ---------- - df : pd.DataFrame - the data frame to be selected selector : Union[pd.Timestamp, slice, str, list] selector - level : Union[pd.Timestamp, slice, str] + level : Union[int, str] the level to use the selector """ # Try to get the right index idx_slc = (selector, slice(None, None)) - if self.get_level_index(df, level) == 1: - idx_slc = idx_slc[1], idx_slc[0] + if self._get_level_index(df, level) == 1: + idx_slc = idx_slc[1], idx_slc[0] return df.loc(axis=0)[idx_slc] - - def fetch(self, selector: Union[pd.Timestamp, slice, str], level='datetime', key=None) -> Union[pd.DataFrame, dict]: - if key is None: - res = {} - for k, df in self._data.items(): - res[k] = self._fetch_df(df, selector, level) + + CS_ALL = '_all' + + def _fetch_df_by_col(self, df: pd.DataFrame, col_set: str) -> pd.DataFrame: + cln = len(df.columns.levels) + if cln == 1: + return df + elif col_set == self.CS_ALL: + return df.droplevel(axis=1, level=0) else: - res = self._fetch_df(self._data[key], selector, level) - return res + return df.loc(axis=1)[col_set] + + def fetch(self, selector: Union[pd.Timestamp, slice, str], level: Union[str, int]='datetime', col_set=CS_ALL) -> pd.DataFrame: + """ + fetch data from underlying data source + + Parameters + ---------- + selector : Union[pd.Timestamp, slice, str] + describe how to select data by index + level : Union[str, int] + which index level to select the data + col_set : str + select a set of meaningful columns.(e.g. features, columns) + + Returns + ------- + pd.DataFrame: + """ + df = self._fetch_df_by_index(self._data, selector, level) + return self._fetch_df_by_col(df, col_set) + + def get_cols(self, col_set=CS_ALL) -> list: + """ + get the column names + + Parameters + ---------- + col_set : str + select a set of meaningful columns.(e.g. features, columns) + + Returns + ------- + list: + list of column names + """ + df = self._data.head() + df = self._fetch_df_by_col(df, col_set) + return df.columns.to_list() class DataHandlerLP(DataHandler): @@ -142,14 +195,13 @@ class DataHandlerLP(DataHandler): # - _proc_learn_df will be processed by infer_processors + learn_processors # - (e.g. _proc_infer_df processed by learn_processors ) - def __init__(self, infer_processors=[], learn_processors=[], process_type=PTYPE_A, **kwargs): + def __init__(self, instruments, start_time=None, end_time=None, data_loader: Tuple[dict, str, DataLoader]=None, infer_processors=[], learn_processors=[], process_type=PTYPE_A, **kwargs): """ - Parameters ---------- infer_processors : list list of of processors to generate data for inference - example of : + example of : 1) classname & kwargs: { "class": "MinMaxNorm", @@ -180,24 +232,18 @@ class DataHandlerLP(DataHandler): self.learn_processors = [] # for lint for pname in 'infer_processors', 'learn_processors': for proc in locals()[pname]: - getattr(self, pname).append(processor_module.init_proc_obj(proc)) + getattr(self, pname).append(init_instance_by_config(proc, processor_module, + accept_types=(processor_module.Processor,))) self.process_type = process_type - super().__init__(**kwargs) + super().__init__(instruments, start_time, end_time, data_loader, **kwargs) def get_all_processors(self): return self.infer_processors + self.learn_processors - def _init_raw_data(self): - """ - initialize the raw data - the raw data will be saved in to `self._data['raw']` - """ - raise NotImplementedError(f"Please implement the `_init_raw_data` method") - def fit(self): for proc in self.get_all_processors(): - proc.fit(self) + proc.fit(self._data) def fit_process_data(self): """ @@ -206,7 +252,7 @@ class DataHandlerLP(DataHandler): The input of the `fit` will be the output of the previous processor """ self.process_data(with_fit=True) - + def process_data(self, with_fit: bool=False): """ @@ -218,50 +264,56 @@ class DataHandlerLP(DataHandler): The input of the `fit` will be the output of the previous processor """ # data for inference - _infer_df = self._data[DataHandlerLP.DK_R] + _infer_df = self._data + if len(self.infer_processors) > 0: # avoid modifying the original data + _infer_df = _infer_df.copy() + for proc in self.infer_processors: if not proc.is_for_infer(): raise TypeError("Only processors usable for inference can be used in `infer_processors` ") if with_fit: - proc.fit(self, _infer_df) + proc.fit(_infer_df) _infer_df = proc(_infer_df) + self._infer = _infer_df # data for learning if self.process_type == DataHandlerLP.PTYPE_I: - _learn_df = self._data[DataHandlerLP.DK_R] + _learn_df = self._data elif self.process_type == DataHandlerLP.PTYPE_A: # based on `infer_df` and append the processor _learn_df = _infer_df else: raise NotImplementedError(f"This type of input is not supported") + if len(self.learn_processors) > 0: # avoid modifying the original data + _learn_df = _learn_df.copy() for proc in self.learn_processors: if with_fit: - proc.fit(self, _learn_df) + proc.fit(_learn_df) _learn_df = proc(_learn_df) - - self._data.update({ - DataHandlerLP.DK_I: _infer_df, - DataHandlerLP.DK_L: _learn_df, - }) + self._learn = _learn_df # init type IT_FIT_SEQ = 'fit_seq' # the input of `fit` will be the output of the previous processor IT_FIT_IND = 'fit_ind' # the input of `fit` will be the original df IT_LS = 'load_state' # The state of the object has been load by pickle - - def init(self, init_type: str=IT_FIT_SEQ, path: Path=None): + + def init(self, init_type: str=IT_FIT_SEQ, enable_cache: bool=False): """ Initialize the data of Qlib Parameters ---------- init_type : str - 'fit' or 'load_state' - path : path - if `init_type` == 'load_state': `path` will be used to load_state + The type `IT_*` listed above + enable_cache : bool + default value is false + if `enable_cache` == True: + the processed data will be saved on disk, and handler will load the cached data from the disk directly + when we call `init` next time """ - self._init_raw_data() + # init raw data + super().init(enable_cache=enable_cache) if init_type == DataHandlerLP.IT_FIT_IND: self.fit() @@ -275,311 +327,53 @@ class DataHandlerLP(DataHandler): # TODO: Be able to cache handler data. Save the memory for data processing + def _get_df_by_key(self, data_key: str=DK_I) -> pd.DataFrame: + df = getattr(self, {self.DK_R: '_data', self.DK_I: "_infer", self.DK_L: "_learn"}[data_key]) + return df -class DataHandlerLPWL(DataHandlerLP): - ''' - DataHandler with (L)earnable (P)rocessor with (L)abel - ''' - - def _init_raw_data(self): + def fetch(self, + selector: Union[pd.Timestamp, slice, str], + level: Union[str, int] = 'datetime', + col_set=DataHandler.CS_ALL, + data_key: str = DK_I) -> pd.DataFrame: """ - init raw_df, feature_names, label_names of DataHandler - if the index of df_feature and df_label are not same, user need to overload this method to merge (e.g. inner, left, right merge). + fetch data from underlying data source + + Parameters + ---------- + selector : Union[pd.Timestamp, slice, str] + describe how to select data by index + level : Union[str, int] + which index level to select the data + col_set : str + select a set of meaningful columns.(e.g. features, columns) + data_key: str + The data to fetch: DK_* + + Returns + ------- + pd.DataFrame: """ - df_features = self.load_feature() - feature_names = df_features.columns + df = self._get_df_by_key(data_key) + df = self._fetch_df_by_index(df, selector, level) + return self._fetch_df_by_col(df, col_set) - df_labels = self.load_label() - label_names = df_labels.columns - - raw_df = df_features.merge(df_labels, left_index=True, right_index=True, how="left") - self.feature_names = feature_names - self.label_names = label_names - self._data['raw'] = raw_df - - def load_feature(self): + def get_cols(self, col_set=DataHandler.CS_ALL, data_key: str=DK_I) -> list: """ - Implement this method to load raw feature. - the format of the feature is below - return: df_features + get the column names + + Parameters + ---------- + col_set : str + select a set of meaningful columns.(e.g. features, columns) + data_key: str + The data to fetch: DK_* + + Returns + ------- + list: + list of column names """ - raise NotImplementedError(f"Please implement `load_feature`") - - def load_label(self): - """ - Implement this method to load and calculate label. - the format of the label is below - - return: df_label - """ - raise NotImplementedError(f"Please implement `load_label`") - - def get_feature_names(self): - return self.feature_names - - def get_label_names(self): - return self.label_names - - -class QLibDataHandler(DataHandlerLPWL): - def __init__(self, start_date, end_date, *args, **kwargs): - # Dates. - self.start_date = start_date - self.end_date = end_date - - # Instruments - instruments = kwargs.pop("instruments", None) - if instruments is None: - market = kwargs.pop("market", "csi500").lower() - data_filter_list = kwargs.pop("data_filter_list", list()) - self.instruments = D.instruments(market, filter_pipe=data_filter_list) - else: - self.instruments = instruments - - # Config of features and labels - self._fields = kwargs.pop("fields", []) - self._names = kwargs.pop("names", []) - self._labels = kwargs.pop("labels", []) - self._label_names = kwargs.pop("label_names", []) - - # Check arguments - assert len(self._fields) > 0, "features list is empty" - assert len(self._labels) > 0, "labels list is empty" - - # Check end_date - # If test_end_date is -1 or greater than the last date, the last date is used - self.end_date = transform_end_date(self.end_date) - - super().__init__(*args, **kwargs) - - def load_feature(self): - """ - Load the raw data. - return: df_features - """ - TimeInspector.set_time_mark() - - if len(self._names) == 0: - names = ["F%d" % i for i in range(len(self._fields))] - else: - names = self._names - - df_features = D.features(self.instruments, self._fields, self.start_date, self.end_date) - df_features.columns = names - - TimeInspector.log_cost_time("Finished loading features.") - - return df_features - - def load_label(self): - """ - Build up labels in df through users' method - :return: df_labels - """ - TimeInspector.set_time_mark() - - if len(self._label_names) == 0: - label_names = ["LABEL%d" % i for i in range(len(self._labels))] - else: - label_names = self._label_names - - df_labels = D.features(self.instruments, self._labels, self.start_date, self.end_date) - df_labels.columns = label_names - - TimeInspector.log_cost_time("Finished loading labels.") - - return df_labels - - -def parse_config_to_fields(config): - """create factors from config - - config = { - 'kbar': {}, # whether to use some hard-code kbar features - 'price': { # whether to use raw price features - 'windows': [0, 1, 2, 3, 4], # use price at n days ago - 'feature': ['OPEN', 'HIGH', 'LOW'] # which price field to use - }, - 'volume': { # whether to use raw volume features - 'windows': [0, 1, 2, 3, 4], # use volume at n days ago - }, - 'rolling': { # whether to use rolling operator based features - 'windows': [5, 10, 20, 30, 60], # rolling windows size - 'include': ['ROC', 'MA', 'STD'], # rolling operator to use - #if include is None we will use default operators - 'exclude': ['RANK'], # rolling operator not to use - } - } - """ - fields = [] - names = [] - if "kbar" in config: - fields += [ - "($close-$open)/$open", - "($high-$low)/$open", - "($close-$open)/($high-$low+1e-12)", - "($high-Greater($open, $close))/$open", - "($high-Greater($open, $close))/($high-$low+1e-12)", - "(Less($open, $close)-$low)/$open", - "(Less($open, $close)-$low)/($high-$low+1e-12)", - "(2*$close-$high-$low)/$open", - "(2*$close-$high-$low)/($high-$low+1e-12)", - ] - names += [ - "KMID", - "KLEN", - "KMID2", - "KUP", - "KUP2", - "KLOW", - "KLOW2", - "KSFT", - "KSFT2", - ] - if "price" in config: - windows = config["price"].get("windows", range(5)) - feature = config["price"].get("feature", ["OPEN", "HIGH", "LOW", "CLOSE", "VWAP"]) - for field in feature: - field = field.lower() - fields += ["Ref($%s, %d)/$close" % (field, d) if d != 0 else "$%s/$close" % field for d in windows] - names += [field.upper() + str(d) for d in windows] - if "volume" in config: - windows = config["volume"].get("windows", range(5)) - fields += ["Ref($volume, %d)/$volume" % d if d != 0 else "$volume/$volume" for d in windows] - names += ["VOLUME" + str(d) for d in windows] - if "rolling" in config: - windows = config["rolling"].get("windows", [5, 10, 20, 30, 60]) - include = config["rolling"].get("include", None) - exclude = config["rolling"].get("exclude", []) - # `exclude` in dataset config unnecessary filed - # `include` in dataset config necessary field - use = lambda x: x not in exclude and (include is None or x in include) - if use("ROC"): - fields += ["Ref($close, %d)/$close" % d for d in windows] - names += ["ROC%d" % d for d in windows] - if use("MA"): - fields += ["Mean($close, %d)/$close" % d for d in windows] - names += ["MA%d" % d for d in windows] - if use("STD"): - fields += ["Std($close, %d)/$close" % d for d in windows] - names += ["STD%d" % d for d in windows] - if use("BETA"): - fields += ["Slope($close, %d)/$close" % d for d in windows] - names += ["BETA%d" % d for d in windows] - if use("RSQR"): - fields += ["Rsquare($close, %d)" % d for d in windows] - names += ["RSQR%d" % d for d in windows] - if use("RESI"): - fields += ["Resi($close, %d)/$close" % d for d in windows] - names += ["RESI%d" % d for d in windows] - if use("MAX"): - fields += ["Max($high, %d)/$close" % d for d in windows] - names += ["MAX%d" % d for d in windows] - if use("LOW"): - fields += ["Min($low, %d)/$close" % d for d in windows] - names += ["MIN%d" % d for d in windows] - if use("QTLU"): - fields += ["Quantile($close, %d, 0.8)/$close" % d for d in windows] - names += ["QTLU%d" % d for d in windows] - if use("QTLD"): - fields += ["Quantile($close, %d, 0.2)/$close" % d for d in windows] - names += ["QTLD%d" % d for d in windows] - if use("RANK"): - fields += ["Rank($close, %d)" % d for d in windows] - names += ["RANK%d" % d for d in windows] - if use("RSV"): - fields += ["($close-Min($low, %d))/(Max($high, %d)-Min($low, %d)+1e-12)" % (d, d, d) for d in windows] - names += ["RSV%d" % d for d in windows] - if use("IMAX"): - fields += ["IdxMax($high, %d)/%d" % (d, d) for d in windows] - names += ["IMAX%d" % d for d in windows] - if use("IMIN"): - fields += ["IdxMin($low, %d)/%d" % (d, d) for d in windows] - names += ["IMIN%d" % d for d in windows] - if use("IMXD"): - fields += ["(IdxMax($high, %d)-IdxMin($low, %d))/%d" % (d, d, d) for d in windows] - names += ["IMXD%d" % d for d in windows] - if use("CORR"): - fields += ["Corr($close, Log($volume+1), %d)" % d for d in windows] - names += ["CORR%d" % d for d in windows] - if use("CORD"): - fields += ["Corr($close/Ref($close,1), Log($volume/Ref($volume, 1)+1), %d)" % d for d in windows] - names += ["CORD%d" % d for d in windows] - if use("CNTP"): - fields += ["Mean($close>Ref($close, 1), %d)" % d for d in windows] - names += ["CNTP%d" % d for d in windows] - if use("CNTN"): - fields += ["Mean($closeRef($close, 1), %d)-Mean($close pd.DataFrame: + """ + load the data as pd.DataFrame + + Returns + ------- + pd.DataFrame: + data load from the under layer source + + Example of the data: + The multi-index of the columns is optional. + feature label + $close $volume Ref($close, 1) Mean($close, 3) $high-$low LABEL0 + datetime instrument + 2010-01-04 SH600000 81.807068 17145150.0 83.737389 83.016739 2.741058 0.0032 + SH600004 13.313329 11800983.0 13.313329 13.317701 0.183632 0.0042 + SH600005 37.796539 12231662.0 38.258602 37.919757 0.970325 0.0289 + """ + pass + + +class QlibDataLoader(DataLoader): + '''Same as QlibDataLoader. The fields can be define by config''' + def __init__(self, config: Tuple[list, tuple, dict], group_fields: bool = False, filter_pipe=None): + """ + Parameters + ---------- + config : Tuple[list ,tuple, dict] + Config will be used to describe the fields and column names + + if `group_fields`: + := { + "group_name1": + "group_name2": + } + else: + := + + := ["expr", ...] | (["expr", ...], ["col_name", ...]) | + + is a config with dict type which could be parsed by `parse_config_to_fields` + + Here is a few examples to describe the fields + TODO: + + group_fields : bool + Will the fields be grouped. Multi-index will be used for the group + """ + if group_fields: + fields_all = [] + name_grp_info = [] + for grp, fields_info in config.items(): + fields, names = self._parse_fields_info(fields_info) + fields_all.extend(fields) + name_grp_info.extend([(grp, n) for n in names]) + self.fields, self.names = fields_all, name_grp_info + else: + self.fields, self.names = self._parse_fields_info(fields_info) + + self.group_fields = group_fields + self.filter_pipe = filter_pipe + + def _parse_fields_info(self, fields_info: Tuple[list, tuple, dict]) -> Tuple[list, list]: + if isinstance(fields_info, dict): + fields, names = parse_config_to_fields(fields_info) + elif isinstance(fields_info, list): + fields = fields_info + names = fields + elif isinstance(fields_info, tuple): + fields, names = fields_info + else: + raise NotImplementedError(f"This type of input is not supported") + return fields, names + + def load(self, + instruments, + config: Tuple[list, tuple, dict], + group_fields=False, + start_time=None, + end_time=None) -> Tuple[pd.DataFrame, dict]: + df = D.features(D.instruments(instruments, filter_pipe=self.filter_pipe), self.fields, start_time, end_time) + df.columns = pd.MultiIndex.from_tuples(self.names) if self.group_fields else self.names + df = df.swaplevel().sort_index() + return df + + +# TODO: make it easier to understand the config language +def parse_config_to_fields(config): + """create factors from config + + config = { + 'kbar': {}, # whether to use some hard-code kbar features + 'price': { # whether to use raw price features + 'windows': [0, 1, 2, 3, 4], # use price at n days ago + 'feature': ['OPEN', 'HIGH', 'LOW'] # which price field to use + }, + 'volume': { # whether to use raw volume features + 'windows': [0, 1, 2, 3, 4], # use volume at n days ago + }, + 'rolling': { # whether to use rolling operator based features + 'windows': [5, 10, 20, 30, 60], # rolling windows size + 'include': ['ROC', 'MA', 'STD'], # rolling operator to use + #if include is None we will use default operators + 'exclude': ['RANK'], # rolling operator not to use + } + } + """ + fields = [] + names = [] + if "kbar" in config: + fields += [ + "($close-$open)/$open", + "($high-$low)/$open", + "($close-$open)/($high-$low+1e-12)", + "($high-Greater($open, $close))/$open", + "($high-Greater($open, $close))/($high-$low+1e-12)", + "(Less($open, $close)-$low)/$open", + "(Less($open, $close)-$low)/($high-$low+1e-12)", + "(2*$close-$high-$low)/$open", + "(2*$close-$high-$low)/($high-$low+1e-12)", + ] + names += [ + "KMID", + "KLEN", + "KMID2", + "KUP", + "KUP2", + "KLOW", + "KLOW2", + "KSFT", + "KSFT2", + ] + if "price" in config: + windows = config["price"].get("windows", range(5)) + feature = config["price"].get("feature", ["OPEN", "HIGH", "LOW", "CLOSE", "VWAP"]) + for field in feature: + field = field.lower() + fields += ["Ref($%s, %d)/$close" % (field, d) if d != 0 else "$%s/$close" % field for d in windows] + names += [field.upper() + str(d) for d in windows] + if "volume" in config: + windows = config["volume"].get("windows", range(5)) + fields += ["Ref($volume, %d)/$volume" % d if d != 0 else "$volume/$volume" for d in windows] + names += ["VOLUME" + str(d) for d in windows] + if "rolling" in config: + windows = config["rolling"].get("windows", [5, 10, 20, 30, 60]) + include = config["rolling"].get("include", None) + exclude = config["rolling"].get("exclude", []) + # `exclude` in dataset config unnecessary filed + # `include` in dataset config necessary field + use = lambda x: x not in exclude and (include is None or x in include) + if use("ROC"): + fields += ["Ref($close, %d)/$close" % d for d in windows] + names += ["ROC%d" % d for d in windows] + if use("MA"): + fields += ["Mean($close, %d)/$close" % d for d in windows] + names += ["MA%d" % d for d in windows] + if use("STD"): + fields += ["Std($close, %d)/$close" % d for d in windows] + names += ["STD%d" % d for d in windows] + if use("BETA"): + fields += ["Slope($close, %d)/$close" % d for d in windows] + names += ["BETA%d" % d for d in windows] + if use("RSQR"): + fields += ["Rsquare($close, %d)" % d for d in windows] + names += ["RSQR%d" % d for d in windows] + if use("RESI"): + fields += ["Resi($close, %d)/$close" % d for d in windows] + names += ["RESI%d" % d for d in windows] + if use("MAX"): + fields += ["Max($high, %d)/$close" % d for d in windows] + names += ["MAX%d" % d for d in windows] + if use("LOW"): + fields += ["Min($low, %d)/$close" % d for d in windows] + names += ["MIN%d" % d for d in windows] + if use("QTLU"): + fields += ["Quantile($close, %d, 0.8)/$close" % d for d in windows] + names += ["QTLU%d" % d for d in windows] + if use("QTLD"): + fields += ["Quantile($close, %d, 0.2)/$close" % d for d in windows] + names += ["QTLD%d" % d for d in windows] + if use("RANK"): + fields += ["Rank($close, %d)" % d for d in windows] + names += ["RANK%d" % d for d in windows] + if use("RSV"): + fields += ["($close-Min($low, %d))/(Max($high, %d)-Min($low, %d)+1e-12)" % (d, d, d) for d in windows] + names += ["RSV%d" % d for d in windows] + if use("IMAX"): + fields += ["IdxMax($high, %d)/%d" % (d, d) for d in windows] + names += ["IMAX%d" % d for d in windows] + if use("IMIN"): + fields += ["IdxMin($low, %d)/%d" % (d, d) for d in windows] + names += ["IMIN%d" % d for d in windows] + if use("IMXD"): + fields += ["(IdxMax($high, %d)-IdxMin($low, %d))/%d" % (d, d, d) for d in windows] + names += ["IMXD%d" % d for d in windows] + if use("CORR"): + fields += ["Corr($close, Log($volume+1), %d)" % d for d in windows] + names += ["CORR%d" % d for d in windows] + if use("CORD"): + fields += ["Corr($close/Ref($close,1), Log($volume/Ref($volume, 1)+1), %d)" % d for d in windows] + names += ["CORD%d" % d for d in windows] + if use("CNTP"): + fields += ["Mean($close>Ref($close, 1), %d)" % d for d in windows] + names += ["CNTP%d" % d for d in windows] + if use("CNTN"): + fields += ["Mean($closeRef($close, 1), %d)-Mean($close (type, dict): - """ - extract class and kwargs from processor info - - Parameters - ---------- - processor : [dict, str] - similar to processor - - Returns - ------- - (type, dict): - the class object and it's arguments. - """ - if isinstance(processor, dict): - # raise AttributeError - klass = globals()[processor['class']] - kwargs = processor['kwargs'] - elif isinstance(processor, str): - klass = globals()[processor] - kwargs = {} - else: - raise NotImplementedError(f"This type of input is not supported") - return klass, kwargs - - -# Place the function here to be able to reference the Processor -def init_proc_obj(processor: [dict, str, Processor]) -> Processor: - """ - Initialize Processor Object - - Parameters - ---------- - processor : [dict, str, Processor] - The info to initialize processor - - Returns - ------- - Processor: - initialized Processor - """ - if not isinstance(processor, Processor): - klass, pkwargs = get_cls_kwargs(processor) - processor = klass(**pkwargs) - return processor - - -class InferProcessor(Processor): - '''This processor is usable for inference''' def is_for_infer(self) -> bool: """ Is this processor usable for inference + Some processors are not usable for inference. Returns ------- @@ -105,37 +72,24 @@ class InferProcessor(Processor): return True -class NInferProcessor(Processor): - '''This processor is not usable for inference''' - def is_for_infer(self) -> bool: - """ - Is this processor usable for inference +class DropnaProcessor(Processor): + def __init__(self, group=None): + self.group = group - Returns - ------- - bool: - if it is usable for infenrece - """ + def __call__(self, df): + return df.dropna(subset=get_group_columns(df, self.group)) + + +class DropnaLabel(DropnaProcessor): + def __init__(self, group='label'): + super().__init__(group=group) + + def is_for_infer(self) -> bool: + '''The samples are dropped according to label. So it is not usable for inference''' return False -class DropnaFeature(InferProcessor): - def fit(self, handler, df=None): - self.feature_names = copy.deepcopy(handler.get_feature_names()) - - def __call__(self, df): - return df.dropna(subset=self.feature_names) - - -class DropnaLabel(InferProcessor): - def fit(self, handler, df=None): - self.label_names = copy.deepcopy(handler.get_label_names()) - - def __call__(self, df): - return df.dropna(subset=self.label_names) - - -class ProcessInf(InferProcessor): +class ProcessInf(Processor): '''Process infinity ''' def __call__(self, df): def replace_inf(data): @@ -151,22 +105,20 @@ class ProcessInf(InferProcessor): return replace_inf(df) -class MinMaxNorm(InferProcessor): - def __init__(self, fit_start_time, fit_end_time): +class MinMaxNorm(Processor): + def __init__(self, fit_start_time, fit_end_time, fields_group=None): self.fit_start_time = fit_start_time self.fit_end_time = fit_end_time + self.fields_group = fields_group - def fit(self, handler, df): - # TODO: 看看这里怎么取数据 - self.min_val = np.nanmin(df[handler.get_feature_names()].values, axis=0) - self.max_val = np.nanmax(df[handler.get_feature_names()].values, axis=0) + def fit(self, df): + cols = get_group_columns(df, self.fields_group) + self.min_val = np.nanmin(df[cols].values, axis=0) + self.max_val = np.nanmax(df[cols].values, axis=0) self.ignore = self.min_val == self.max_val - self.feature_names = copy.deepcopy(handler.get_feature_names()) + self.cols = cols def __call__(self, df): - # FIXME: The df will be changed inplace. It's very dangerous - # The code below is ugly - df = df.copy() # currently copy is used def normalize(x, min_val=self.min_val, max_val=self.max_val, ignore=self.ignore): if (~ignore).all(): return (x - min_val) / (max_val - min_val) @@ -174,25 +126,24 @@ class MinMaxNorm(InferProcessor): if not ignore[i]: x[i] = (x[i] - min_val) / (max_val - min_val) return x - df.loc(axis=1)[self.feature_names] = normalize(df[self.feature_names].values) + df.loc(axis=1)[self.cols] = normalize(df[self.cols].values) return df -class ZscoreNorm(InferProcessor): - def __init__(self, fit_start_time, fit_end_time): +class ZscoreNorm(Processor): + def __init__(self, fit_start_time, fit_end_time, fields_group=None): self.fit_start_time = fit_start_time self.fit_end_time = fit_end_time + self.fields_group = fields_group - def fit(self, handler, df): - self.mean_train = np.nanmean(df[handler.get_feature_names()].values, axis=0) - self.std_train = np.nanstd(df[handler.get_feature_names()].values, axis=0) + def fit(self, df): + cols = get_group_columns(df, self.fields_group) + self.mean_train = np.nanmean(df[cols].values, axis=0) + self.std_train = np.nanstd(df[cols].values, axis=0) self.ignore = self.std_train == 0 - self.feature_names = handler.get_feature_names() + self.cols = cols def __call__(self, df): - # FIXME: The df will be changed inplace. It's very dangerous - # The code below is ugly - df = df.copy() # currently copy is used def normalize(x, mean_train=self.mean_train, std_train=self.std_train, ignore=self.ignore): if (~ignore).all(): return (x - mean_train) / std_train @@ -200,12 +151,27 @@ class ZscoreNorm(InferProcessor): if not ignore[i]: x[i] = (x[i] - mean_train) / std_train return x - df.loc(axis=1)[self.feature_names] = normalize(df[self.feature_names].values) + df.loc(axis=1)[self.cols] = normalize(df[self.cols].values) return df -class ConfigSectionProcessor(InferProcessor): - def __init__(self, **kwargs): +class CSZScoreNorm(Processor): + '''Cross Sectional ZScore Normalization''' + def __init__(self, fields_group=None): + self.fields_group = fields_group + + def __call__(self, df): + # try not modify original dataframe + cols = get_group_columns(df,self.fields_group) + df[cols] = df[cols].groupby('datetime').apply(lambda df: (df - df.mean()).div(df.std())) + return df + + +# TODO: make the config language easier to understand +class ConfigSectionProcessor(Processor): + # TODO: this class is not well tested + # FIXME: this will raise error when multi-index is passed in + def __init__(self, fields_group=None, **kwargs): super().__init__() # Options self.fillna_feature = kwargs.get("fillna_feature", True) @@ -214,9 +180,7 @@ class ConfigSectionProcessor(InferProcessor): self.shrink_feature_outlier = kwargs.get("shrink_feature_outlier", True) self.clip_label_outlier = kwargs.get("clip_label_outlier", False) - def fit(self, handler, df=None): - self.feature_names = handler.get_feature_names() - self.label_names = handler.get_label_names() + self.fields_group = None def __call__(self, df): return self._transform(df) @@ -245,19 +209,22 @@ class ConfigSectionProcessor(InferProcessor): TimeInspector.set_time_mark() - # Copy - df_new = df.copy() + # Copy the focus part and change it to single level + selected_cols = get_group_columns(df, self.fields_group) + df_focus = df[selected_cols].copy() + if len(df_focus.columns.levels) > 1: + df_focus = df_focus.droplevel(level=0) # Label - cols = df.columns[df.columns.str.contains("^LABEL")] - df_new[cols] = df[cols].groupby(level="datetime").apply(_label_norm) + cols = df_focus.columns[df_focus.columns.str.contains("^LABEL")] + df_focus[cols] = df_focus[cols].groupby(level="datetime").apply(_label_norm) # Features - cols = df.columns[df.columns.str.contains("^KLEN|^KLOW|^KUP")] - df_new[cols] = df[cols].apply(lambda x: x ** 0.25).groupby(level="datetime").apply(_feature_norm) + cols = df_focus.columns[df_focus.columns.str.contains("^KLEN|^KLOW|^KUP")] + df_focus[cols] = df_focus[cols].apply(lambda x: x ** 0.25).groupby(level="datetime").apply(_feature_norm) - cols = df.columns[df.columns.str.contains("^KLOW2|^KUP2")] - df_new[cols] = df[cols].apply(lambda x: x ** 0.5).groupby(level="datetime").apply(_feature_norm) + cols = df_focus.columns[df_focus.columns.str.contains("^KLOW2|^KUP2")] + df_focus[cols] = df_focus[cols].apply(lambda x: x ** 0.5).groupby(level="datetime").apply(_feature_norm) _cols = [ "KMID", @@ -282,27 +249,29 @@ class ConfigSectionProcessor(InferProcessor): "VSUMD", ] pat = "|".join(["^" + x for x in _cols]) - cols = df.columns[df.columns.str.contains(pat) & (~df.columns.isin(["HIGH0", "LOW0"]))] - df_new[cols] = df[cols].groupby(level="datetime").apply(_feature_norm) + cols = df_focus.columns[df_focus.columns.str.contains(pat) & (~df_focus.columns.isin(["HIGH0", "LOW0"]))] + df_focus[cols] = df_focus[cols].groupby(level="datetime").apply(_feature_norm) - cols = df.columns[df.columns.str.contains("^STD|^VOLUME|^VMA|^VSTD")] - df_new[cols] = df[cols].apply(np.log).groupby(level="datetime").apply(_feature_norm) + cols = df_focus.columns[df_focus.columns.str.contains("^STD|^VOLUME|^VMA|^VSTD")] + df_focus[cols] = df_focus[cols].apply(np.log).groupby(level="datetime").apply(_feature_norm) - cols = df.columns[df.columns.str.contains("^RSQR")] - df_new[cols] = df[cols].fillna(0).groupby(level="datetime").apply(_feature_norm) + cols = df_focus.columns[df_focus.columns.str.contains("^RSQR")] + df_focus[cols] = df_focus[cols].fillna(0).groupby(level="datetime").apply(_feature_norm) - cols = df.columns[df.columns.str.contains("^MAX|^HIGH0")] - df_new[cols] = df[cols].apply(lambda x: (x - 1) ** 0.5).groupby(level="datetime").apply(_feature_norm) + cols = df_focus.columns[df_focus.columns.str.contains("^MAX|^HIGH0")] + df_focus[cols] = df_focus[cols].apply(lambda x: (x - 1) ** 0.5).groupby(level="datetime").apply(_feature_norm) - cols = df.columns[df.columns.str.contains("^MIN|^LOW0")] - df_new[cols] = df[cols].apply(lambda x: (1 - x) ** 0.5).groupby(level="datetime").apply(_feature_norm) + cols = df_focus.columns[df_focus.columns.str.contains("^MIN|^LOW0")] + df_focus[cols] = df_focus[cols].apply(lambda x: (1 - x) ** 0.5).groupby(level="datetime").apply(_feature_norm) - cols = df.columns[df.columns.str.contains("^CORR|^CORD")] - df_new[cols] = df[cols].apply(np.exp).groupby(level="datetime").apply(_feature_norm) + cols = df_focus.columns[df_focus.columns.str.contains("^CORR|^CORD")] + df_focus[cols] = df_focus[cols].apply(np.exp).groupby(level="datetime").apply(_feature_norm) - cols = df.columns[df.columns.str.contains("^WVMA")] - df_new[cols] = df[cols].apply(np.log1p).groupby(level="datetime").apply(_feature_norm) + cols = df_focus.columns[df_focus.columns.str.contains("^WVMA")] + df_focus[cols] = df_focus[cols].apply(np.log1p).groupby(level="datetime").apply(_feature_norm) + + df[selected_cols] = df_focus.values TimeInspector.log_cost_time("Finished preprocessing data.") - return df_new + return df diff --git a/qlib/log.py b/qlib/log.py index 7db9ea92d..1f06f87f5 100644 --- a/qlib/log.py +++ b/qlib/log.py @@ -8,6 +8,7 @@ import os import re from logging import config as logging_config from time import time +from contextlib import contextmanager from .config import C @@ -79,6 +80,28 @@ class TimeInspector(object): cost_time = time() - cls.time_marks.pop() cls.timer_logger.info("Time cost: {0:.5f} | {1}".format(cost_time, info)) + @contextmanager + @classmethod + def logt(cls, name="", show_start=False): + """logt. + Log the time of the inside code + + Parameters + ---------- + name : + name + show_start : + show_start + """ + if show_start: + cls.timer_logger.info(f"Begin {name}") + cls.set_time_mark() + try: + yield None + finally: + pass + cls.log_cost_time() + def set_log_with_config(log_config: dict): """set log with config diff --git a/qlib/utils/__init__.py b/qlib/utils/__init__.py index 0e0b76e1c..b10735868 100644 --- a/qlib/utils/__init__.py +++ b/qlib/utils/__init__.py @@ -23,6 +23,7 @@ import contextlib import numpy as np import pandas as pd from pathlib import Path +from typing import Union, Tuple from ..config import C from ..log import get_module_logger @@ -164,6 +165,71 @@ def get_module_by_module_path(module_path): return module +def get_cls_kwargs(config: Union[dict, str], module) -> (type, dict): + """ + extract class and kwargs from config info + + Parameters + ---------- + config : [dict, str] + similar to config + + module : Python module + It should be a python module to load the class type + + Returns + ------- + (type, dict): + the class object and it's arguments. + """ + if isinstance(config, dict): + # raise AttributeError + klass = getattr(module, config['class']) + kwargs = config['kwargs'] + elif isinstance(config, str): + klass = getattr(module, config) + kwargs = {} + else: + raise NotImplementedError(f"This type of input is not supported") + return klass, kwargs + + +def init_instance_by_config(config: Union[str, dict], module=None, accept_types: Tuple[type]=tuple([])) -> object: + """ + get initialized instance with config + + Parameters + ---------- + config : Union[str, dict] + dict example. + { + 'class': 'ClassName', + 'kwargs': dict, # It is optional. {} will be used if not given + 'model_path': path, # It is optional if module is given + } + str example. + "ClassName": getattr(module, config)() will be used. + module : Python module + Optional. It should be a python module. + + accept_types: Tuple[type] + Optional. If the config is a instance of specific type, return the config directly. + + Returns + ------- + object: + An initialized object based on the config info + """ + if isinstance(config, accept_types): + return config + + if module is None: + module = get_module_by_module_path(config["module_path"]) + + klass, kwargs = get_cls_kwargs(config, module) + return klass(**kwargs) + + def compare_dict_value(src_data: dict, dst_data: dict): """Compare dict value From aee507d5ddd2eade265ebe8bd62be868faa98bd5 Mon Sep 17 00:00:00 2001 From: Young Date: Mon, 26 Oct 2020 13:26:01 +0000 Subject: [PATCH 006/241] adjust data and model interface --- examples/workflow_by_code.py | 44 ++++++ qlib/contrib/data/handler.py | 185 ++++++++++++++++++++++- qlib/contrib/data/processor.py | 117 +++++++++++++++ qlib/contrib/online/__init__.py | 18 +++ qlib/data/base.py | 5 +- qlib/data/data.py | 16 +- qlib/data/dataset/__init__.py | 8 + qlib/data/dataset/loader.py | 256 +++++--------------------------- qlib/data/dataset/processor.py | 110 -------------- qlib/data/filter.py | 5 +- qlib/model/base.py | 125 +++------------- qlib/workflow/__init__.py | 0 12 files changed, 431 insertions(+), 458 deletions(-) create mode 100644 qlib/contrib/data/processor.py create mode 100644 qlib/workflow/__init__.py diff --git a/examples/workflow_by_code.py b/examples/workflow_by_code.py index 9f0d5b02f..326e9dee0 100644 --- a/examples/workflow_by_code.py +++ b/examples/workflow_by_code.py @@ -16,6 +16,8 @@ from qlib.contrib.evaluate import ( ) from qlib.utils import exists_qlib_data +from qlib.model.learner import train_model + if __name__ == "__main__": @@ -62,6 +64,48 @@ if __name__ == "__main__": data = handler.fetch(slice('2008-01-01', '2014-12-31'), data_key=handler.DK_I) print(data) + 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, + } + }, + "data": { + "dataset": { + "class": "DatasetH", + "module_path": "qlib.data.dataset", + "kwargs": { + 'handler': { + "class": "Alpha158", + "kwargs": DATA_HANDLER_CONFIG + }, + "train_start_time": "2008-01-01", + "train_end_time": "2014-12-31", + "validate_start_time": "2015-01-01", + "validate_end_time": "2016-12-31", + "test_start_time": "2017-01-01", + "test_end_time": "2020-08-01", + } + } + }, + # You shoud record the data in specific sequence + # "record": ['SignalRecord', 'SigAnaRecord', 'PortAnaRecord'], + } + + model = train_model(task) + + + sys.exit(0) # I have tested the code above --------------------------------------------- x_train, y_train, x_validate, y_validate, x_test, y_test = Alpha158(**DATA_HANDLER_CONFIG).get_split_data( diff --git a/qlib/contrib/data/handler.py b/qlib/contrib/data/handler.py index c9959535a..45e4855c1 100644 --- a/qlib/contrib/data/handler.py +++ b/qlib/contrib/data/handler.py @@ -25,10 +25,12 @@ class ALPHA360(DataHandlerLP): }, "label": self.get_label_config() }, - "group_fields": True, } } - infer_processors = ["ConfigSectionProcessor"] # ConfigSectionProcessor will normalize LABEL0 + infer_processors = [{ + "class": "ConfigSectionProcessor", + "module_path": "qlib.contrib.data.processor" + }] # ConfigSectionProcessor will normalize LABEL0 super().__init__(instruments, start_time, end_time, data_loader=data_loader, infer_processors=infer_processors) def get_label_config(self): @@ -83,7 +85,6 @@ class Alpha158(DataHandlerLP): "feature": self.get_feature_config(), "label": self.get_label_config() }, - "group_fields": True, } } super().__init__(instruments, @@ -94,7 +95,7 @@ class Alpha158(DataHandlerLP): learn_processors=learn_processors) def get_feature_config(self): - return { + conf = { "kbar": {}, "price": { "windows": [0], @@ -102,10 +103,186 @@ class Alpha158(DataHandlerLP): }, "rolling": {}, } + return self.parse_config_to_fields(conf) def get_label_config(self): return (["Ref($close, -2)/Ref($close, -1) - 1"], ["LABEL0"]) + @staticmethod + def parse_config_to_fields(config): + """create factors from config + + config = { + 'kbar': {}, # whether to use some hard-code kbar features + 'price': { # whether to use raw price features + 'windows': [0, 1, 2, 3, 4], # use price at n days ago + 'feature': ['OPEN', 'HIGH', 'LOW'] # which price field to use + }, + 'volume': { # whether to use raw volume features + 'windows': [0, 1, 2, 3, 4], # use volume at n days ago + }, + 'rolling': { # whether to use rolling operator based features + 'windows': [5, 10, 20, 30, 60], # rolling windows size + 'include': ['ROC', 'MA', 'STD'], # rolling operator to use + #if include is None we will use default operators + 'exclude': ['RANK'], # rolling operator not to use + } + } + """ + fields = [] + names = [] + if "kbar" in config: + fields += [ + "($close-$open)/$open", + "($high-$low)/$open", + "($close-$open)/($high-$low+1e-12)", + "($high-Greater($open, $close))/$open", + "($high-Greater($open, $close))/($high-$low+1e-12)", + "(Less($open, $close)-$low)/$open", + "(Less($open, $close)-$low)/($high-$low+1e-12)", + "(2*$close-$high-$low)/$open", + "(2*$close-$high-$low)/($high-$low+1e-12)", + ] + names += [ + "KMID", + "KLEN", + "KMID2", + "KUP", + "KUP2", + "KLOW", + "KLOW2", + "KSFT", + "KSFT2", + ] + if "price" in config: + windows = config["price"].get("windows", range(5)) + feature = config["price"].get("feature", ["OPEN", "HIGH", "LOW", "CLOSE", "VWAP"]) + for field in feature: + field = field.lower() + fields += ["Ref($%s, %d)/$close" % (field, d) if d != 0 else "$%s/$close" % field for d in windows] + names += [field.upper() + str(d) for d in windows] + if "volume" in config: + windows = config["volume"].get("windows", range(5)) + fields += ["Ref($volume, %d)/$volume" % d if d != 0 else "$volume/$volume" for d in windows] + names += ["VOLUME" + str(d) for d in windows] + if "rolling" in config: + windows = config["rolling"].get("windows", [5, 10, 20, 30, 60]) + include = config["rolling"].get("include", None) + exclude = config["rolling"].get("exclude", []) + # `exclude` in dataset config unnecessary filed + # `include` in dataset config necessary field + use = lambda x: x not in exclude and (include is None or x in include) + if use("ROC"): + fields += ["Ref($close, %d)/$close" % d for d in windows] + names += ["ROC%d" % d for d in windows] + if use("MA"): + fields += ["Mean($close, %d)/$close" % d for d in windows] + names += ["MA%d" % d for d in windows] + if use("STD"): + fields += ["Std($close, %d)/$close" % d for d in windows] + names += ["STD%d" % d for d in windows] + if use("BETA"): + fields += ["Slope($close, %d)/$close" % d for d in windows] + names += ["BETA%d" % d for d in windows] + if use("RSQR"): + fields += ["Rsquare($close, %d)" % d for d in windows] + names += ["RSQR%d" % d for d in windows] + if use("RESI"): + fields += ["Resi($close, %d)/$close" % d for d in windows] + names += ["RESI%d" % d for d in windows] + if use("MAX"): + fields += ["Max($high, %d)/$close" % d for d in windows] + names += ["MAX%d" % d for d in windows] + if use("LOW"): + fields += ["Min($low, %d)/$close" % d for d in windows] + names += ["MIN%d" % d for d in windows] + if use("QTLU"): + fields += ["Quantile($close, %d, 0.8)/$close" % d for d in windows] + names += ["QTLU%d" % d for d in windows] + if use("QTLD"): + fields += ["Quantile($close, %d, 0.2)/$close" % d for d in windows] + names += ["QTLD%d" % d for d in windows] + if use("RANK"): + fields += ["Rank($close, %d)" % d for d in windows] + names += ["RANK%d" % d for d in windows] + if use("RSV"): + fields += ["($close-Min($low, %d))/(Max($high, %d)-Min($low, %d)+1e-12)" % (d, d, d) for d in windows] + names += ["RSV%d" % d for d in windows] + if use("IMAX"): + fields += ["IdxMax($high, %d)/%d" % (d, d) for d in windows] + names += ["IMAX%d" % d for d in windows] + if use("IMIN"): + fields += ["IdxMin($low, %d)/%d" % (d, d) for d in windows] + names += ["IMIN%d" % d for d in windows] + if use("IMXD"): + fields += ["(IdxMax($high, %d)-IdxMin($low, %d))/%d" % (d, d, d) for d in windows] + names += ["IMXD%d" % d for d in windows] + if use("CORR"): + fields += ["Corr($close, Log($volume+1), %d)" % d for d in windows] + names += ["CORR%d" % d for d in windows] + if use("CORD"): + fields += ["Corr($close/Ref($close,1), Log($volume/Ref($volume, 1)+1), %d)" % d for d in windows] + names += ["CORD%d" % d for d in windows] + if use("CNTP"): + fields += ["Mean($close>Ref($close, 1), %d)" % d for d in windows] + names += ["CNTP%d" % d for d in windows] + if use("CNTN"): + fields += ["Mean($closeRef($close, 1), %d)-Mean($close= -3, -3 - (x + 3).div(x.min() + 3) * 0.5, inplace=True) + if self.fillna_feature: + x.fillna(0, inplace=True) + return x + + TimeInspector.set_time_mark() + + # Copy the focus part and change it to single level + selected_cols = get_group_columns(df, self.fields_group) + df_focus = df[selected_cols].copy() + if len(df_focus.columns.levels) > 1: + df_focus = df_focus.droplevel(level=0) + + # Label + cols = df_focus.columns[df_focus.columns.str.contains("^LABEL")] + df_focus[cols] = df_focus[cols].groupby(level="datetime").apply(_label_norm) + + # Features + cols = df_focus.columns[df_focus.columns.str.contains("^KLEN|^KLOW|^KUP")] + df_focus[cols] = df_focus[cols].apply(lambda x: x ** 0.25).groupby(level="datetime").apply(_feature_norm) + + cols = df_focus.columns[df_focus.columns.str.contains("^KLOW2|^KUP2")] + df_focus[cols] = df_focus[cols].apply(lambda x: x ** 0.5).groupby(level="datetime").apply(_feature_norm) + + _cols = [ + "KMID", + "KSFT", + "OPEN", + "HIGH", + "LOW", + "CLOSE", + "VWAP", + "ROC", + "MA", + "BETA", + "RESI", + "QTLU", + "QTLD", + "RSV", + "SUMP", + "SUMN", + "SUMD", + "VSUMP", + "VSUMN", + "VSUMD", + ] + pat = "|".join(["^" + x for x in _cols]) + cols = df_focus.columns[df_focus.columns.str.contains(pat) & (~df_focus.columns.isin(["HIGH0", "LOW0"]))] + df_focus[cols] = df_focus[cols].groupby(level="datetime").apply(_feature_norm) + + cols = df_focus.columns[df_focus.columns.str.contains("^STD|^VOLUME|^VMA|^VSTD")] + df_focus[cols] = df_focus[cols].apply(np.log).groupby(level="datetime").apply(_feature_norm) + + cols = df_focus.columns[df_focus.columns.str.contains("^RSQR")] + df_focus[cols] = df_focus[cols].fillna(0).groupby(level="datetime").apply(_feature_norm) + + cols = df_focus.columns[df_focus.columns.str.contains("^MAX|^HIGH0")] + df_focus[cols] = df_focus[cols].apply(lambda x: (x - 1) ** 0.5).groupby(level="datetime").apply(_feature_norm) + + cols = df_focus.columns[df_focus.columns.str.contains("^MIN|^LOW0")] + df_focus[cols] = df_focus[cols].apply(lambda x: (1 - x) ** 0.5).groupby(level="datetime").apply(_feature_norm) + + cols = df_focus.columns[df_focus.columns.str.contains("^CORR|^CORD")] + df_focus[cols] = df_focus[cols].apply(np.exp).groupby(level="datetime").apply(_feature_norm) + + cols = df_focus.columns[df_focus.columns.str.contains("^WVMA")] + df_focus[cols] = df_focus[cols].apply(np.log1p).groupby(level="datetime").apply(_feature_norm) + + df[selected_cols] = df_focus.values + + TimeInspector.log_cost_time("Finished preprocessing data.") + + return df diff --git a/qlib/contrib/online/__init__.py b/qlib/contrib/online/__init__.py index e69de29bb..71389882e 100644 --- a/qlib/contrib/online/__init__.py +++ b/qlib/contrib/online/__init__.py @@ -0,0 +1,18 @@ +''' +TODO: + +- Online needs that the model have such method + def get_data_with_date(self, date, **kwargs): + """ + Will be called in online module + need to return the data that used to predict the label (score) of stocks at date. + + :param + date: pd.Timestamp + predict date + :return: + data: the input data that used to predict the label (score) of stocks at predict date. + """ + raise NotImplementedError("get_data_with_date for this model is not implemented.") + +''' diff --git a/qlib/data/base.py b/qlib/data/base.py index c357700c0..433b6585a 100644 --- a/qlib/data/base.py +++ b/qlib/data/base.py @@ -6,12 +6,10 @@ from __future__ import division from __future__ import print_function import abc -import six import pandas as pd -@six.add_metaclass(abc.ABCMeta) -class Expression(object): +class Expression(abc.ABC): """Expression base class""" def __str__(self): @@ -218,7 +216,6 @@ class Feature(Expression): return 0, 0 -@six.add_metaclass(abc.ABCMeta) class ExpressionOps(Expression): """Operator Expression diff --git a/qlib/data/data.py b/qlib/data/data.py index dc2c5886c..c41d32f6e 100644 --- a/qlib/data/data.py +++ b/qlib/data/data.py @@ -7,7 +7,6 @@ from __future__ import print_function import os import abc -import six import time import queue import bisect @@ -27,8 +26,7 @@ from .base import Feature from .cache import DiskDatasetCache, DiskExpressionCache -@six.add_metaclass(abc.ABCMeta) -class CalendarProvider(object): +class CalendarProvider(abc.ABC): """Calendar provider base class Provide calendar data. @@ -128,8 +126,7 @@ class CalendarProvider(object): return hash_args(start_time, end_time, freq, future) -@six.add_metaclass(abc.ABCMeta) -class InstrumentProvider(object): +class InstrumentProvider(abc.ABC): """Instrument provider base class Provide instrument data. @@ -214,8 +211,7 @@ class InstrumentProvider(object): raise ValueError(f"Unknown instrument type {inst}") -@six.add_metaclass(abc.ABCMeta) -class FeatureProvider(object): +class FeatureProvider(abc.ABC): """Feature provider class Provide feature data. @@ -246,8 +242,7 @@ class FeatureProvider(object): raise NotImplementedError("Subclass of FeatureProvider must implement `feature` method") -@six.add_metaclass(abc.ABCMeta) -class ExpressionProvider(object): +class ExpressionProvider(abc.ABC): """Expression provider class Provide Expression data. @@ -298,8 +293,7 @@ class ExpressionProvider(object): raise NotImplementedError("Subclass of ExpressionProvider must implement `Expression` method") -@six.add_metaclass(abc.ABCMeta) -class DatasetProvider(object): +class DatasetProvider(abc.ABC): """Dataset provider class Provide Dataset data. diff --git a/qlib/data/dataset/__init__.py b/qlib/data/dataset/__init__.py index e69de29bb..ec6cb2c4b 100644 --- a/qlib/data/dataset/__init__.py +++ b/qlib/data/dataset/__init__.py @@ -0,0 +1,8 @@ + +class Dataset: + ''' + Preparing data for model training. + The type of dataset depends on the model. (It could be pd.DataFrame, pytorch.DataLoader, etc.) + ''' + def generate(self): + pass diff --git a/qlib/data/dataset/loader.py b/qlib/data/dataset/loader.py index 7da042b7c..b94280a83 100644 --- a/qlib/data/dataset/loader.py +++ b/qlib/data/dataset/loader.py @@ -16,6 +16,17 @@ class DataLoader(ABC): """ load the data as pd.DataFrame + Parameters + ---------- + self : [TODO:type] + [TODO:description] + instruments : [TODO:type] + [TODO:description] + start_time : [TODO:type] + [TODO:description] + end_time : [TODO:type] + [TODO:description] + Returns ------- pd.DataFrame: @@ -35,240 +46,51 @@ class DataLoader(ABC): class QlibDataLoader(DataLoader): '''Same as QlibDataLoader. The fields can be define by config''' - def __init__(self, config: Tuple[list, tuple, dict], group_fields: bool = False, filter_pipe=None): + def __init__(self, config: Tuple[list, tuple, dict], filter_pipe=None): """ Parameters ---------- config : Tuple[list ,tuple, dict] Config will be used to describe the fields and column names - if `group_fields`: - := { - "group_name1": - "group_name2": - } - else: - := + := { + "group_name1": + "group_name2": + } - := ["expr", ...] | (["expr", ...], ["col_name", ...]) | + := - is a config with dict type which could be parsed by `parse_config_to_fields` + := ["expr", ...] | (["expr", ...], ["col_name", ...]) - Here is a few examples to describe the fields + Here is a few examples to describe the fields TODO: - - group_fields : bool - Will the fields be grouped. Multi-index will be used for the group """ - if group_fields: - fields_all = [] - name_grp_info = [] - for grp, fields_info in config.items(): - fields, names = self._parse_fields_info(fields_info) - fields_all.extend(fields) - name_grp_info.extend([(grp, n) for n in names]) - self.fields, self.names = fields_all, name_grp_info - else: - self.fields, self.names = self._parse_fields_info(fields_info) + self.is_group = isinstance(config, dict) + + if self.is_group: + self.fields = {grp: self._parse_fields_info(fields_info) for grp, fields_info in config.items()} + else: + self.fields = self._parse_fields_info(fields_info) - self.group_fields = group_fields self.filter_pipe = filter_pipe - def _parse_fields_info(self, fields_info: Tuple[list, tuple, dict]) -> Tuple[list, list]: - if isinstance(fields_info, dict): - fields, names = parse_config_to_fields(fields_info) - elif isinstance(fields_info, list): - fields = fields_info - names = fields + def _parse_fields_info(self, fields_info: Tuple[list, tuple]) -> Tuple[list, list]: + if isinstance(fields_info, list): + exprs = names = fields_info elif isinstance(fields_info, tuple): - fields, names = fields_info + exprs, names = fields_info else: raise NotImplementedError(f"This type of input is not supported") - return fields, names + return exprs, names - def load(self, - instruments, - config: Tuple[list, tuple, dict], - group_fields=False, - start_time=None, - end_time=None) -> Tuple[pd.DataFrame, dict]: - df = D.features(D.instruments(instruments, filter_pipe=self.filter_pipe), self.fields, start_time, end_time) - df.columns = pd.MultiIndex.from_tuples(self.names) if self.group_fields else self.names + def load(self, instruments, start_time=None, end_time=None) -> pd.DataFrame: + def _get_df(exprs, names): + df = D.features(D.instruments(instruments, filter_pipe=self.filter_pipe), exprs, start_time, end_time) + df.columns = names + return df + if self.is_group: + df = pd.concat({grp: _get_df(exprs, names) for grp, (exprs, names) in self.fields.items()}, axis=1) + else: + df = _get_df(exprs, names) df = df.swaplevel().sort_index() return df - - -# TODO: make it easier to understand the config language -def parse_config_to_fields(config): - """create factors from config - - config = { - 'kbar': {}, # whether to use some hard-code kbar features - 'price': { # whether to use raw price features - 'windows': [0, 1, 2, 3, 4], # use price at n days ago - 'feature': ['OPEN', 'HIGH', 'LOW'] # which price field to use - }, - 'volume': { # whether to use raw volume features - 'windows': [0, 1, 2, 3, 4], # use volume at n days ago - }, - 'rolling': { # whether to use rolling operator based features - 'windows': [5, 10, 20, 30, 60], # rolling windows size - 'include': ['ROC', 'MA', 'STD'], # rolling operator to use - #if include is None we will use default operators - 'exclude': ['RANK'], # rolling operator not to use - } - } - """ - fields = [] - names = [] - if "kbar" in config: - fields += [ - "($close-$open)/$open", - "($high-$low)/$open", - "($close-$open)/($high-$low+1e-12)", - "($high-Greater($open, $close))/$open", - "($high-Greater($open, $close))/($high-$low+1e-12)", - "(Less($open, $close)-$low)/$open", - "(Less($open, $close)-$low)/($high-$low+1e-12)", - "(2*$close-$high-$low)/$open", - "(2*$close-$high-$low)/($high-$low+1e-12)", - ] - names += [ - "KMID", - "KLEN", - "KMID2", - "KUP", - "KUP2", - "KLOW", - "KLOW2", - "KSFT", - "KSFT2", - ] - if "price" in config: - windows = config["price"].get("windows", range(5)) - feature = config["price"].get("feature", ["OPEN", "HIGH", "LOW", "CLOSE", "VWAP"]) - for field in feature: - field = field.lower() - fields += ["Ref($%s, %d)/$close" % (field, d) if d != 0 else "$%s/$close" % field for d in windows] - names += [field.upper() + str(d) for d in windows] - if "volume" in config: - windows = config["volume"].get("windows", range(5)) - fields += ["Ref($volume, %d)/$volume" % d if d != 0 else "$volume/$volume" for d in windows] - names += ["VOLUME" + str(d) for d in windows] - if "rolling" in config: - windows = config["rolling"].get("windows", [5, 10, 20, 30, 60]) - include = config["rolling"].get("include", None) - exclude = config["rolling"].get("exclude", []) - # `exclude` in dataset config unnecessary filed - # `include` in dataset config necessary field - use = lambda x: x not in exclude and (include is None or x in include) - if use("ROC"): - fields += ["Ref($close, %d)/$close" % d for d in windows] - names += ["ROC%d" % d for d in windows] - if use("MA"): - fields += ["Mean($close, %d)/$close" % d for d in windows] - names += ["MA%d" % d for d in windows] - if use("STD"): - fields += ["Std($close, %d)/$close" % d for d in windows] - names += ["STD%d" % d for d in windows] - if use("BETA"): - fields += ["Slope($close, %d)/$close" % d for d in windows] - names += ["BETA%d" % d for d in windows] - if use("RSQR"): - fields += ["Rsquare($close, %d)" % d for d in windows] - names += ["RSQR%d" % d for d in windows] - if use("RESI"): - fields += ["Resi($close, %d)/$close" % d for d in windows] - names += ["RESI%d" % d for d in windows] - if use("MAX"): - fields += ["Max($high, %d)/$close" % d for d in windows] - names += ["MAX%d" % d for d in windows] - if use("LOW"): - fields += ["Min($low, %d)/$close" % d for d in windows] - names += ["MIN%d" % d for d in windows] - if use("QTLU"): - fields += ["Quantile($close, %d, 0.8)/$close" % d for d in windows] - names += ["QTLU%d" % d for d in windows] - if use("QTLD"): - fields += ["Quantile($close, %d, 0.2)/$close" % d for d in windows] - names += ["QTLD%d" % d for d in windows] - if use("RANK"): - fields += ["Rank($close, %d)" % d for d in windows] - names += ["RANK%d" % d for d in windows] - if use("RSV"): - fields += ["($close-Min($low, %d))/(Max($high, %d)-Min($low, %d)+1e-12)" % (d, d, d) for d in windows] - names += ["RSV%d" % d for d in windows] - if use("IMAX"): - fields += ["IdxMax($high, %d)/%d" % (d, d) for d in windows] - names += ["IMAX%d" % d for d in windows] - if use("IMIN"): - fields += ["IdxMin($low, %d)/%d" % (d, d) for d in windows] - names += ["IMIN%d" % d for d in windows] - if use("IMXD"): - fields += ["(IdxMax($high, %d)-IdxMin($low, %d))/%d" % (d, d, d) for d in windows] - names += ["IMXD%d" % d for d in windows] - if use("CORR"): - fields += ["Corr($close, Log($volume+1), %d)" % d for d in windows] - names += ["CORR%d" % d for d in windows] - if use("CORD"): - fields += ["Corr($close/Ref($close,1), Log($volume/Ref($volume, 1)+1), %d)" % d for d in windows] - names += ["CORD%d" % d for d in windows] - if use("CNTP"): - fields += ["Mean($close>Ref($close, 1), %d)" % d for d in windows] - names += ["CNTP%d" % d for d in windows] - if use("CNTN"): - fields += ["Mean($closeRef($close, 1), %d)-Mean($close= -3, -3 - (x + 3).div(x.min() + 3) * 0.5, inplace=True) - if self.fillna_feature: - x.fillna(0, inplace=True) - return x - - TimeInspector.set_time_mark() - - # Copy the focus part and change it to single level - selected_cols = get_group_columns(df, self.fields_group) - df_focus = df[selected_cols].copy() - if len(df_focus.columns.levels) > 1: - df_focus = df_focus.droplevel(level=0) - - # Label - cols = df_focus.columns[df_focus.columns.str.contains("^LABEL")] - df_focus[cols] = df_focus[cols].groupby(level="datetime").apply(_label_norm) - - # Features - cols = df_focus.columns[df_focus.columns.str.contains("^KLEN|^KLOW|^KUP")] - df_focus[cols] = df_focus[cols].apply(lambda x: x ** 0.25).groupby(level="datetime").apply(_feature_norm) - - cols = df_focus.columns[df_focus.columns.str.contains("^KLOW2|^KUP2")] - df_focus[cols] = df_focus[cols].apply(lambda x: x ** 0.5).groupby(level="datetime").apply(_feature_norm) - - _cols = [ - "KMID", - "KSFT", - "OPEN", - "HIGH", - "LOW", - "CLOSE", - "VWAP", - "ROC", - "MA", - "BETA", - "RESI", - "QTLU", - "QTLD", - "RSV", - "SUMP", - "SUMN", - "SUMD", - "VSUMP", - "VSUMN", - "VSUMD", - ] - pat = "|".join(["^" + x for x in _cols]) - cols = df_focus.columns[df_focus.columns.str.contains(pat) & (~df_focus.columns.isin(["HIGH0", "LOW0"]))] - df_focus[cols] = df_focus[cols].groupby(level="datetime").apply(_feature_norm) - - cols = df_focus.columns[df_focus.columns.str.contains("^STD|^VOLUME|^VMA|^VSTD")] - df_focus[cols] = df_focus[cols].apply(np.log).groupby(level="datetime").apply(_feature_norm) - - cols = df_focus.columns[df_focus.columns.str.contains("^RSQR")] - df_focus[cols] = df_focus[cols].fillna(0).groupby(level="datetime").apply(_feature_norm) - - cols = df_focus.columns[df_focus.columns.str.contains("^MAX|^HIGH0")] - df_focus[cols] = df_focus[cols].apply(lambda x: (x - 1) ** 0.5).groupby(level="datetime").apply(_feature_norm) - - cols = df_focus.columns[df_focus.columns.str.contains("^MIN|^LOW0")] - df_focus[cols] = df_focus[cols].apply(lambda x: (1 - x) ** 0.5).groupby(level="datetime").apply(_feature_norm) - - cols = df_focus.columns[df_focus.columns.str.contains("^CORR|^CORD")] - df_focus[cols] = df_focus[cols].apply(np.exp).groupby(level="datetime").apply(_feature_norm) - - cols = df_focus.columns[df_focus.columns.str.contains("^WVMA")] - df_focus[cols] = df_focus[cols].apply(np.log1p).groupby(level="datetime").apply(_feature_norm) - - df[selected_cols] = df_focus.values - - TimeInspector.log_cost_time("Finished preprocessing data.") - - return df diff --git a/qlib/data/filter.py b/qlib/data/filter.py index 3a36b1678..47b093b67 100644 --- a/qlib/data/filter.py +++ b/qlib/data/filter.py @@ -7,14 +7,12 @@ from abc import abstractmethod import re import pandas as pd import numpy as np -import six import abc from .data import Cal, DatasetD -@six.add_metaclass(abc.ABCMeta) -class BaseDFilter(object): +class BaseDFilter(abc.ABC): """Dynamic Instruments Filter Abstract class Users can override this class to construct their own filter @@ -50,7 +48,6 @@ class BaseDFilter(object): raise NotImplementedError("Subclass of BaseDFilter must reimplement `to_config` method") -@six.add_metaclass(abc.ABCMeta) class SeriesDFilter(BaseDFilter): """Dynamic Instruments Filter Abstract class to filter a series of certain features diff --git a/qlib/model/base.py b/qlib/model/base.py index b3ea917a5..66b54705a 100644 --- a/qlib/model/base.py +++ b/qlib/model/base.py @@ -1,22 +1,26 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. - - -from __future__ import division -from __future__ import print_function - import abc -import six +from ..utils.serial import Serializable -@six.add_metaclass(abc.ABCMeta) -class Model(object): - """Model base class""" +class BaseModel(Serializable, metaclass=abc.ABCMeta): + '''Modeling things''' - @property - def name(self): - return type(self).__name__ + @abc.abstractmethod + def predict(self, *args, **kwargs) -> object: + """ Make predictions after modeling things """ + pass + def __call__(self, *args, **kwargs) -> object: + """ levarge Python syntactic sugar to make the models' behaviors like functions """ + return self.predict(*args, **kwargs) + + +class Model(BaseModel): + '''Learnable Models''' + + # TODO: Make the model easier. def fit(self, x_train, y_train, x_valid, y_valid, w_train=None, w_valid=None, **kwargs): """fix train with cross-validation Fit model when ex_config.finetune is False @@ -43,25 +47,7 @@ class Model(object): """ raise NotImplementedError() - def score(self, x_test, y_test, w_test=None, **kwargs): - """evaluate model with test data/label - - Parameters - ---------- - x_test : pd.dataframe - test data - y_test : pd.dataframe - test label - w_test : pd.dataframe - test weight - - Returns - ---------- - float - evaluation score - """ - raise NotImplementedError() - + @abc.abstractmethod def predict(self, x_test, **kwargs): """predict given test data @@ -76,80 +62,3 @@ class Model(object): test predict label """ raise NotImplementedError() - - def save(self, fname, **kwargs): - """save model - - Parameters - ---------- - fname : str - model filename - """ - # TODO: Currently need to save the model as a single file, otherwise the estimator may not be compatible - raise NotImplementedError() - - def load(self, buffer, **kwargs): - """load model - - Parameters - ---------- - buffer : bytes - binary data of model parameters - - Returns - ---------- - Model - loaded model - """ - raise NotImplementedError() - - def get_data_with_date(self, date, **kwargs): - """ - Will be called in online module - need to return the data that used to predict the label (score) of stocks at date. - - :param - date: pd.Timestamp - predict date - :return: - data: the input data that used to predict the label (score) of stocks at predict date. - """ - raise NotImplementedError("get_data_with_date for this model is not implemented.") - - def finetune(self, x_train, y_train, x_valid, y_valid, w_train=None, w_valid=None, **kwargs): - """Finetune model - In `RollingTrainer`: - if loader.model_index is None: - If provide 'Static Model', based on the provided 'Static' model update. - If provide 'Rolling Model', skip the model of load, based on the last 'provided model' update. - - if loader.model_index is not None: - Based on the provided model(loader.model_index) update. - - In `StaticTrainer`: - If the load is 'static model': - Based on the 'static model' update - If the load is 'rolling model': - Based on the provided model(`loader.model_index`) update. If `loader.model_index` is None, use the last model. - - Parameters - ---------- - x_train : pd.dataframe - train data - y_train : pd.dataframe - train label - x_valid : pd.dataframe - valid data - y_valid : pd.dataframe - valid label - w_train : pd.dataframe - train weight - w_valid : pd.dataframe - valid weight - - Returns - ---------- - Model - finetune model - """ - raise NotImplementedError("Finetune for this model is not implemented.") diff --git a/qlib/workflow/__init__.py b/qlib/workflow/__init__.py new file mode 100644 index 000000000..e69de29bb From 1a9ee6cef89c7ddf7c3813437f47c8f14ae0070d Mon Sep 17 00:00:00 2001 From: Jactus Date: Tue, 27 Oct 2020 14:30:24 +0800 Subject: [PATCH 007/241] Add Recorder and ExpManager --- qlib/workflow/__init__.py | 157 +++++++++++++++++ qlib/workflow/exp.py | 265 +++++++++++++++++++++++++++++ qlib/workflow/record.py | 343 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 765 insertions(+) create mode 100644 qlib/workflow/exp.py create mode 100644 qlib/workflow/record.py diff --git a/qlib/workflow/__init__.py b/qlib/workflow/__init__.py index e69de29bb..76d5e7d4c 100644 --- a/qlib/workflow/__init__.py +++ b/qlib/workflow/__init__.py @@ -0,0 +1,157 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +from contextlib import contextmanager +from .record import MLflowRecorder +from .exp import MLflowExpManager + +class Record: + def __init__(self): + pass + + @contextmanager + def start_exp(self, experiment_name=None, uri=None, project_path=None, artifact_location=None, nested=False): + raise NotImplementedError(f"Please implement the `start_exp` method.") + + def search_runs(self, experiment_ids=None, filter_string='', run_view_type=1, max_results=100000, order_by=None): + raise NotImplementedError(f"Please implement the `search_runs` method.") + + def get_exp(self, experiment_id): + raise NotImplementedError(f"Please implement the `get_exp` method.") + + def get_exp_by_name(self, experiment_name): + raise NotImplementedError(f"Please implement the `get_exp_by_name` method.") + + def create_exp(self, experiment_name, artifact_location=None): + raise NotImplementedError(f"Please implement the `create_exp` method.") + + def set_exp(self, experiment_name): + raise NotImplementedError(f"Please implement the `set_exp` method.") + + def delete_exp(self, experiment_id): + raise NotImplementedError(f"Please implement the `create_exp` method.") + + def set_tracking_uri(self, uri): + raise NotImplementedError(f"Please implement the `set_tracking_uri` method.") + + def get_tracking_uri(self): + raise NotImplementedError(f"Please implement the `get_tracking_uri` method.") + + def get_recorder(self): + raise NotImplementedError(f"Please implement the `get_recorder` method.") + + def save_object(self, name, data): + raise NotImplementedError(f"Please implement the `save_object` method.") + + def save_objects(self, name_data_list): + raise NotImplementedError(f"Please implement the `save_objects` method.") + + def load_object(self, name): + raise NotImplementedError(f"Please implement the `load_object` method.") + + def log_param(self, key, value): + raise NotImplementedError(f"Please implement the `log_param` method.") + + def log_params(self, params): + raise NotImplementedError(f"Please implement the `log_params` method.") + + def log_metric(self, key, value, step=None): + raise NotImplementedError(f"Please implement the `log_metric` method.") + + def log_metrics(self, metrics, step=None): + raise NotImplementedError(f"Please implement the `log_metrics` method.") + + def set_tag(self, key, value): + raise NotImplementedError(f"Please implement the `set_tag` method.") + + def set_tags(self, tags): + raise NotImplementedError(f"Please implement the `log_tags` method.") + + def delete_tag(self, key): + raise NotImplementedError(f"Please implement the `delete_tag` method.") + + def log_artifact(self, local_path, artifact_path=None): + raise NotImplementedError(f"Please implement the `log_artifact` method.") + + def log_artifacts(self, local_dir, artifact_path=None): + raise NotImplementedError(f"Please implement the `log_artifacts` method.") + + def get_artifact_uri(self, artifact_path=None): + raise NotImplementedError(f"Please implement the `get_artifact_uri` method.") + +class MLflowRecord(Record): + def __init__(self): + self.exp_manager = MLflowExpManager() + + @contextmanager + def start_exp(self, experiment_name=None, uri=None, project_path=None, artifact_location=None, nested=False): + yield self.exp_manager.start_exp(experiment_name, uri, project_path, artifact_location, nested) + + def search_runs(self, experiment_ids=None, filter_string='', run_view_type=1, max_results=100000, order_by=None): + return self.exp_manager.search_runs(experiment_ids, filter_string, run_view_type, max_results, order_by) + + def get_exp(self, experiment_id): + return self.exp_manager.get_exp(experiment_id) + + def get_exp_by_name(self, experiment_name): + return self.exp_manager.get_exp_by_name(experiment_name) + + def create_exp(self, experiment_name, artifact_location=None): + self.exp_manager.create_exp(experiment_name, artifact_location) + + def set_exp(self, experiment_name): + self.exp_manager.set_exp(experiment_name) + + def delete_exp(self, experiment_id): + self.exp_manager.delete_exp(experiment_id) + + def set_tracking_uri(self, uri): + self.exp_manager.set_tracking_uri(uri) + + def get_tracking_uri(self): + return self.exp_manager.get_tracking_uri() + + def get_recorder(self): + return self.exp_manager.get_recorder() + + def save_object(self, name, data): + self.exp_manager.active_recorder.save_object(name, data) + + def save_objects(self, name_data_list): + self.exp_manager.active_recorder.save_objects(name_data_list) + + def load_object(self, name): + return self.exp_manager.active_recorder.load_object(name) + + def log_param(self, key, value): + self.exp_manager.active_recorder.log_param(key, value) + + def log_params(self, params): + self.exp_manager.active_recorder.log_params(params) + + def log_metric(self, key, value, step=None): + self.exp_manager.active_recorder.log_metric(key, value, step) + + def log_metrics(self, metrics, step=None): + self.exp_manager.active_recorder.log_metrics(metrics, step) + + def set_tag(self, key, value): + self.exp_manager.active_recorder.set_tag(key, value) + + def set_tags(self, tags): + self.exp_manager.active_recorder.set_tags(tags) + + def delete_tag(self, key): + self.exp_manager.active_recorder.delete_tag(key) + + def log_artifact(self, local_path, artifact_path=None): + self.exp_manager.active_recorder.log_artifact(local_path, artifact_path) + + def log_artifacts(self, local_dir, artifact_path=None): + self.exp_manager.active_recorder.log_artifacts(local_dir, artifact_path) + + def get_artifact_uri(self, artifact_path=None): + return self.exp_manager.active_recorder.get_artifact_uri(artifact_path) + +# global record +R = MLflowRecord() \ No newline at end of file diff --git a/qlib/workflow/exp.py b/qlib/workflow/exp.py new file mode 100644 index 000000000..f3cedea90 --- /dev/null +++ b/qlib/workflow/exp.py @@ -0,0 +1,265 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +import mlflow +from contextlib import contextmanager +from .record import MLflowRecorder + +class ExpManager: + """ + This is the `ExpManager` class for managing the experiments. The API is designed similar to mlflow. + (The link: https://mlflow.org/docs/latest/python_api/mlflow.html) + """ + def __init__(self): + self.active_recorder = None + self.experiments = dict() # store the experiment names -> list of recorders. + self.exp_ids = list() + + def _store_exp(self, id, name): + """ + Store the experiments in the experiments holder. + """ + if id in self.exp_ids: + raise Exception('Something went wrong when creating the experiment. Please check if the experiment is already created.') + if name in self.experiments: + assert int(id) == int(self.experiments[name][0]), 'Experiment id and name are not consistent when storing the experiment.' + else: + self.exp_ids.append(id) + self.experiments[name] = [id] + + def start_exp(self, project_path, experiment_name=None, uri=None, artifact_location=None, nested=False): + """ + Start running an experiment. This method can only work in the `with` statement. + + Parameters + ---------- + project_path : str + path for the project. + experiment_name : str + name of the active experiment. + uri : str + the current tracking URI. + artifact_location : str + the location to store all the artifacts. + nested : boolean + controls whether run is nested in parent run. + + Returns + None + """ + raise NotImplementedError(f"Please implement the `start_exp` method.") + + def end_exp(self): + """ + End an active experiment. + """ + raise NotImplementedError(f"Please implement the `end_exp` method.") + + def search_runs(self, experiment_ids=None, filter_string='', run_view_type=1, max_results=100000, order_by=None): + """ + Get a pandas DataFrame of runs that fit the search criteria. + + Parameters + ---------- + experiment_ids : list + list of experiment IDs. + filter_string : str + filter query string, defaults to searching all runs. + run_view_type : int + one of enum values ACTIVE_ONLY, DELETED_ONLY, or ALL (e.g. in mlflow.entities.ViewType). + max_results : int + the maximum number of runs to put in the dataframe. + order_by : list + list of columns to order by (e.g., “metrics.rmse”). + + Returns + ------- + A pandas.DataFrame of runs. + """ + raise NotImplementedError(f"Please implement the `search_runs` method.") + + def get_exp(self, experiment_id): + """ + Retrieve an experiment by experiment_id from the backend store. + + Parameters + ---------- + experiment_id : str + the experiment id to return. + + Returns + ------- + An experiment object (e.g. mlflow.entities.Experiment). + """ + raise NotImplementedError(f"Please implement the `get_exp` method.") + + def get_exp_by_name(self, experiment_name): + """ + Retrieve an experiment by experiment name from the backend store. + + Parameters + ---------- + experiment_name : str + the experiment name to return. + + Returns + ------- + An experiment object (e.g. mlflow.entities.Experiment). + """ + raise NotImplementedError(f"Please implement the `get_exp_by_name` method.") + + def create_exp(self, experiment_name, artifact_location=None): + """ + Create an experiment. + + Parameters + ---------- + experiment_name : str + the experiment name, which must be unique. + artifact_location : str + the location to store run artifacts. + + Returns + ------- + String id of created experiment. + """ + raise NotImplementedError(f"Please implement the `create_exp` method.") + + def set_exp(self, experiment_name): + """ + Set the experiment to be active. + + Parameters + ---------- + experiment_name : str + the experiment name, which must be unique. + + Returns + ------- + String id of created experiment. + """ + raise NotImplementedError(f"Please implement the `set_exp` method.") + + def delete_exp(self, experiment_id): + """ + Delete an experiment. + + Parameters + ---------- + experiment_id : str + the experiment id. + + Returns + ------- + None + """ + raise NotImplementedError(f"Please implement the `create_exp` method.") + + def set_tracking_uri(self, uri): + """ + Set the tracking server URI. + + Parameters + ---------- + uri : str + the uri of the tracking server, can be An empty string, or a local file path, prefixed with file:/. + or An HTTP URI or A Databricks workspace. + Returns + ------- + None + """ + raise NotImplementedError(f"Please implement the `set_tracking_uri` method.") + + def get_tracking_uri(self): + """ + Get the tracking server URI. + + Parameters + ---------- + + Returns + ------- + The tracking URI. + """ + raise NotImplementedError(f"Please implement the `get_tracking_uri` method.") + + def get_recorder(self): + """ + Get the current active Recorder. + + Parameters + ---------- + + Returns + ------- + An Recorder object. + """ + raise NotImplementedError(f"Please implement the `get_recorder` method.") + + +class MLflowExpManager(ExpManager): + ''' + Use mlflow to implement ExpManager. + ''' + def start_exp(self, experiment_name=None, uri=None, project_path=None, artifact_location=None, nested=False): + # set the tracking uri + if uri is None: + assert project_path is not None, "Please provide the project_path if no uri is provided in order to set a proper tracking uri." + print('No tracking URI is provided. The default tracking URI is set as `mlruns` under the project path.') + mlflow.set_tracking_uri(str(project_path / "mlruns")) + else: + mlflow.set_tracking_uri(uri) + # start the experiment + if experiment_name is None: + print('No experiment name provided. The default experiment name is set as `experiment`.') + experiment_id = self.create_exp('experiment', artifact_location) + # set the active experiment + self.set_exp('experiment') + experiment_name = 'experiment' + else: + if experiment_name not in self.experiments: + if self.get_exp_by_name(experiment_name) is not None: + raise Exception('The experiment has already been created before. Please pick another name or delete the files under tracking uri.') + experiment_id = self.create_exp(experiment_name, artifact_location) + else: + experiment_id = self.experiments(experiment_name)[0] + # set the active experiment + self.set_exp(experiment_name) + + # store the id and name + self._store_exp(experiment_id, experiment_name) + # set up recorder + recorder = MLflowRecorder(experiment_id) + self.active_recorder = recorder + # store the recorder + self.experiments[experiment_name].append(self.active_recorder) + + return self.active_recorder.start_run(experiment_id=experiment_id, nested=nested) + + def search_runs(self, experiment_ids=None, filter_string='', run_view_type=1, max_results=100000, order_by=None): + return mlflow.search_runs(experiment_ids, filter_string, run_view_type, max_results, order_by) + + def get_exp(self, experiment_id): + return mlflow.get_experiment(experiment_id) + + def get_exp_by_name(self, experiment_name): + return mlflow.get_experiment_by_name(experiment_name) + + def create_exp(self, experiment_name, artifact_location=None): + return mlflow.create_experiment(experiment_name, artifact_location) + + def set_exp(self, experiment_name): + mlflow.set_experiment(experiment_name) + + def delete_exp(self, experiment_id): + mlflow.delete_experiment(experiment_id) + self.experiments = {key:val for key, val in self.experiments.items() if val[0] != experiment_id} + + def set_tracking_uri(self, uri): + mlflow.set_tracking_uri(uri) + + def get_tracking_uri(self): + return mlflow.get_tracking_uri() + + def get_recorder(self): + return self.active_recorder \ No newline at end of file diff --git a/qlib/workflow/record.py b/qlib/workflow/record.py new file mode 100644 index 000000000..7895cf0fb --- /dev/null +++ b/qlib/workflow/record.py @@ -0,0 +1,343 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +import mlflow +import shutil +from pathlib import Path +from ..utils.objm import FileManager + +class Recorder: + """ + This is the `Recorder` class for logging the experiments. The API is designed similar to mlflow. + (The link: https://mlflow.org/docs/latest/python_api/mlflow.html) + """ + + def __init__(self, experiment_id, project_path=None): + self.experiment_id = experiment_id + self.recorder_id = None + self.recorder_name = None + self.fm = None + self.artifact_uri = None + + def set_recorder_name(self, rname): + self.recorder_name = rname + + def save_object(self, name, data): + """ + Save object such as prediction file or model checkpoints. + + Parameters + ---------- + name : str + name of the file to be saved. + data : any type + the data to be saved. + + Returns + ------- + None. + """ + raise NotImplementedError(f"Please implement the `save_object` method.") + + def save_objects(self, name_data_list): + """ + Save objects such as prediction file or model checkpoints. + + Parameters + ---------- + name_data_list : list + list of (name, data) pairs + + Returns + ------- + None. + """ + raise NotImplementedError(f"Please implement the `save_objects` method.") + + def load_object(self, name): + """ + Load objects such as prediction file or model checkpoints. + + Parameters + ---------- + name : str + name of the file to be loaded. + + Returns + ------- + The saved object. + """ + raise NotImplementedError(f"Please implement the `load_object` method.") + + def start_run(self, run_id=None, experiment_id=None, + run_name=None, nested=False): + """ + Start running the Recorder. The return value can be used as a context manager within a `with` block; + otherwise, you must call end_run() to terminate the current run. (See `ActiveRun` class in mlflow) + + Parameters + ---------- + run_id : str + id of the active Recorder. + experiment_id : str + id of the active experiment. + run_name : str + name of the Recorder. + nested : boolean + controls whether run is nested in parent run. + + Returns + ------- + An active running object (e.g. mlflow.ActiveRun object). + """ + raise NotImplementedError(f"Please implement the `start_run` method.") + + def end_run(self): + """ + End an active Recorder. + """ + raise NotImplementedError(f"Please implement the `end_run` method.") + + def log_param(self, key, value): + """ + Log a parameter under the current run. + + Parameters + ---------- + key : str + the name of the parameter + value : str + the value of the parameter + + Returns + ------- + None + """ + raise NotImplementedError(f"Please implement the `log_param` method.") + + def log_params(self, params): + """ + Log a batch of params for the current run. + + Parameters + ---------- + params : dict + dictionary of param_name: String -> value: String. + + Returns + ------- + None + """ + raise NotImplementedError(f"Please implement the `log_params` method.") + + def log_metric(self, key, value, step=None): + """ + Log a metric under the current run. + + Parameters + ---------- + key : str + the name of the metric + value : float + the value of the metric + + Returns + ------- + None + """ + raise NotImplementedError(f"Please implement the `log_metric` method.") + + def log_metrics(self, metrics, step=None): + """ + Log multiple metrics for the current run. + + Parameters + ---------- + metrics : dict + dictionary of metric_name: String -> value: Float. + + Returns + ------- + None + """ + raise NotImplementedError(f"Please implement the `log_metrics` method.") + + def set_tag(self, key, value): + """ + Set a tag under the current run. + + Parameters + ---------- + key : str + the name of the tag + value : str + the value of the tag + + Returns + ------- + None + """ + raise NotImplementedError(f"Please implement the `set_tag` method.") + + def set_tags(self, tags): + """ + Log a batch of tags for the current run. + + Parameters + ---------- + tags : dict + dictionary of tag_name: String -> value: String. + + Returns + ------- + None + """ + raise NotImplementedError(f"Please implement the `log_tags` method.") + + def delete_tag(self, key): + """ + Delete a tag from a run. + + Parameters + ---------- + key : str + the name of the tag to be deleted. + + Returns + ------- + None + """ + raise NotImplementedError(f"Please implement the `delete_tag` method.") + + def log_artifact(self, local_path, artifact_path=None): + """ + Log a local file or directory as an artifact of the currently active run. + + Parameters + ---------- + local_path : str + path to the file to write. + artifact_path : str + the directory in `artifact_uri` to write to. + + Returns + ------- + None + """ + raise NotImplementedError(f"Please implement the `log_artifact` method.") + + def log_artifacts(self, local_dir, artifact_path=None): + """ + Log all the contents of a local directory as artifacts of the run. + + Parameters + ---------- + local_dir : str + path to the directory of files to write. + artifact_path : str + the directory in `artifact_uri` to write to. + + Returns + ------- + None + """ + raise NotImplementedError(f"Please implement the `log_artifacts` method.") + + def get_artifact_uri(self, artifact_path=None): + """ + Get the absolute URI of the specified artifact in the currently active run. + + Parameters + ---------- + artifact_path : str + the directory in `artifact_uri` to write to. + + Returns + ------- + An absolute URI referring to the specified artifact or currently active Recorder. + """ + raise NotImplementedError(f"Please implement the `get_artifact_uri` method.") + + +class MLflowRecorder(Recorder): + ''' + Use mlflow to implement a Recorder. + ''' + def start_run(self, run_id=None, experiment_id=None, + run_name=None, nested=False): + if run_id is None: + run_id = self.recorder_id + if experiment_id is None: + experiment_id = self.experiment_id + if run_name is None: + run_name = self.recorder_name + # start the run + run = mlflow.start_run(run_id, experiment_id, run_name, nested) + # save the run id and artifact_uri + self.recorder_id = run.info.run_id + self.artifact_uri = run.info.artifact_uri + # set up file manager for saving objects + if self.artifact_uri.startswith('file:/'): + self.fm = FileManager(Path(urllib.parse.urlparse(self.artifact_uri).path)) + else: + self.fm = FileManager(Path(self.artifact_uri)) + print(self.artifact_uri) + return run + + def end_run(self): + mlflow.end_run() + + def save_object(self, name, data): + self.fm.save_obj(data, name) + import urllib + print(urllib.parse.urlparse(self.artifact_uri).scheme) + try: + self.log_artifact(self.fm.path / name) + except shutil.SameFileError: + pass + except Exception as e: + print(e) + + def save_objects(self, name_data_list): + self.fm.save_objs(name_data_list) + try: + self.log_artifacts(self.fm.path) + except shutil.SameFileError: + pass + except Exception as e: + print(e) + + def load_object(self, name): + return self.fm.load_obj(name) + + def log_param(self, key, value): + mlflow.log_param(key, value) + + def log_params(self, params): + mlflow.log_params(params) + + def log_metric(self, key, value, step=None): + mlflow.log_metric(key, value, step) + + def log_metrics(self, metrics, step=None): + mlflow.log_metrics(metrics, step) + + def set_tag(self, key, value): + mlflow.set_tag(key, value) + + def set_tags(self, tags): + mlflow.set_tags(tags) + + def delete_tag(self, key): + mlflow.delete_tag(key) + + def log_artifact(self, local_path, artifact_path=None): + mlflow.log_artifact(local_path, artifact_path) + + def log_artifacts(self, local_dir, artifact_path=None): + mlflow.log_artifacts(local_dir, artifact_path) + + def get_artifact_uri(self, artifact_path=None): + if self.artifact_uri is not None: + return self.artifact_uri + return mlflow.get_artifact_uri(artifact_path) \ No newline at end of file From a50c9008b87507afeb3d73d4e8e203fe99eec713 Mon Sep 17 00:00:00 2001 From: Young Date: Wed, 28 Oct 2020 14:07:33 +0000 Subject: [PATCH 008/241] pass the whole workflow --- examples/workflow_by_code.py | 74 +++++------------ qlib/contrib/backtest/backtest.py | 17 ++-- qlib/contrib/evaluate.py | 15 ++-- qlib/contrib/model/gbdt.py | 103 ++++++++--------------- qlib/data/dataset/__init__.py | 133 +++++++++++++++++++++++++++++- qlib/data/dataset/handler.py | 57 +++++-------- qlib/data/dataset/utils.py | 32 +++++++ qlib/model/base.py | 43 +++------- qlib/utils/__init__.py | 5 +- qlib/workflow/__init__.py | 38 ++++----- 10 files changed, 296 insertions(+), 221 deletions(-) create mode 100644 qlib/data/dataset/utils.py diff --git a/examples/workflow_by_code.py b/examples/workflow_by_code.py index 326e9dee0..bc5f9337e 100644 --- a/examples/workflow_by_code.py +++ b/examples/workflow_by_code.py @@ -16,7 +16,8 @@ from qlib.contrib.evaluate import ( ) from qlib.utils import exists_qlib_data -from qlib.model.learner import train_model +# from qlib.model.learner import train_model +from qlib.utils import init_instance_by_config if __name__ == "__main__": @@ -57,13 +58,6 @@ if __name__ == "__main__": "test_end_time": "2020-08-01", } - # use default DataHandler - # custom DataHandler, refer to: TODO: DataHandler API url - handler = Alpha158(**DATA_HANDLER_CONFIG) - - data = handler.fetch(slice('2008-01-01', '2014-12-31'), data_key=handler.DK_I) - print(data) - task = { "model": { "class": "LGBModel", @@ -80,59 +74,33 @@ if __name__ == "__main__": "num_threads": 20, } }, - "data": { - "dataset": { - "class": "DatasetH", - "module_path": "qlib.data.dataset", - "kwargs": { - 'handler': { - "class": "Alpha158", - "kwargs": DATA_HANDLER_CONFIG - }, - "train_start_time": "2008-01-01", - "train_end_time": "2014-12-31", - "validate_start_time": "2015-01-01", - "validate_end_time": "2016-12-31", - "test_start_time": "2017-01-01", - "test_end_time": "2020-08-01", + "dataset": { + "class": "DatasetH", + "module_path": "qlib.data.dataset", + "kwargs": { + 'handler': { + "class": "Alpha158", + "module_path": "qlib.contrib.data.handler", + "kwargs": DATA_HANDLER_CONFIG + }, + 'segments': { + 'train': ("2008-01-01", "2014-12-31"), + 'valid': ("2015-01-01", "2016-12-31",), + 'test': ("2017-01-01", "2020-08-01",), } } - }, + } # You shoud record the data in specific sequence # "record": ['SignalRecord', 'SigAnaRecord', 'PortAnaRecord'], } - model = train_model(task) + # model = train_model(task) + model = init_instance_by_config(task['model']) + dataset = init_instance_by_config(task['dataset']) + model.fit(dataset) - - sys.exit(0) # I have tested the code above --------------------------------------------- - - x_train, y_train, x_validate, y_validate, x_test, y_test = Alpha158(**DATA_HANDLER_CONFIG).get_split_data( - **TRAINER_CONFIG - ) - - MODEL_CONFIG = { - "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, - } - # use default model - # custom Model, refer to: TODO: Model API url - model = LGBModel(**MODEL_CONFIG) - model.fit(x_train, y_train, x_validate, y_validate) - _pred = model.predict(x_test) - _pred = pd.DataFrame(_pred, index=x_test.index, columns=y_test.columns) - - # backtest requires pred_score - pred_score = pd.DataFrame(index=_pred.index) - pred_score["score"] = _pred.iloc(axis=1)[0] + pred_score = model.predict(dataset) # save pred_score to file pred_score_path = Path("~/tmp/qlib/pred_score.pkl").expanduser() diff --git a/qlib/contrib/backtest/backtest.py b/qlib/contrib/backtest/backtest.py index ea7220133..a1b2e5bce 100644 --- a/qlib/contrib/backtest/backtest.py +++ b/qlib/contrib/backtest/backtest.py @@ -10,6 +10,7 @@ from ...data import D from .account import Account from ...config import C from ...log import get_module_logger +from ...data.dataset.utils import get_level_index LOG = get_module_logger("backtest") @@ -18,7 +19,8 @@ def backtest(pred, strategy, trade_exchange, shift, verbose, account, benchmark) """Parameters ---------- pred : pandas.DataFrame - predict should has index and one `score` column + predict should has index and one `score` column + Qlib want to support multi-singal strategy in the future. So pd.Series is not used. strategy : Strategy() strategy part for backtest trade_exchange : Exchange() @@ -43,6 +45,12 @@ def backtest(pred, strategy, trade_exchange, shift, verbose, account, benchmark) `benchmark` is str, will use the daily change as the 'bench'. benchmark code, default is SH000905 CSI500 """ + # Convert format if the input format is not expected + if get_level_index(pred, level='datetime') == 1: + pred = pred.swaplevel().sort_index() + if isinstance(pred, pd.Series): + pred = pred.to_frame('score') + trade_account = 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()) @@ -71,10 +79,9 @@ def backtest(pred, strategy, trade_exchange, shift, verbose, account, benchmark) # 1. Load the score_series at pred_date try: - score = pred.loc(axis=0)[:, pred_date] # (stock_id, trade_date) multi_index, score in pdate - score_series = score.reset_index(level="datetime", drop=True)[ - "score" - ] # pd.Series(index:stock_id, data: score) + score = pred.loc(axis=0)[pred_date, :] # (trade_date, stock_id) multi_index, score in pdate + score_series = score.reset_index(level="datetime", + drop=True)["score"] # pd.Series(index:stock_id, data: score) except KeyError: LOG.warning("No score found on predict date[{:%Y-%m-%d}]".format(trade_date)) score_series = None diff --git a/qlib/contrib/evaluate.py b/qlib/contrib/evaluate.py index 8c427c16e..6dcefbb80 100644 --- a/qlib/contrib/evaluate.py +++ b/qlib/contrib/evaluate.py @@ -15,6 +15,7 @@ from .backtest.backtest import backtest as backtest_func, get_date_range from ..data import D from ..config import C +from ..data.dataset.utils import get_level_index logger = get_module_logger("Evaluate") @@ -158,11 +159,11 @@ def get_exchange( if deal_price[0] != "$": deal_price = "$" + deal_price if extract_codes: - codes = sorted(pred.index.get_level_values(0).unique()) + codes = sorted(pred.index.get_level_values('instrument').unique()) else: codes = "all" # TODO: We must ensure that 'all.txt' includes all the stocks - dates = sorted(pred.index.get_level_values(1).unique()) + dates = sorted(pred.index.get_level_values('datetime').unique()) dates = np.append(dates, get_date_range(dates[-1], shift=shift)) exchange = Exchange( @@ -187,7 +188,7 @@ def backtest(pred, account=1e9, shift=1, benchmark="SH000905", verbose=True, **k # backtest workflow related or commmon arguments pred : pandas.DataFrame - predict should has index and one `score` column + predict should has index and one `score` column account : float init account value shift : int @@ -297,6 +298,8 @@ def long_short_backtest( "short": short_returns(excess), "long_short": long_short_returns} """ + if get_level_index(pred, level='datetime') == 1: + pred = pred.swaplevel().sort_index() if trade_unit is None: trade_unit = C.trade_unit @@ -333,13 +336,13 @@ def long_short_backtest( ls_returns = {} for pdate, date in zip(predict_dates, trade_dates): - score = pred.loc(axis=0)[:, pdate] + score = pred.loc(axis=0)[pdate, :] score = score.reset_index().sort_values(by="score", ascending=False) long_stocks = list(score.iloc[:topk]["instrument"]) short_stocks = list(score.iloc[-topk:]["instrument"]) - score = score.set_index(["instrument", "datetime"]).sort_index() + score = score.set_index(["datetime", "instrument"]).sort_index() long_profit = [] short_profit = [] @@ -363,7 +366,7 @@ def long_short_backtest( else: short_profit.append(-profit) - for stock in list(score.loc(axis=0)[:, pdate].index.get_level_values(level=0)): + for stock in list(score.loc(axis=0)[pdate, :].index.get_level_values(level=0)): # exclude the suspend stock if trade_exchange.check_stock_suspended(stock_id=stock, trade_date=date): continue diff --git a/qlib/contrib/model/gbdt.py b/qlib/contrib/model/gbdt.py index b0c52edcb..2769f2282 100644 --- a/qlib/contrib/model/gbdt.py +++ b/qlib/contrib/model/gbdt.py @@ -1,91 +1,60 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. - -from __future__ import division -from __future__ import print_function - import numpy as np +import pandas as pd import lightgbm as lgb -from sklearn.metrics import roc_auc_score, mean_squared_error from ...model.base import Model -from ...utils import drop_nan_by_y_index +from ...data.dataset import DatasetH +from ...data.dataset.handler import DataHandlerLP class LGBModel(Model): - """LightGBM Model - - Parameters - ---------- - param_update : dict - training parameters - """ - - _params = dict() - + """LightGBM Model""" def __init__(self, loss="mse", **kwargs): if loss not in {"mse", "binary"}: raise NotImplementedError - self._scorer = mean_squared_error if loss == "mse" else roc_auc_score - self._params.update(objective=loss, **kwargs) - self._model = None + self._params = {'objective': loss} + self._params.update(kwargs) + self.model = None + + def fit(self, + dataset: DatasetH, + num_boost_round=1000, + early_stopping_rounds=50, + verbose_eval=20, + evals_result=dict(), + **kwargs): + + df_train, df_valid = dataset.prepare(['train', 'valid'], + col_set=['feature', 'label'], + data_key=DataHandlerLP.DK_L) + x_train, y_train = df_train['feature'], df_train['label'] + x_valid, y_valid = df_valid['feature'], df_valid['label'] - def fit( - self, - x_train, - y_train, - x_valid, - y_valid, - w_train=None, - w_valid=None, - num_boost_round=1000, - early_stopping_rounds=50, - verbose_eval=20, - evals_result=dict(), - **kwargs - ): # Lightgbm need 1D array as its label if y_train.values.ndim == 2 and y_train.values.shape[1] == 1: y_train_1d, y_valid_1d = np.squeeze(y_train.values), np.squeeze(y_valid.values) else: raise ValueError("LightGBM doesn't support multi-label training") - w_train_weight = None if w_train is None else w_train.values - w_valid_weight = None if w_valid is None else w_valid.values - - dtrain = lgb.Dataset(x_train.values, label=y_train_1d, weight=w_train_weight) - dvalid = lgb.Dataset(x_valid.values, label=y_valid_1d, weight=w_valid_weight) - self._model = lgb.train( - self._params, - dtrain, - num_boost_round=num_boost_round, - valid_sets=[dtrain, dvalid], - valid_names=["train", "valid"], - early_stopping_rounds=early_stopping_rounds, - verbose_eval=verbose_eval, - evals_result=evals_result, - **kwargs - ) + dtrain = lgb.Dataset(x_train.values, label=y_train_1d) + dvalid = lgb.Dataset(x_valid.values, label=y_valid_1d) + self.model = lgb.train(self._params, + dtrain, + num_boost_round=num_boost_round, + valid_sets=[dtrain, dvalid], + valid_names=["train", "valid"], + early_stopping_rounds=early_stopping_rounds, + verbose_eval=verbose_eval, + evals_result=evals_result, + **kwargs) evals_result["train"] = list(evals_result["train"].values())[0] evals_result["valid"] = list(evals_result["valid"].values())[0] - def predict(self, x_test): - if self._model is None: + def predict(self, dataset): + if self.model is None: raise ValueError("model is not fitted yet!") - return self._model.predict(x_test.values) - - def score(self, x_test, y_test, w_test=None): - # Remove rows from x, y and w, which contain Nan in any columns in y_test. - x_test, y_test, w_test = drop_nan_by_y_index(x_test, y_test, w_test) - preds = self.predict(x_test) - w_test_weight = None if w_test is None else w_test.values - return self._scorer(y_test.values, preds, sample_weight=w_test_weight) - - def save(self, filename): - if self._model is None: - raise ValueError("model is not fitted yet!") - self._model.save_model(filename) - - def load(self, buffer): - self._model = lgb.Booster(params={"model_str": buffer.decode("utf-8")}) + x_test = dataset.prepare('test', col_set='feature') + return pd.Series(self.model.predict(np.squeeze(x_test.values)), index=x_test.index) diff --git a/qlib/data/dataset/__init__.py b/qlib/data/dataset/__init__.py index ec6cb2c4b..fcf17546f 100644 --- a/qlib/data/dataset/__init__.py +++ b/qlib/data/dataset/__init__.py @@ -1,8 +1,133 @@ +from ...utils.serial import Serializable +from typing import Union, List, Tuple +from ...utils import init_instance_by_config +from .handler import DataHandler +import pandas as pd -class Dataset: + +class Dataset(Serializable): ''' - Preparing data for model training. - The type of dataset depends on the model. (It could be pd.DataFrame, pytorch.DataLoader, etc.) + Preparing data for model training and inferencing. ''' - def generate(self): + def __init__(self, *args, **kwargs): + ''' + init is designed to finish following steps + - setup data + - The data related attributes' names should start with '_' so that it will not be saved on disk when serializing + - initialize the state of the dataset(info to prepare the data) + - The name of essential state for preparing data should not start with '_' so that it could be serialized on disk when serializing. + + The data could specify the info to caculate the essential data for preparation + ''' + self.setup_data(*args, **kwargs) + super().__init__() + + def setup_data(self, *args, **kwargs): + """ + setup the data + + We split the setup_data function for following situation + - 1) User have a Dataset object with learned status on disk + - 2) User load the Dataset object from the disk(Note the init function is skiped) + - 3) User call `setup_data` to load new data + - 4) User prepare data for model based on previous status + """ pass + + def prepare(self, *args, **kwargs) -> object: + """ + The type of dataset depends on the model. (It could be pd.DataFrame, pytorch.DataLoader, etc.) + The parameters should specify the scope for the prepared data + The method sould + - process the data + - return the processed data + + Returns + ------- + object: + return the object + """ + pass + + +class DatasetH(Dataset): + ''' + Dataset with Data(H)anler + + User should try to put the data preprocessing functions into handler. + Only following data processing functions should be placed in Dataset + - The processing is related to specific model. + - The processing is related to data split + ''' + def __init__(self, handler: Union[dict, DataHandler], segments: list): + """ + Parameters + ---------- + handler : Union[dict, DataHandler] + handler will be passed into setup_data + segments : list + handler will be passed into setup_data + """ + super().__init__(handler, segments) + + def setup_data(self, handler: Union[dict, DataHandler], segments: list): + """ + setup the underlying data + + Parameters + ---------- + handler : Union[dict, DataHandler] + handler could be + 1) insntance of `DataHandler` + 2) config of `DataHandler`. Please refer to `DataHandler` + segments : list + Describe the options to segment the data. + Here are some examples + 1) 'segments': { + 'train': ("2008-01-01", "2014-12-31"), + 'valid': ("2017-01-01", "2020-08-01",), + 'test': ("2015-01-01", "2016-12-31",), + } + 2) 'segments': { + 'insample': ("2008-01-01", "2014-12-31"), + 'outsample': ("2017-01-01", "2020-08-01",), + } + """ + self._handler = init_instance_by_config(handler, accept_types=DataHandler) + self._segments = segments + + def prepare(self, + segments: Union[List[str], Tuple[str], str, slice], + col_set=DataHandler.CS_ALL, + **kwargs) -> Union[List[pd.DataFrame], pd.DataFrame]: + """ + prepare the data for learning and inference + + Parameters + ---------- + segments : Union[List[str], Tuple[str], str, slice] + Describe the scope of the data to be prepared + Here are some examples + 1) 'train' + 2) ['train', 'valid'] + col_set : [TODO:type] + [TODO:description] + + Returns + ------- + Union[List[pd.DataFrame], pd.DataFrame]: + [TODO:description] + + Raises + ------ + NotImplementedError: + [TODO:description] + """ + if isinstance(segments, (list, tuple)): + return [ + self._handler.fetch(slice(*self._segments[seg]), col_set=col_set, **kwargs) for seg in segments + ] + elif isinstance(segments, str): + return self._handler.fetch(slice(*self._segments[segments]), col_set=col_set, **kwargs) + else: + raise NotImplementedError(f"This type of input is not supported") diff --git a/qlib/data/dataset/handler.py b/qlib/data/dataset/handler.py index 7cc7995ea..4f33ae73c 100644 --- a/qlib/data/dataset/handler.py +++ b/qlib/data/dataset/handler.py @@ -5,7 +5,7 @@ import abc import bisect import logging -from typing import Union, Tuple +from typing import Union, Tuple, List import pandas as pd import numpy as np @@ -15,6 +15,7 @@ from ...data import D from ...config import C from ...utils import parse_config, transform_end_date, init_instance_by_config from ...utils.serial import Serializable +from .utils import get_level_index from pathlib import Path from .loader import DataLoader @@ -82,34 +83,6 @@ class DataHandler(Serializable): self._data = self.data_loader.load(self.instruments, self.start_time, self.end_time) # TODO: cache - def _get_level_index(self, df: pd.DataFrame, level=Union[str, int]) -> int: - """ - - get the level index of `df` given `level` - - Parameters - ---------- - df : pd.DataFrame - data - level : Union[str, int] - index level - - Returns - ------- - int: - The level index in the multiple index - """ - if isinstance(level, str): - try: - return df.index.names.index(level) - except (AttributeError, ValueError): - # NOTE: If level index is not given in the data, the default level index will be ('datetime', 'instrument') - return ('datetime', 'instrument').index(level) - elif isinstance(level, int): - return level - else: - raise NotImplementedError(f"This type of input is not supported") - def _fetch_df_by_index(self, df: pd.DataFrame, selector: Union[pd.Timestamp, slice, str, list], level: Union[str, int]) -> pd.DataFrame: """ fetch data from `data` with `selector` and `level` @@ -123,11 +96,11 @@ class DataHandler(Serializable): """ # Try to get the right index idx_slc = (selector, slice(None, None)) - if self._get_level_index(df, level) == 1: + if get_level_index(df, level) == 1: idx_slc = idx_slc[1], idx_slc[0] return df.loc(axis=0)[idx_slc] - CS_ALL = '_all' + CS_ALL = '__all' def _fetch_df_by_col(self, df: pd.DataFrame, col_set: str) -> pd.DataFrame: cln = len(df.columns.levels) @@ -138,7 +111,10 @@ class DataHandler(Serializable): else: return df.loc(axis=1)[col_set] - def fetch(self, selector: Union[pd.Timestamp, slice, str], level: Union[str, int]='datetime', col_set=CS_ALL) -> pd.DataFrame: + def fetch(self, + selector: Union[pd.Timestamp, slice, str], + level: Union[str, int] = 'datetime', + col_set: Union[str, List[str]] = CS_ALL) -> pd.DataFrame: """ fetch data from underlying data source @@ -148,8 +124,11 @@ class DataHandler(Serializable): describe how to select data by index level : Union[str, int] which index level to select the data - col_set : str - select a set of meaningful columns.(e.g. features, columns) + col_set : Union[str, List[str]] + if isinstance(col_set, str): + select a set of meaningful columns.(e.g. features, columns) + if isinstance(col_set, List[str]): + select several sets of meaningful columns, the returned data has multiple levels Returns ------- @@ -195,7 +174,15 @@ class DataHandlerLP(DataHandler): # - _proc_learn_df will be processed by infer_processors + learn_processors # - (e.g. _proc_infer_df processed by learn_processors ) - def __init__(self, instruments, start_time=None, end_time=None, data_loader: Tuple[dict, str, DataLoader]=None, infer_processors=[], learn_processors=[], process_type=PTYPE_A, **kwargs): + def __init__(self, + instruments, + start_time=None, + end_time=None, + data_loader: Tuple[dict, str, DataLoader] = None, + infer_processors=[], + learn_processors=[], + process_type=PTYPE_A, + **kwargs): """ Parameters ---------- diff --git a/qlib/data/dataset/utils.py b/qlib/data/dataset/utils.py new file mode 100644 index 000000000..c97256896 --- /dev/null +++ b/qlib/data/dataset/utils.py @@ -0,0 +1,32 @@ +from typing import Union +import pandas as pd + + +def get_level_index(df: pd.DataFrame, level=Union[str, int]) -> int: + """ + + get the level index of `df` given `level` + + Parameters + ---------- + df : pd.DataFrame + data + level : Union[str, int] + index level + + Returns + ------- + int: + The level index in the multiple index + """ + if isinstance(level, str): + try: + return df.index.names.index(level) + except (AttributeError, ValueError): + # NOTE: If level index is not given in the data, the default level index will be ('datetime', 'instrument') + return ('datetime', 'instrument').index(level) + elif isinstance(level, int): + return level + else: + raise NotImplementedError(f"This type of input is not supported") + diff --git a/qlib/model/base.py b/qlib/model/base.py index 66b54705a..11bd76d06 100644 --- a/qlib/model/base.py +++ b/qlib/model/base.py @@ -2,6 +2,7 @@ # Licensed under the MIT License. import abc from ..utils.serial import Serializable +from ..data.dataset import Dataset class BaseModel(Serializable, metaclass=abc.ABCMeta): @@ -20,45 +21,27 @@ class BaseModel(Serializable, metaclass=abc.ABCMeta): class Model(BaseModel): '''Learnable Models''' - # TODO: Make the model easier. - def fit(self, x_train, y_train, x_valid, y_valid, w_train=None, w_valid=None, **kwargs): - """fix train with cross-validation - Fit model when ex_config.finetune is False + def fit(self, dataset: Dataset): + """ + Learn model from the base model + + ** NOTE **: The the attribute names of learned model should **not** start with '_'. So that the model could be + dumped to disk. Parameters ---------- - x_train : pd.dataframe - train data - y_train : pd.dataframe - train label - x_valid : pd.dataframe - valid data - y_valid : pd.dataframe - valid label - w_train : pd.dataframe - train weight - w_valid : pd.dataframe - valid weight - - Returns - ---------- - Model - trained model + dataset : Dataset + dataset will generate the processed data from model training """ raise NotImplementedError() @abc.abstractmethod - def predict(self, x_test, **kwargs): - """predict given test data + def predict(self, dataset: Dataset) -> object: + """give prediction given Dataset Parameters ---------- - x_test : pd.dataframe - test data - - Returns - ---------- - np.ndarray - test predict label + dataset : Dataset + dataset will generate the processed dataset from model training """ raise NotImplementedError() diff --git a/qlib/utils/__init__.py b/qlib/utils/__init__.py index b10735868..5fc704541 100644 --- a/qlib/utils/__init__.py +++ b/qlib/utils/__init__.py @@ -194,7 +194,7 @@ def get_cls_kwargs(config: Union[dict, str], module) -> (type, dict): return klass, kwargs -def init_instance_by_config(config: Union[str, dict], module=None, accept_types: Tuple[type]=tuple([])) -> object: +def init_instance_by_config(config: Union[str, dict], module=None, accept_types: Union[type, Tuple[type]]=tuple([])) -> object: """ get initialized instance with config @@ -212,8 +212,9 @@ def init_instance_by_config(config: Union[str, dict], module=None, accept_types: module : Python module Optional. It should be a python module. - accept_types: Tuple[type] + accept_types: Union[type, Tuple[type]] Optional. If the config is a instance of specific type, return the config directly. + This will be passed into the second parameter of isinstance. Returns ------- diff --git a/qlib/workflow/__init__.py b/qlib/workflow/__init__.py index 76d5e7d4c..ad7815ffa 100644 --- a/qlib/workflow/__init__.py +++ b/qlib/workflow/__init__.py @@ -12,19 +12,19 @@ class Record: @contextmanager def start_exp(self, experiment_name=None, uri=None, project_path=None, artifact_location=None, nested=False): raise NotImplementedError(f"Please implement the `start_exp` method.") - + def search_runs(self, experiment_ids=None, filter_string='', run_view_type=1, max_results=100000, order_by=None): raise NotImplementedError(f"Please implement the `search_runs` method.") - + def get_exp(self, experiment_id): raise NotImplementedError(f"Please implement the `get_exp` method.") def get_exp_by_name(self, experiment_name): raise NotImplementedError(f"Please implement the `get_exp_by_name` method.") - + def create_exp(self, experiment_name, artifact_location=None): raise NotImplementedError(f"Please implement the `create_exp` method.") - + def set_exp(self, experiment_name): raise NotImplementedError(f"Please implement the `set_exp` method.") @@ -33,10 +33,10 @@ class Record: def set_tracking_uri(self, uri): raise NotImplementedError(f"Please implement the `set_tracking_uri` method.") - + def get_tracking_uri(self): raise NotImplementedError(f"Please implement the `get_tracking_uri` method.") - + def get_recorder(self): raise NotImplementedError(f"Please implement the `get_recorder` method.") @@ -48,7 +48,7 @@ class Record: def load_object(self, name): raise NotImplementedError(f"Please implement the `load_object` method.") - + def log_param(self, key, value): raise NotImplementedError(f"Please implement the `log_param` method.") @@ -60,7 +60,7 @@ class Record: def log_metrics(self, metrics, step=None): raise NotImplementedError(f"Please implement the `log_metrics` method.") - + def set_tag(self, key, value): raise NotImplementedError(f"Please implement the `set_tag` method.") @@ -69,7 +69,7 @@ class Record: def delete_tag(self, key): raise NotImplementedError(f"Please implement the `delete_tag` method.") - + def log_artifact(self, local_path, artifact_path=None): raise NotImplementedError(f"Please implement the `log_artifact` method.") @@ -86,19 +86,19 @@ class MLflowRecord(Record): @contextmanager def start_exp(self, experiment_name=None, uri=None, project_path=None, artifact_location=None, nested=False): yield self.exp_manager.start_exp(experiment_name, uri, project_path, artifact_location, nested) - + def search_runs(self, experiment_ids=None, filter_string='', run_view_type=1, max_results=100000, order_by=None): return self.exp_manager.search_runs(experiment_ids, filter_string, run_view_type, max_results, order_by) - + def get_exp(self, experiment_id): return self.exp_manager.get_exp(experiment_id) def get_exp_by_name(self, experiment_name): return self.exp_manager.get_exp_by_name(experiment_name) - + def create_exp(self, experiment_name, artifact_location=None): self.exp_manager.create_exp(experiment_name, artifact_location) - + def set_exp(self, experiment_name): self.exp_manager.set_exp(experiment_name) @@ -107,10 +107,10 @@ class MLflowRecord(Record): def set_tracking_uri(self, uri): self.exp_manager.set_tracking_uri(uri) - + def get_tracking_uri(self): return self.exp_manager.get_tracking_uri() - + def get_recorder(self): return self.exp_manager.get_recorder() @@ -122,7 +122,7 @@ class MLflowRecord(Record): def load_object(self, name): return self.exp_manager.active_recorder.load_object(name) - + def log_param(self, key, value): self.exp_manager.active_recorder.log_param(key, value) @@ -134,7 +134,7 @@ class MLflowRecord(Record): def log_metrics(self, metrics, step=None): self.exp_manager.active_recorder.log_metrics(metrics, step) - + def set_tag(self, key, value): self.exp_manager.active_recorder.set_tag(key, value) @@ -143,7 +143,7 @@ class MLflowRecord(Record): def delete_tag(self, key): self.exp_manager.active_recorder.delete_tag(key) - + def log_artifact(self, local_path, artifact_path=None): self.exp_manager.active_recorder.log_artifact(local_path, artifact_path) @@ -154,4 +154,4 @@ class MLflowRecord(Record): return self.exp_manager.active_recorder.get_artifact_uri(artifact_path) # global record -R = MLflowRecord() \ No newline at end of file +R = MLflowRecord() From 60d0cfcf64f90c80d958315b9dbf65ffd0437b01 Mon Sep 17 00:00:00 2001 From: Jactus Date: Thu, 29 Oct 2020 12:58:52 +0800 Subject: [PATCH 009/241] Update Exp related codes --- qlib/__init__.py | 12 +- qlib/config.py | 6 + qlib/data/data.py | 39 +----- qlib/utils/__init__.py | 36 +++++ qlib/workflow/__init__.py | 168 ++++++------------------ qlib/workflow/exp.py | 258 +++--------------------------------- qlib/workflow/expm.py | 236 +++++++++++++++++++++++++++++++++ qlib/workflow/record.py | 270 +++++++++++--------------------------- 8 files changed, 426 insertions(+), 599 deletions(-) create mode 100644 qlib/workflow/expm.py diff --git a/qlib/__init__.py b/qlib/__init__.py index 8d0b322b1..f2b2c28ac 100644 --- a/qlib/__init__.py +++ b/qlib/__init__.py @@ -13,7 +13,7 @@ import platform import yaml from pathlib import Path -from .utils import can_use_cache +from .utils import can_use_cache, init_instance_by_config, get_module_by_module_path # init qlib @@ -22,6 +22,7 @@ def init(default_conf="client", **kwargs): from .data.data import register_all_wrappers from .log import get_module_logger, set_log_with_config from .data.cache import H + from .workflow import R, QlibRecorder C.reset() H.clear() @@ -79,6 +80,15 @@ def init(default_conf="client", **kwargs): if "flask_server" in C: LOG.info(f"flask_server={C['flask_server']}, flask_port={C['flask_port']}") + + # set up QlibRecorder + default_uri = str(Path(os.getcwd()).resolve() / "mlruns") + current_uri = C['exp_uri'] if C['exp_uri'] is not None else default_uri + # exp manager module + module = get_module_by_module_path('qlib.workflow') + exp_manager = init_instance_by_config(C['exp_manager'], module) + qr = QlibRecorder(exp_manager, default_uri, current_uri) + R.register(qr) def _mount_nfs_uri(C): diff --git a/qlib/config.py b/qlib/config.py index ff01fe5e8..db5fab69c 100644 --- a/qlib/config.py +++ b/qlib/config.py @@ -124,6 +124,12 @@ _default_config = { }, "loggers": {"qlib": {"level": "DEBUG", "handlers": ["console"]}}, }, + # Defatult config for experiment manager + "exp_manager": { + "class": "MLflowExpManager", + "kwargs": {} + }, + "exp_uri": None, } MODE_CONF = { diff --git a/qlib/data/data.py b/qlib/data/data.py index c41d32f6e..476cc9682 100644 --- a/qlib/data/data.py +++ b/qlib/data/data.py @@ -24,6 +24,7 @@ from ..log import get_module_logger from ..utils import parse_field, read_bin, hash_args, normalize_cache_fields from .base import Feature from .cache import DiskDatasetCache, DiskExpressionCache +from ..utils import Wrapper, get_provider_obj, register_wrapper class CalendarProvider(abc.ABC): @@ -1019,44 +1020,6 @@ class ClientProvider(BaseProvider): DatasetD.set_conn(self.client) -class Wrapper(object): - """Data Provider Wrapper""" - - def __init__(self): - self._provider = None - - def register(self, provider): - self._provider = provider - - def __getattr__(self, key): - if self._provider is None: - raise AttributeError("Please run qlib.init() first using qlib") - return getattr(self._provider, key) - - -def get_cls_from_name(cls_name): - return getattr(importlib.import_module(".data", package="qlib"), cls_name) - - -def get_provider_obj(config, **params): - if isinstance(config, dict): - params.update(config["kwargs"]) - config = config["class"] - return get_cls_from_name(config)(**params) - - -def register_wrapper(wrapper, cls_or_obj): - """register_wrapper - - :param wrapper: A wrapper of all kinds of providers - :param cls_or_obj: A class or class name or object instance in data/data.py - """ - if isinstance(cls_or_obj, str): - cls_or_obj = get_cls_from_name(cls_or_obj) - obj = cls_or_obj() if isinstance(cls_or_obj, type) else cls_or_obj - wrapper.register(obj) - - Cal = Wrapper() Inst = Wrapper() FeatureD = Wrapper() diff --git a/qlib/utils/__init__.py b/qlib/utils/__init__.py index b10735868..8467db600 100644 --- a/qlib/utils/__init__.py +++ b/qlib/utils/__init__.py @@ -611,3 +611,39 @@ def exists_qlib_data(qlib_dir): return False return True + + +#################### Wrapper ##################### +class Wrapper(object): + """Data Provider Wrapper""" + + def __init__(self): + self._provider = None + + def register(self, provider): + self._provider = provider + + def __getattr__(self, key): + if self._provider is None: + raise AttributeError("Please run qlib.init() first using qlib") + return getattr(self._provider, key) + + +def get_provider_obj(config, **params): + module = get_module_by_module_path("qlib.data") + klass, kwargs = get_cls_kwargs(config, module) + kwargs.update(params) + return klass(**kwargs) + + +def register_wrapper(wrapper, cls_or_obj): + """register_wrapper + + :param wrapper: A wrapper of all kinds of providers + :param cls_or_obj: A class or class name or object instance in data/data.py + """ + if isinstance(cls_or_obj, str): + module = get_module_by_module_path("qlib.data") + cls_or_obj = getattr(module, cls_or_obj) + obj = cls_or_obj() if isinstance(cls_or_obj, type) else cls_or_obj + wrapper.register(obj) \ No newline at end of file diff --git a/qlib/workflow/__init__.py b/qlib/workflow/__init__.py index 76d5e7d4c..db5112470 100644 --- a/qlib/workflow/__init__.py +++ b/qlib/workflow/__init__.py @@ -2,156 +2,62 @@ # Licensed under the MIT License. from contextlib import contextmanager -from .record import MLflowRecorder -from .exp import MLflowExpManager +from .expm import * +from ..utils import Wrapper -class Record: - def __init__(self): - pass +class QlibRecorder: + def __init__(self, exp_manager, default_uri, current_uri): + self.exp_manager = exp_manager + self.default_uri = default_uri + self.current_uri = current_uri @contextmanager - def start_exp(self, experiment_name=None, uri=None, project_path=None, artifact_location=None, nested=False): - raise NotImplementedError(f"Please implement the `start_exp` method.") + def start(self, experiment_name): + run = self.start_exp(experiment_name, self.current_uri) + yield run + self.end_exp() - def search_runs(self, experiment_ids=None, filter_string='', run_view_type=1, max_results=100000, order_by=None): - raise NotImplementedError(f"Please implement the `search_runs` method.") + def start_exp(self, experiment_name=None): + return self.exp_manager.start_exp(experiment_name, self.current_uri) + + def end_exp(self): + self.exp_manager.end_exp() - def get_exp(self, experiment_id): - raise NotImplementedError(f"Please implement the `get_exp` method.") - - def get_exp_by_name(self, experiment_name): - raise NotImplementedError(f"Please implement the `get_exp_by_name` method.") + def search_records(self, experiment_ids, **kwargs): + return self.exp_manager.search_records(experiment_ids, **kwargs) - def create_exp(self, experiment_name, artifact_location=None): - raise NotImplementedError(f"Please implement the `create_exp` method.") - - def set_exp(self, experiment_name): - raise NotImplementedError(f"Please implement the `set_exp` method.") - - def delete_exp(self, experiment_id): - raise NotImplementedError(f"Please implement the `create_exp` method.") - - def set_tracking_uri(self, uri): - raise NotImplementedError(f"Please implement the `set_tracking_uri` method.") - - def get_tracking_uri(self): - raise NotImplementedError(f"Please implement the `get_tracking_uri` method.") - - def get_recorder(self): - raise NotImplementedError(f"Please implement the `get_recorder` method.") - - def save_object(self, name, data): - raise NotImplementedError(f"Please implement the `save_object` method.") - - def save_objects(self, name_data_list): - raise NotImplementedError(f"Please implement the `save_objects` method.") - - def load_object(self, name): - raise NotImplementedError(f"Please implement the `load_object` method.") - - def log_param(self, key, value): - raise NotImplementedError(f"Please implement the `log_param` method.") - - def log_params(self, params): - raise NotImplementedError(f"Please implement the `log_params` method.") - - def log_metric(self, key, value, step=None): - raise NotImplementedError(f"Please implement the `log_metric` method.") - - def log_metrics(self, metrics, step=None): - raise NotImplementedError(f"Please implement the `log_metrics` method.") - - def set_tag(self, key, value): - raise NotImplementedError(f"Please implement the `set_tag` method.") - - def set_tags(self, tags): - raise NotImplementedError(f"Please implement the `log_tags` method.") - - def delete_tag(self, key): - raise NotImplementedError(f"Please implement the `delete_tag` method.") - - def log_artifact(self, local_path, artifact_path=None): - raise NotImplementedError(f"Please implement the `log_artifact` method.") - - def log_artifacts(self, local_dir, artifact_path=None): - raise NotImplementedError(f"Please implement the `log_artifacts` method.") - - def get_artifact_uri(self, artifact_path=None): - raise NotImplementedError(f"Please implement the `get_artifact_uri` method.") - -class MLflowRecord(Record): - def __init__(self): - self.exp_manager = MLflowExpManager() - - @contextmanager - def start_exp(self, experiment_name=None, uri=None, project_path=None, artifact_location=None, nested=False): - yield self.exp_manager.start_exp(experiment_name, uri, project_path, artifact_location, nested) - - def search_runs(self, experiment_ids=None, filter_string='', run_view_type=1, max_results=100000, order_by=None): - return self.exp_manager.search_runs(experiment_ids, filter_string, run_view_type, max_results, order_by) - - def get_exp(self, experiment_id): - return self.exp_manager.get_exp(experiment_id) - - def get_exp_by_name(self, experiment_name): - return self.exp_manager.get_exp_by_name(experiment_name) - - def create_exp(self, experiment_name, artifact_location=None): - self.exp_manager.create_exp(experiment_name, artifact_location) - - def set_exp(self, experiment_name): - self.exp_manager.set_exp(experiment_name) + def get_exp(self, experiment_id=None, experiment_name=None): + return self.exp_manager.get_exp(experiment_id, experiment_name) def delete_exp(self, experiment_id): self.exp_manager.delete_exp(experiment_id) - def set_tracking_uri(self, uri): - self.exp_manager.set_tracking_uri(uri) - - def get_tracking_uri(self): - return self.exp_manager.get_tracking_uri() - + def get_uri(self, type): + return self.exp_manager.get_uri(type) + def get_recorder(self): - return self.exp_manager.get_recorder() + return self.exp_manager.active_recorder - def save_object(self, name, data): - self.exp_manager.active_recorder.save_object(name, data) + def save_object(self, data=None, name=None, local_path=None): + self.exp_manager.active_recorder.save_object(data, name, local_path) - def save_objects(self, name_data_list): - self.exp_manager.active_recorder.save_objects(name_data_list) + def save_objects(self, data_name_list=None, local_path=None): + self.exp_manager.active_recorder.save_objects(data_name_list, local_path) def load_object(self, name): return self.exp_manager.active_recorder.load_object(name) + + def log_params(self, **kwargs): + self.exp_manager.active_recorder.log_params(**kwargs) + + def log_metrics(self, step=None, **kwargs): + self.exp_manager.active_recorder.log_metrics(step, **kwargs) - def log_param(self, key, value): - self.exp_manager.active_recorder.log_param(key, value) - - def log_params(self, params): - self.exp_manager.active_recorder.log_params(params) - - def log_metric(self, key, value, step=None): - self.exp_manager.active_recorder.log_metric(key, value, step) - - def log_metrics(self, metrics, step=None): - self.exp_manager.active_recorder.log_metrics(metrics, step) - - def set_tag(self, key, value): - self.exp_manager.active_recorder.set_tag(key, value) - - def set_tags(self, tags): - self.exp_manager.active_recorder.set_tags(tags) + def set_tags(self, **kwargs): + self.exp_manager.active_recorder.set_tags(**kwargs) def delete_tag(self, key): self.exp_manager.active_recorder.delete_tag(key) - - def log_artifact(self, local_path, artifact_path=None): - self.exp_manager.active_recorder.log_artifact(local_path, artifact_path) - - def log_artifacts(self, local_dir, artifact_path=None): - self.exp_manager.active_recorder.log_artifacts(local_dir, artifact_path) - - def get_artifact_uri(self, artifact_path=None): - return self.exp_manager.active_recorder.get_artifact_uri(artifact_path) # global record -R = MLflowRecord() \ No newline at end of file +R = Wrapper() \ No newline at end of file diff --git a/qlib/workflow/exp.py b/qlib/workflow/exp.py index f3cedea90..9e076aced 100644 --- a/qlib/workflow/exp.py +++ b/qlib/workflow/exp.py @@ -2,67 +2,23 @@ # Licensed under the MIT License. import mlflow -from contextlib import contextmanager -from .record import MLflowRecorder +from pathlib import Path -class ExpManager: +class Experiment: """ - This is the `ExpManager` class for managing the experiments. The API is designed similar to mlflow. - (The link: https://mlflow.org/docs/latest/python_api/mlflow.html) + Thie is the `Experiment` class for each experiment being run. The API is designed """ def __init__(self): - self.active_recorder = None - self.experiments = dict() # store the experiment names -> list of recorders. - self.exp_ids = list() - - def _store_exp(self, id, name): - """ - Store the experiments in the experiments holder. - """ - if id in self.exp_ids: - raise Exception('Something went wrong when creating the experiment. Please check if the experiment is already created.') - if name in self.experiments: - assert int(id) == int(self.experiments[name][0]), 'Experiment id and name are not consistent when storing the experiment.' - else: - self.exp_ids.append(id) - self.experiments[name] = [id] + self.name = None + self.id = None + self.recorders = list() - def start_exp(self, project_path, experiment_name=None, uri=None, artifact_location=None, nested=False): + def search_records(self, **kwargs): """ - Start running an experiment. This method can only work in the `with` statement. + Get a pandas DataFrame of records that fit the search criteria of the experiment. Parameters ---------- - project_path : str - path for the project. - experiment_name : str - name of the active experiment. - uri : str - the current tracking URI. - artifact_location : str - the location to store all the artifacts. - nested : boolean - controls whether run is nested in parent run. - - Returns - None - """ - raise NotImplementedError(f"Please implement the `start_exp` method.") - - def end_exp(self): - """ - End an active experiment. - """ - raise NotImplementedError(f"Please implement the `end_exp` method.") - - def search_runs(self, experiment_ids=None, filter_string='', run_view_type=1, max_results=100000, order_by=None): - """ - Get a pandas DataFrame of runs that fit the search criteria. - - Parameters - ---------- - experiment_ids : list - list of experiment IDs. filter_string : str filter query string, defaults to searching all runs. run_view_type : int @@ -74,192 +30,18 @@ class ExpManager: Returns ------- - A pandas.DataFrame of runs. + A pandas.DataFrame of records. """ - raise NotImplementedError(f"Please implement the `search_runs` method.") - - def get_exp(self, experiment_id): - """ - Retrieve an experiment by experiment_id from the backend store. - - Parameters - ---------- - experiment_id : str - the experiment id to return. - - Returns - ------- - An experiment object (e.g. mlflow.entities.Experiment). - """ - raise NotImplementedError(f"Please implement the `get_exp` method.") - - def get_exp_by_name(self, experiment_name): - """ - Retrieve an experiment by experiment name from the backend store. - - Parameters - ---------- - experiment_name : str - the experiment name to return. - - Returns - ------- - An experiment object (e.g. mlflow.entities.Experiment). - """ - raise NotImplementedError(f"Please implement the `get_exp_by_name` method.") - - def create_exp(self, experiment_name, artifact_location=None): - """ - Create an experiment. - - Parameters - ---------- - experiment_name : str - the experiment name, which must be unique. - artifact_location : str - the location to store run artifacts. - - Returns - ------- - String id of created experiment. - """ - raise NotImplementedError(f"Please implement the `create_exp` method.") - - def set_exp(self, experiment_name): - """ - Set the experiment to be active. - - Parameters - ---------- - experiment_name : str - the experiment name, which must be unique. - - Returns - ------- - String id of created experiment. - """ - raise NotImplementedError(f"Please implement the `set_exp` method.") - - def delete_exp(self, experiment_id): - """ - Delete an experiment. - - Parameters - ---------- - experiment_id : str - the experiment id. - - Returns - ------- - None - """ - raise NotImplementedError(f"Please implement the `create_exp` method.") - - def set_tracking_uri(self, uri): - """ - Set the tracking server URI. - - Parameters - ---------- - uri : str - the uri of the tracking server, can be An empty string, or a local file path, prefixed with file:/. - or An HTTP URI or A Databricks workspace. - Returns - ------- - None - """ - raise NotImplementedError(f"Please implement the `set_tracking_uri` method.") - - def get_tracking_uri(self): - """ - Get the tracking server URI. - - Parameters - ---------- - - Returns - ------- - The tracking URI. - """ - raise NotImplementedError(f"Please implement the `get_tracking_uri` method.") - - def get_recorder(self): - """ - Get the current active Recorder. - - Parameters - ---------- - - Returns - ------- - An Recorder object. - """ - raise NotImplementedError(f"Please implement the `get_recorder` method.") + raise NotImplementedError(f"Please implement the `search_records` method.") -class MLflowExpManager(ExpManager): - ''' - Use mlflow to implement ExpManager. - ''' - def start_exp(self, experiment_name=None, uri=None, project_path=None, artifact_location=None, nested=False): - # set the tracking uri - if uri is None: - assert project_path is not None, "Please provide the project_path if no uri is provided in order to set a proper tracking uri." - print('No tracking URI is provided. The default tracking URI is set as `mlruns` under the project path.') - mlflow.set_tracking_uri(str(project_path / "mlruns")) - else: - mlflow.set_tracking_uri(uri) - # start the experiment - if experiment_name is None: - print('No experiment name provided. The default experiment name is set as `experiment`.') - experiment_id = self.create_exp('experiment', artifact_location) - # set the active experiment - self.set_exp('experiment') - experiment_name = 'experiment' - else: - if experiment_name not in self.experiments: - if self.get_exp_by_name(experiment_name) is not None: - raise Exception('The experiment has already been created before. Please pick another name or delete the files under tracking uri.') - experiment_id = self.create_exp(experiment_name, artifact_location) - else: - experiment_id = self.experiments(experiment_name)[0] - # set the active experiment - self.set_exp(experiment_name) - - # store the id and name - self._store_exp(experiment_id, experiment_name) - # set up recorder - recorder = MLflowRecorder(experiment_id) - self.active_recorder = recorder - # store the recorder - self.experiments[experiment_name].append(self.active_recorder) - - return self.active_recorder.start_run(experiment_id=experiment_id, nested=nested) - - def search_runs(self, experiment_ids=None, filter_string='', run_view_type=1, max_results=100000, order_by=None): - return mlflow.search_runs(experiment_ids, filter_string, run_view_type, max_results, order_by) - - def get_exp(self, experiment_id): - return mlflow.get_experiment(experiment_id) - - def get_exp_by_name(self, experiment_name): - return mlflow.get_experiment_by_name(experiment_name) - - def create_exp(self, experiment_name, artifact_location=None): - return mlflow.create_experiment(experiment_name, artifact_location) - - def set_exp(self, experiment_name): - mlflow.set_experiment(experiment_name) - - def delete_exp(self, experiment_id): - mlflow.delete_experiment(experiment_id) - self.experiments = {key:val for key, val in self.experiments.items() if val[0] != experiment_id} - - def set_tracking_uri(self, uri): - mlflow.set_tracking_uri(uri) - - def get_tracking_uri(self): - return mlflow.get_tracking_uri() - - def get_recorder(self): - return self.active_recorder \ No newline at end of file +class MLflowExperiment(Experiment): + """ + Use mlflow to implement Experiment. + """ + def search_records(self, **kwargs): + filter_string = '' if kwargs.get('filter_string') is None else kwargs.get('filter_string') + run_view_type = 1 if kwargs.get('run_view_type') is None else kwargs.get('run_view_type') + max_results = 100000 if kwargs.get('max_results') is None else kwargs.get('max_results') + order_by = kwargs.get('order_by') + return mlflow.search_runs([self.experiment_id], filter_string, run_view_type, max_results, order_by) \ No newline at end of file diff --git a/qlib/workflow/expm.py b/qlib/workflow/expm.py new file mode 100644 index 000000000..36a945f42 --- /dev/null +++ b/qlib/workflow/expm.py @@ -0,0 +1,236 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +import mlflow +import os +from pathlib import Path +from contextlib import contextmanager +from .exp import MLflowExperiment +from .record import MLflowRecorder + +class ExpManager: + """ + This is the `ExpManager` class for managing the experiments. The API is designed similar to mlflow. + (The link: https://mlflow.org/docs/latest/python_api/mlflow.html) + """ + def __init__(self): + self.default_uri = None + self.active_recorder = None # only one recorder can running each time + self.experiments = dict() # store the experiment name --> Experiment object + + def start_exp(self, experiment_name=None, uri=None, **kwargs): + """ + Start running an experiment. + + Parameters + ---------- + experiment_name : str + name of the active experiment. + uri : str + the current tracking URI. + artifact_location : str + the location to store all the artifacts. + nested : boolean + controls whether run is nested in parent run. + + Returns + An object wrapped by context manager. + """ + raise NotImplementedError(f"Please implement the `start_exp` method.") + + def end_exp(self, **kwargs): + """ + End an running experiment. + + Parameters + ---------- + experiment_name : str + name of the active experiment. + """ + raise NotImplementedError(f"Please implement the `end_exp` method.") + + def search_records(self, experiment_ids=None, **kwargs): + """ + Get a pandas DataFrame of records that fit the search criteria. + + Parameters + ---------- + experiment_ids : list + list of experiment IDs. + filter_string : str + filter query string, defaults to searching all runs. + run_view_type : int + one of enum values ACTIVE_ONLY, DELETED_ONLY, or ALL (e.g. in mlflow.entities.ViewType). + max_results : int + the maximum number of runs to put in the dataframe. + order_by : list + list of columns to order by (e.g., “metrics.rmse”). + + Returns + ------- + A pandas.DataFrame of runs. + """ + raise NotImplementedError(f"Please implement the `search_records` method.") + + def __create_exp(self, experiment_name, artifact_location=None): + """ + Create an experiment. + + Parameters + ---------- + experiment_name : str + the experiment name, which must be unique. + artifact_location : str + the location to store run artifacts. + + Returns + ------- + An experiment object. + """ + raise NotImplementedError(f"Please implement the `create_exp` method.") + + def get_exp(self, experiment_id=None, experiment_name=None): + """ + Retrieve an experiment by experiment_id from the backend store. + + Parameters + ---------- + experiment_id : str + the experiment id to return. + + Returns + ------- + An experiment object. + """ + raise NotImplementedError(f"Please implement the `get_exp` method.") + + def delete_exp(self, experiment_id): + """ + Delete an experiment. + + Parameters + ---------- + experiment_id : str + the experiment id. + """ + raise NotImplementedError(f"Please implement the `create_exp` method.") + + def get_uri(self, type): + """ + Get the default tracking URI or current URI. + + Parameters + ---------- + type : str + the type of the tracking URI one wants to retrieve. + + Returns + ------- + The tracking URI string. + """ + raise NotImplementedError(f"Please implement the `create_exp` method.") + + def get_recorder(self): + """ + Get the current active Recorder. + + Parameters + ---------- + + Returns + ------- + An Recorder object. + """ + raise NotImplementedError(f"Please implement the `get_recorder` method.") + + +class MLflowExpManager(ExpManager): + ''' + Use mlflow to implement ExpManager. + ''' + def __init__(self): + super(MLflowExpManager, self).__init__() + self.default_uri = None + self.current_uri = None + + def start_exp(self, experiment_name=None, uri=None): + # create experiment + experiment = self.__create_exp(experiment_name, uri) + # set up recorder + recorder = MLflowRecorder(experiment.id) + self.active_recorder = recorder + # store the recorder + experiment.recorders.append(self.active_recorder) + # store the experiment + self.experiments[experiment_name] = experiment + + return self.active_recorder.start_run(experiment_id=experiment.id) + + def end_exp(self): + self.active_recorder.end_run() + self.active_recorder = None + + def __create_exp(self, experiment_name=None, uri=None): + # init experiment + experiment = MLflowExperiment() + # set the tracking uri + if uri is None: + print('No tracking URI is provided. The default tracking URI is set as `mlruns` under the working directory.') + else: + self.current_uri = uri + mlflow.set_tracking_uri(self.current_uri) + # start the experiment + if experiment_name is None: + print('No experiment name provided. The default experiment name is set as `experiment`.') + experiment_id = mlflow.create_experiment('experiment') + # set the active experiment + mlflow.set_experiment('experiment') + experiment_name = 'experiment' + else: + if experiment_name not in self.experiments: + if mlflow.get_experiment_by_name(experiment_name) is not None: + raise Exception('The experiment has already been created before. Please pick another name or delete the files under uri.') + experiment_id = mlflow.create_experiment(experiment_name) + else: + experiment_id = self.experiments[experiment_name].id + experiment = self.experiments[experiment_name] + # set the active experiment + mlflow.set_experiment(experiment_name) + # set up experiment + experiment.id = experiment_id + experiment.name = experiment_name + + return experiment + + def search_records(self, experiment_ids, **kwargs): + filter_string = '' if kwargs.get('filter_string') is None else kwargs.get('filter_string') + run_view_type = 1 if kwargs.get('run_view_type') is None else kwargs.get('run_view_type') + max_results = 100000 if kwargs.get('max_results') is None else kwargs.get('max_results') + order_by = kwargs.get('order_by') + return mlflow.search_runs(experiment_ids, filter_string, run_view_type, max_results, order_by) + + def get_exp(self, experiment_id=None, experiment_name=None): + assert experiment_id is not None or experiment_name is not None, 'Please provide at least one of the experiment id or name to retrieve an experiment.' + if experiment_name is not None: + return self.experiments[experiment_name] + elif: + for name in self.experiments: + if self.experiments[name].id == experiment_id: + return self.experiments[name] + else: + print('No valid experiment is found. Please make sure the id and name are correctly given.') + + def delete_exp(self, experiment_id): + mlflow.delete_experiment(experiment_id) + self.experiments = {key:val for key, val in self.experiments.items() if val.id != experiment_id} + + def get_uri(self, type): + if uri == 'default': + return self.default_uri + elif uri == 'current': + return self.current_uri + else: + raise ValueError('Input type is not supported. Please choose type default or current to get the uri.') + + def get_recorder(self): + return self.active_recorder \ No newline at end of file diff --git a/qlib/workflow/record.py b/qlib/workflow/record.py index 7895cf0fb..071c92691 100644 --- a/qlib/workflow/record.py +++ b/qlib/workflow/record.py @@ -2,7 +2,7 @@ # Licensed under the MIT License. import mlflow -import shutil +import shutil, os, pickle, tempfile, codecs from pathlib import Path from ..utils.objm import FileManager @@ -12,45 +12,39 @@ class Recorder: (The link: https://mlflow.org/docs/latest/python_api/mlflow.html) """ - def __init__(self, experiment_id, project_path=None): + def __init__(self, experiment_id): self.experiment_id = experiment_id self.recorder_id = None self.recorder_name = None - self.fm = None - self.artifact_uri = None def set_recorder_name(self, rname): self.recorder_name = rname - def save_object(self, name, data): + def save_object(self, data, name, local_path=None): """ - Save object such as prediction file or model checkpoints. + Save object such as prediction file or model checkpoints to the artifact URI. Parameters ---------- - name : str - name of the file to be saved. data : any type the data to be saved. - - Returns - ------- - None. + name : str + name of the file to be saved. + local_path : str + if provided, them save the file or directory to the artifact URI. """ raise NotImplementedError(f"Please implement the `save_object` method.") - def save_objects(self, name_data_list): + def save_objects(self, data_name_list, local_path=None): """ - Save objects such as prediction file or model checkpoints. + Save objects such as prediction file or model checkpoints to the artifact URI. Parameters ---------- - name_data_list : list - list of (name, data) pairs - - Returns - ------- - None. + data_name_list : list + list of (data, name) pairs + local_path : str + if provided, them save the file or directory to the artifact URI. """ raise NotImplementedError(f"Please implement the `save_objects` method.") @@ -98,99 +92,36 @@ class Recorder: """ raise NotImplementedError(f"Please implement the `end_run` method.") - def log_param(self, key, value): - """ - Log a parameter under the current run. - - Parameters - ---------- - key : str - the name of the parameter - value : str - the value of the parameter - - Returns - ------- - None - """ - raise NotImplementedError(f"Please implement the `log_param` method.") - - def log_params(self, params): + def log_params(self, **kwargs): """ Log a batch of params for the current run. Parameters ---------- - params : dict - dictionary of param_name: String -> value: String. - - Returns - ------- - None + keyword arguments + key, value pair to be logged as parameters. """ raise NotImplementedError(f"Please implement the `log_params` method.") - def log_metric(self, key, value, step=None): - """ - Log a metric under the current run. - - Parameters - ---------- - key : str - the name of the metric - value : float - the value of the metric - - Returns - ------- - None - """ - raise NotImplementedError(f"Please implement the `log_metric` method.") - - def log_metrics(self, metrics, step=None): + def log_metrics(self, step=None, **kwargs): """ Log multiple metrics for the current run. Parameters ---------- - metrics : dict - dictionary of metric_name: String -> value: Float. - - Returns - ------- - None + keyword arguments + key, value pair to be logged as metrics. """ raise NotImplementedError(f"Please implement the `log_metrics` method.") - - def set_tag(self, key, value): - """ - Set a tag under the current run. - Parameters - ---------- - key : str - the name of the tag - value : str - the value of the tag - - Returns - ------- - None - """ - raise NotImplementedError(f"Please implement the `set_tag` method.") - - def set_tags(self, tags): + def set_tags(self, **kwargs): """ Log a batch of tags for the current run. Parameters ---------- - tags : dict - dictionary of tag_name: String -> value: String. - - Returns - ------- - None + keyword arguments + key, value pair to be logged as tags. """ raise NotImplementedError(f"Please implement the `log_tags` method.") @@ -202,67 +133,22 @@ class Recorder: ---------- key : str the name of the tag to be deleted. - - Returns - ------- - None """ raise NotImplementedError(f"Please implement the `delete_tag` method.") - - def log_artifact(self, local_path, artifact_path=None): - """ - Log a local file or directory as an artifact of the currently active run. - - Parameters - ---------- - local_path : str - path to the file to write. - artifact_path : str - the directory in `artifact_uri` to write to. - - Returns - ------- - None - """ - raise NotImplementedError(f"Please implement the `log_artifact` method.") - - def log_artifacts(self, local_dir, artifact_path=None): - """ - Log all the contents of a local directory as artifacts of the run. - - Parameters - ---------- - local_dir : str - path to the directory of files to write. - artifact_path : str - the directory in `artifact_uri` to write to. - - Returns - ------- - None - """ - raise NotImplementedError(f"Please implement the `log_artifacts` method.") - - def get_artifact_uri(self, artifact_path=None): - """ - Get the absolute URI of the specified artifact in the currently active run. - - Parameters - ---------- - artifact_path : str - the directory in `artifact_uri` to write to. - - Returns - ------- - An absolute URI referring to the specified artifact or currently active Recorder. - """ - raise NotImplementedError(f"Please implement the `get_artifact_uri` method.") class MLflowRecorder(Recorder): ''' Use mlflow to implement a Recorder. + + Due to the fact that mlflow will only log artifact from a file or directory, we decide to + use file manager to help maintain the objects in the project. ''' + def __init__(self, experiment_id): + super(MLflowRecorder, self).__init__(experiment_id) + self.fm = None + self.temp_dir = None + def start_run(self, run_id=None, experiment_id=None, run_name=None, nested=False): if run_id is None: @@ -277,65 +163,67 @@ class MLflowRecorder(Recorder): self.recorder_id = run.info.run_id self.artifact_uri = run.info.artifact_uri # set up file manager for saving objects - if self.artifact_uri.startswith('file:/'): - self.fm = FileManager(Path(urllib.parse.urlparse(self.artifact_uri).path)) - else: - self.fm = FileManager(Path(self.artifact_uri)) - print(self.artifact_uri) + self.temp_dir = tempfile.mkdtemp() + self.fm = FileManager(Path(self.temp_dir).absolute()) return run def end_run(self): mlflow.end_run() + shutil.rmtree(self.temp_dir) - def save_object(self, name, data): - self.fm.save_obj(data, name) - import urllib - print(urllib.parse.urlparse(self.artifact_uri).scheme) - try: - self.log_artifact(self.fm.path / name) - except shutil.SameFileError: - pass - except Exception as e: - print(e) + def save_object(self, data, name, local_path=None): + if local_path is None: + assert data is not None and name is not None, "Please provide data and name input." + self.fm.save_obj(data, name) + mlflow.log_artifact(self.fm.path / name) + self.fm.remove(name) + else: + mlflow.log_artifact(local_path) - def save_objects(self, name_data_list): - self.fm.save_objs(name_data_list) - try: - self.log_artifacts(self.fm.path) - except shutil.SameFileError: - pass - except Exception as e: - print(e) + def save_objects(self, data_name_list, local_path=None): + if local_path is None: + assert data_name_list is not None, "Please provide data_name_list input." + self.fm.save_objs(data_name_list) + mlflow.log_artifacts(self.fm.path) + for obj, name in data_name_list: + self.fm.remove(name) + else: + mlflow.log_artifacts(local_path) def load_object(self, name): - return self.fm.load_obj(name) + client = mlflow.tracking.MlflowClient() + path = client.download_artifacts(self.recorder_id, name) + try: + with Path(path).open('rb') as f: + f.seek(0) + return pickle.load(f) + except: + with codecs.open(path, mode="r", encoding='utf-8') as f: + return f.read() + + def log_params(self, **kwargs): + keys = list(kwargs.keys()) + if len(keys) == 0: + mlflow.log_param(keys[0], kwargs.get(keys[0])) + else: + mlflow.log_params(dict(kwargs)) - def log_param(self, key, value): - mlflow.log_param(key, value) - - def log_params(self, params): - mlflow.log_params(params) - - def log_metric(self, key, value, step=None): - mlflow.log_metric(key, value, step) - - def log_metrics(self, metrics, step=None): - mlflow.log_metrics(metrics, step) + def log_metrics(self, step=None, **kwargs): + keys = list(kwargs.keys()) + if len(keys) == 0: + mlflow.log_metric(keys[0], kwargs.get(keys[0])) + else: + mlflow.log_metrics(dict(kwargs)) - def set_tag(self, key, value): - mlflow.set_tag(key, value) - - def set_tags(self, tags): - mlflow.set_tags(tags) + def set_tags(self, **kwargs): + keys = list(kwargs.keys()) + if len(keys) == 0: + mlflow.set_tag(keys[0], kwargs.get(keys[0])) + else: + mlflow.set_tags(dict(kwargs)) def delete_tag(self, key): mlflow.delete_tag(key) - - def log_artifact(self, local_path, artifact_path=None): - mlflow.log_artifact(local_path, artifact_path) - - def log_artifacts(self, local_dir, artifact_path=None): - mlflow.log_artifacts(local_dir, artifact_path) def get_artifact_uri(self, artifact_path=None): if self.artifact_uri is not None: From da9d1c8ac65ba4cfbf48acf2025a24a615f54a31 Mon Sep 17 00:00:00 2001 From: Jactus Date: Thu, 29 Oct 2020 13:22:49 +0800 Subject: [PATCH 010/241] Format with black --- qlib/__init__.py | 10 +-- qlib/config.py | 5 +- qlib/contrib/backtest/backtest.py | 9 +-- qlib/contrib/data/handler.py | 76 ++++++++++----------- qlib/contrib/data/processor.py | 5 +- qlib/contrib/evaluate.py | 6 +- qlib/contrib/model/gbdt.py | 51 ++++++++------- qlib/data/dataset/__init__.py | 25 ++++--- qlib/data/dataset/handler.py | 105 +++++++++++++++++------------- qlib/data/dataset/loader.py | 57 ++++++++-------- qlib/data/dataset/processor.py | 22 ++++--- qlib/data/dataset/utils.py | 3 +- qlib/model/base.py | 4 +- qlib/utils/__init__.py | 12 ++-- qlib/utils/objm.py | 15 +++-- qlib/utils/serial.py | 8 +-- qlib/workflow/__init__.py | 12 ++-- qlib/workflow/exp.py | 15 +++-- qlib/workflow/expm.py | 67 ++++++++++--------- qlib/workflow/record.py | 34 +++++----- 20 files changed, 290 insertions(+), 251 deletions(-) diff --git a/qlib/__init__.py b/qlib/__init__.py index f2b2c28ac..154d4ea08 100644 --- a/qlib/__init__.py +++ b/qlib/__init__.py @@ -39,7 +39,7 @@ def init(default_conf="client", **kwargs): LOG.info(f"default_conf: {default_conf}.") C.set_mode(default_conf) - C.set_region(kwargs.get('region', C['region'] if 'region' in C else REG_CN )) + C.set_region(kwargs.get("region", C["region"] if "region" in C else REG_CN)) for k, v in kwargs.items(): C[k] = v @@ -80,13 +80,13 @@ def init(default_conf="client", **kwargs): if "flask_server" in C: LOG.info(f"flask_server={C['flask_server']}, flask_port={C['flask_port']}") - + # set up QlibRecorder default_uri = str(Path(os.getcwd()).resolve() / "mlruns") - current_uri = C['exp_uri'] if C['exp_uri'] is not None else default_uri + current_uri = C["exp_uri"] if C["exp_uri"] is not None else default_uri # exp manager module - module = get_module_by_module_path('qlib.workflow') - exp_manager = init_instance_by_config(C['exp_manager'], module) + module = get_module_by_module_path("qlib.workflow") + exp_manager = init_instance_by_config(C["exp_manager"], module) qr = QlibRecorder(exp_manager, default_uri, current_uri) R.register(qr) diff --git a/qlib/config.py b/qlib/config.py index db5fab69c..0e2a264af 100644 --- a/qlib/config.py +++ b/qlib/config.py @@ -125,10 +125,7 @@ _default_config = { "loggers": {"qlib": {"level": "DEBUG", "handlers": ["console"]}}, }, # Defatult config for experiment manager - "exp_manager": { - "class": "MLflowExpManager", - "kwargs": {} - }, + "exp_manager": {"class": "MLflowExpManager", "kwargs": {}}, "exp_uri": None, } diff --git a/qlib/contrib/backtest/backtest.py b/qlib/contrib/backtest/backtest.py index a1b2e5bce..52e74e14b 100644 --- a/qlib/contrib/backtest/backtest.py +++ b/qlib/contrib/backtest/backtest.py @@ -46,10 +46,10 @@ def backtest(pred, strategy, trade_exchange, shift, verbose, account, benchmark) benchmark code, default is SH000905 CSI500 """ # Convert format if the input format is not expected - if get_level_index(pred, level='datetime') == 1: + if get_level_index(pred, level="datetime") == 1: pred = pred.swaplevel().sort_index() if isinstance(pred, pd.Series): - pred = pred.to_frame('score') + pred = pred.to_frame("score") trade_account = Account(init_cash=account) _pred_dates = pred.index.get_level_values(level="datetime") @@ -80,8 +80,9 @@ def backtest(pred, strategy, trade_exchange, shift, verbose, account, benchmark) # 1. Load the score_series at pred_date try: score = pred.loc(axis=0)[pred_date, :] # (trade_date, stock_id) multi_index, score in pdate - score_series = score.reset_index(level="datetime", - drop=True)["score"] # pd.Series(index:stock_id, data: score) + score_series = score.reset_index(level="datetime", drop=True)[ + "score" + ] # pd.Series(index:stock_id, data: score) except KeyError: LOG.warning("No score found on predict date[{:%Y-%m-%d}]".format(trade_date)) score_series = None diff --git a/qlib/contrib/data/handler.py b/qlib/contrib/data/handler.py index 45e4855c1..2e7f2febc 100644 --- a/qlib/contrib/data/handler.py +++ b/qlib/contrib/data/handler.py @@ -16,21 +16,16 @@ class ALPHA360(DataHandlerLP): "kwargs": { "config": { "feature": { - "price": { - "windows": range(60) - }, - "volume": { - "windows": range(60) - }, + "price": {"windows": range(60)}, + "volume": {"windows": range(60)}, }, - "label": self.get_label_config() + "label": self.get_label_config(), }, - } + }, } - infer_processors = [{ - "class": "ConfigSectionProcessor", - "module_path": "qlib.contrib.data.processor" - }] # ConfigSectionProcessor will normalize LABEL0 + infer_processors = [ + {"class": "ConfigSectionProcessor", "module_path": "qlib.contrib.data.processor"} + ] # ConfigSectionProcessor will normalize LABEL0 super().__init__(instruments, start_time, end_time, data_loader=data_loader, infer_processors=infer_processors) def get_label_config(self): @@ -49,12 +44,7 @@ class Alpha158(DataHandlerLP): start_time=None, end_time=None, infer_processors=[], - learn_processors=["DropnaLabel", { - "class": "CSZScoreNorm", - "kwargs": { - "fields_group": "label" - } - }], + learn_processors=["DropnaLabel", {"class": "CSZScoreNorm", "kwargs": {"fields_group": "label"}}], fit_start_time=None, fit_end_time=None, ): @@ -65,11 +55,13 @@ class Alpha158(DataHandlerLP): klass, pkwargs = get_cls_kwargs(p, processor_module) # FIXME: It's hard code here!!!!! if isinstance(klass, (MinMaxNorm, ZscoreNorm)): - assert (fit_start_time is not None and fit_end_time is not None) - pkwargs.update({ - "fit_start_time": fit_start_time, - "fit_end_time": fit_end_time, - }) + assert fit_start_time is not None and fit_end_time is not None + pkwargs.update( + { + "fit_start_time": fit_start_time, + "fit_end_time": fit_end_time, + } + ) new_l.append({"class": klass.__name__, "kwargs": pkwargs}) else: new_l.append(p) @@ -81,18 +73,17 @@ class Alpha158(DataHandlerLP): data_loader = { "class": "QlibDataLoader", "kwargs": { - "config": { - "feature": self.get_feature_config(), - "label": self.get_label_config() - }, - } + "config": {"feature": self.get_feature_config(), "label": self.get_label_config()}, + }, } - super().__init__(instruments, - start_time, - end_time, - data_loader=data_loader, - infer_processors=infer_processors, - learn_processors=learn_processors) + super().__init__( + instruments, + start_time, + end_time, + data_loader=data_loader, + infer_processors=infer_processors, + learn_processors=learn_processors, + ) def get_feature_config(self): conf = { @@ -247,7 +238,8 @@ class Alpha158(DataHandlerLP): if use("SUMD"): fields += [ "(Sum(Greater($close-Ref($close, 1), 0), %d)-Sum(Greater(Ref($close, 1)-$close, 0), %d))" - "/(Sum(Abs($close-Ref($close, 1)), %d)+1e-12)" % (d, d, d) for d in windows + "/(Sum(Abs($close-Ref($close, 1)), %d)+1e-12)" % (d, d, d) + for d in windows ] names += ["SUMD%d" % d for d in windows] if use("VMA"): @@ -258,26 +250,30 @@ class Alpha158(DataHandlerLP): names += ["VSTD%d" % d for d in windows] if use("WVMA"): fields += [ - "Std(Abs($close/Ref($close, 1)-1)*$volume, %d)/(Mean(Abs($close/Ref($close, 1)-1)*$volume, %d)+1e-12)" % - (d, d) for d in windows + "Std(Abs($close/Ref($close, 1)-1)*$volume, %d)/(Mean(Abs($close/Ref($close, 1)-1)*$volume, %d)+1e-12)" + % (d, d) + for d in windows ] names += ["WVMA%d" % d for d in windows] if use("VSUMP"): fields += [ - "Sum(Greater($volume-Ref($volume, 1), 0), %d)/(Sum(Abs($volume-Ref($volume, 1)), %d)+1e-12)" % (d, d) + "Sum(Greater($volume-Ref($volume, 1), 0), %d)/(Sum(Abs($volume-Ref($volume, 1)), %d)+1e-12)" + % (d, d) for d in windows ] names += ["VSUMP%d" % d for d in windows] if use("VSUMN"): fields += [ - "Sum(Greater(Ref($volume, 1)-$volume, 0), %d)/(Sum(Abs($volume-Ref($volume, 1)), %d)+1e-12)" % (d, d) + "Sum(Greater(Ref($volume, 1)-$volume, 0), %d)/(Sum(Abs($volume-Ref($volume, 1)), %d)+1e-12)" + % (d, d) for d in windows ] names += ["VSUMN%d" % d for d in windows] if use("VSUMD"): fields += [ "(Sum(Greater($volume-Ref($volume, 1), 0), %d)-Sum(Greater(Ref($volume, 1)-$volume, 0), %d))" - "/(Sum(Abs($volume-Ref($volume, 1)), %d)+1e-12)" % (d, d, d) for d in windows + "/(Sum(Abs($volume-Ref($volume, 1)), %d)+1e-12)" % (d, d, d) + for d in windows ] names += ["VSUMD%d" % d for d in windows] diff --git a/qlib/contrib/data/processor.py b/qlib/contrib/data/processor.py index 9fca094a4..35b242510 100644 --- a/qlib/contrib/data/processor.py +++ b/qlib/contrib/data/processor.py @@ -8,9 +8,10 @@ from ...data.dataset.processor import Processor, get_group_columns class ConfigSectionProcessor(Processor): - ''' + """ This processor is designed for Alpha158. And will be replaced by simple processors in the future - ''' + """ + def __init__(self, fields_group=None, **kwargs): super().__init__() # Options diff --git a/qlib/contrib/evaluate.py b/qlib/contrib/evaluate.py index 6dcefbb80..a4b6d87dc 100644 --- a/qlib/contrib/evaluate.py +++ b/qlib/contrib/evaluate.py @@ -159,11 +159,11 @@ def get_exchange( if deal_price[0] != "$": deal_price = "$" + deal_price if extract_codes: - codes = sorted(pred.index.get_level_values('instrument').unique()) + codes = sorted(pred.index.get_level_values("instrument").unique()) else: codes = "all" # TODO: We must ensure that 'all.txt' includes all the stocks - dates = sorted(pred.index.get_level_values('datetime').unique()) + dates = sorted(pred.index.get_level_values("datetime").unique()) dates = np.append(dates, get_date_range(dates[-1], shift=shift)) exchange = Exchange( @@ -298,7 +298,7 @@ def long_short_backtest( "short": short_returns(excess), "long_short": long_short_returns} """ - if get_level_index(pred, level='datetime') == 1: + if get_level_index(pred, level="datetime") == 1: pred = pred.swaplevel().sort_index() if trade_unit is None: diff --git a/qlib/contrib/model/gbdt.py b/qlib/contrib/model/gbdt.py index 2769f2282..61c617b8d 100644 --- a/qlib/contrib/model/gbdt.py +++ b/qlib/contrib/model/gbdt.py @@ -12,26 +12,29 @@ from ...data.dataset.handler import DataHandlerLP class LGBModel(Model): """LightGBM Model""" + def __init__(self, loss="mse", **kwargs): if loss not in {"mse", "binary"}: raise NotImplementedError - self._params = {'objective': loss} + self._params = {"objective": loss} self._params.update(kwargs) self.model = None - def fit(self, - dataset: DatasetH, - num_boost_round=1000, - early_stopping_rounds=50, - verbose_eval=20, - evals_result=dict(), - **kwargs): + def fit( + self, + dataset: DatasetH, + num_boost_round=1000, + early_stopping_rounds=50, + verbose_eval=20, + evals_result=dict(), + **kwargs + ): - df_train, df_valid = dataset.prepare(['train', 'valid'], - col_set=['feature', 'label'], - data_key=DataHandlerLP.DK_L) - x_train, y_train = df_train['feature'], df_train['label'] - x_valid, y_valid = df_valid['feature'], df_valid['label'] + df_train, df_valid = dataset.prepare( + ["train", "valid"], col_set=["feature", "label"], data_key=DataHandlerLP.DK_L + ) + x_train, y_train = df_train["feature"], df_train["label"] + x_valid, y_valid = df_valid["feature"], df_valid["label"] # Lightgbm need 1D array as its label if y_train.values.ndim == 2 and y_train.values.shape[1] == 1: @@ -41,20 +44,22 @@ class LGBModel(Model): dtrain = lgb.Dataset(x_train.values, label=y_train_1d) dvalid = lgb.Dataset(x_valid.values, label=y_valid_1d) - self.model = lgb.train(self._params, - dtrain, - num_boost_round=num_boost_round, - valid_sets=[dtrain, dvalid], - valid_names=["train", "valid"], - early_stopping_rounds=early_stopping_rounds, - verbose_eval=verbose_eval, - evals_result=evals_result, - **kwargs) + self.model = lgb.train( + self._params, + dtrain, + num_boost_round=num_boost_round, + valid_sets=[dtrain, dvalid], + valid_names=["train", "valid"], + early_stopping_rounds=early_stopping_rounds, + verbose_eval=verbose_eval, + evals_result=evals_result, + **kwargs + ) evals_result["train"] = list(evals_result["train"].values())[0] evals_result["valid"] = list(evals_result["valid"].values())[0] def predict(self, dataset): if self.model is None: raise ValueError("model is not fitted yet!") - x_test = dataset.prepare('test', col_set='feature') + x_test = dataset.prepare("test", col_set="feature") return pd.Series(self.model.predict(np.squeeze(x_test.values)), index=x_test.index) diff --git a/qlib/data/dataset/__init__.py b/qlib/data/dataset/__init__.py index fcf17546f..d5b8a12e9 100644 --- a/qlib/data/dataset/__init__.py +++ b/qlib/data/dataset/__init__.py @@ -6,11 +6,12 @@ import pandas as pd class Dataset(Serializable): - ''' + """ Preparing data for model training and inferencing. - ''' + """ + def __init__(self, *args, **kwargs): - ''' + """ init is designed to finish following steps - setup data - The data related attributes' names should start with '_' so that it will not be saved on disk when serializing @@ -18,7 +19,7 @@ class Dataset(Serializable): - The name of essential state for preparing data should not start with '_' so that it could be serialized on disk when serializing. The data could specify the info to caculate the essential data for preparation - ''' + """ self.setup_data(*args, **kwargs) super().__init__() @@ -51,14 +52,15 @@ class Dataset(Serializable): class DatasetH(Dataset): - ''' + """ Dataset with Data(H)anler User should try to put the data preprocessing functions into handler. Only following data processing functions should be placed in Dataset - The processing is related to specific model. - The processing is related to data split - ''' + """ + def __init__(self, handler: Union[dict, DataHandler], segments: list): """ Parameters @@ -96,10 +98,9 @@ class DatasetH(Dataset): self._handler = init_instance_by_config(handler, accept_types=DataHandler) self._segments = segments - def prepare(self, - segments: Union[List[str], Tuple[str], str, slice], - col_set=DataHandler.CS_ALL, - **kwargs) -> Union[List[pd.DataFrame], pd.DataFrame]: + def prepare( + self, segments: Union[List[str], Tuple[str], str, slice], col_set=DataHandler.CS_ALL, **kwargs + ) -> Union[List[pd.DataFrame], pd.DataFrame]: """ prepare the data for learning and inference @@ -124,9 +125,7 @@ class DatasetH(Dataset): [TODO:description] """ if isinstance(segments, (list, tuple)): - return [ - self._handler.fetch(slice(*self._segments[seg]), col_set=col_set, **kwargs) for seg in segments - ] + return [self._handler.fetch(slice(*self._segments[seg]), col_set=col_set, **kwargs) for seg in segments] elif isinstance(segments, str): return self._handler.fetch(slice(*self._segments[segments]), col_set=col_set, **kwargs) else: diff --git a/qlib/data/dataset/handler.py b/qlib/data/dataset/handler.py index 4f33ae73c..04715c892 100644 --- a/qlib/data/dataset/handler.py +++ b/qlib/data/dataset/handler.py @@ -25,7 +25,7 @@ from . import loader as data_loader_module # TODO: A more general handler interface which does not relies on internal pd.DataFrame is needed. class DataHandler(Serializable): - ''' + """ The steps to using a handler 1. initialized data handler (call by `init`). 2. use the data @@ -46,13 +46,21 @@ class DataHandler(Serializable): SH600004 13.313329 11800983.0 13.313329 13.317701 0.183632 0.0042 SH600005 37.796539 12231662.0 38.258602 37.919757 0.970325 0.0289 - ''' - def __init__(self, instruments, start_time=None, end_time=None, data_loader: Tuple[dict, str, DataLoader]=None, init_data=True): + """ + + def __init__( + self, + instruments, + start_time=None, + end_time=None, + data_loader: Tuple[dict, str, DataLoader] = None, + init_data=True, + ): # Set logger self.logger = get_module_logger("DataHandler") # Setup data loader - assert(data_loader is not None) # to make start_time end_time could have None default value + assert data_loader is not None # to make start_time end_time could have None default value self.data_loader = init_instance_by_config(data_loader, data_loader_module, accept_types=DataLoader) self.instruments = instruments @@ -62,7 +70,7 @@ class DataHandler(Serializable): self.init() super().__init__() - def init(self, enable_cache: bool=True): + def init(self, enable_cache: bool = True): """ initialize the data. In case of running intialization for multiple time, it will do nothing for the second time. @@ -83,7 +91,9 @@ class DataHandler(Serializable): self._data = self.data_loader.load(self.instruments, self.start_time, self.end_time) # TODO: cache - def _fetch_df_by_index(self, df: pd.DataFrame, selector: Union[pd.Timestamp, slice, str, list], level: Union[str, int]) -> pd.DataFrame: + def _fetch_df_by_index( + self, df: pd.DataFrame, selector: Union[pd.Timestamp, slice, str, list], level: Union[str, int] + ) -> pd.DataFrame: """ fetch data from `data` with `selector` and `level` @@ -100,7 +110,7 @@ class DataHandler(Serializable): idx_slc = idx_slc[1], idx_slc[0] return df.loc(axis=0)[idx_slc] - CS_ALL = '__all' + CS_ALL = "__all" def _fetch_df_by_col(self, df: pd.DataFrame, col_set: str) -> pd.DataFrame: cln = len(df.columns.levels) @@ -111,10 +121,12 @@ class DataHandler(Serializable): else: return df.loc(axis=1)[col_set] - def fetch(self, - selector: Union[pd.Timestamp, slice, str], - level: Union[str, int] = 'datetime', - col_set: Union[str, List[str]] = CS_ALL) -> pd.DataFrame: + def fetch( + self, + selector: Union[pd.Timestamp, slice, str], + level: Union[str, int] = "datetime", + col_set: Union[str, List[str]] = CS_ALL, + ) -> pd.DataFrame: """ fetch data from underlying data source @@ -157,32 +169,35 @@ class DataHandler(Serializable): class DataHandlerLP(DataHandler): - ''' + """ DataHandler with **(L)earnable (P)rocessor** - ''' + """ + # data key - DK_R = 'raw' - DK_I = 'infer' - DK_L = 'learn' + DK_R = "raw" + DK_I = "infer" + DK_L = "learn" # process type - PTYPE_I = 'independent' + PTYPE_I = "independent" # - _proc_infer_df will processed by infer_processors # - _proc_learn_df will be processed by learn_processors - PTYPE_A = 'append' + PTYPE_A = "append" # - _proc_infer_df will processed by infer_processors # - _proc_learn_df will be processed by infer_processors + learn_processors # - (e.g. _proc_infer_df processed by learn_processors ) - def __init__(self, - instruments, - start_time=None, - end_time=None, - data_loader: Tuple[dict, str, DataLoader] = None, - infer_processors=[], - learn_processors=[], - process_type=PTYPE_A, - **kwargs): + def __init__( + self, + instruments, + start_time=None, + end_time=None, + data_loader: Tuple[dict, str, DataLoader] = None, + infer_processors=[], + learn_processors=[], + process_type=PTYPE_A, + **kwargs, + ): """ Parameters ---------- @@ -217,10 +232,11 @@ class DataHandlerLP(DataHandler): # Setup preprocessor self.infer_processors = [] # for lint self.learn_processors = [] # for lint - for pname in 'infer_processors', 'learn_processors': + for pname in "infer_processors", "learn_processors": for proc in locals()[pname]: - getattr(self, pname).append(init_instance_by_config(proc, processor_module, - accept_types=(processor_module.Processor,))) + getattr(self, pname).append( + init_instance_by_config(proc, processor_module, accept_types=(processor_module.Processor,)) + ) self.process_type = process_type super().__init__(instruments, start_time, end_time, data_loader, **kwargs) @@ -240,8 +256,7 @@ class DataHandlerLP(DataHandler): """ self.process_data(with_fit=True) - - def process_data(self, with_fit: bool=False): + def process_data(self, with_fit: bool = False): """ process_data data. Fun `processor.fit` if necessary @@ -281,11 +296,11 @@ class DataHandlerLP(DataHandler): self._learn = _learn_df # init type - IT_FIT_SEQ = 'fit_seq' # the input of `fit` will be the output of the previous processor - IT_FIT_IND = 'fit_ind' # the input of `fit` will be the original df - IT_LS = 'load_state' # The state of the object has been load by pickle + IT_FIT_SEQ = "fit_seq" # the input of `fit` will be the output of the previous processor + IT_FIT_IND = "fit_ind" # the input of `fit` will be the original df + IT_LS = "load_state" # The state of the object has been load by pickle - def init(self, init_type: str=IT_FIT_SEQ, enable_cache: bool=False): + def init(self, init_type: str = IT_FIT_SEQ, enable_cache: bool = False): """ Initialize the data of Qlib @@ -314,15 +329,17 @@ class DataHandlerLP(DataHandler): # TODO: Be able to cache handler data. Save the memory for data processing - def _get_df_by_key(self, data_key: str=DK_I) -> pd.DataFrame: - df = getattr(self, {self.DK_R: '_data', self.DK_I: "_infer", self.DK_L: "_learn"}[data_key]) + def _get_df_by_key(self, data_key: str = DK_I) -> pd.DataFrame: + df = getattr(self, {self.DK_R: "_data", self.DK_I: "_infer", self.DK_L: "_learn"}[data_key]) return df - def fetch(self, - selector: Union[pd.Timestamp, slice, str], - level: Union[str, int] = 'datetime', - col_set=DataHandler.CS_ALL, - data_key: str = DK_I) -> pd.DataFrame: + def fetch( + self, + selector: Union[pd.Timestamp, slice, str], + level: Union[str, int] = "datetime", + col_set=DataHandler.CS_ALL, + data_key: str = DK_I, + ) -> pd.DataFrame: """ fetch data from underlying data source @@ -345,7 +362,7 @@ class DataHandlerLP(DataHandler): df = self._fetch_df_by_index(df, selector, level) return self._fetch_df_by_col(df, col_set) - def get_cols(self, col_set=DataHandler.CS_ALL, data_key: str=DK_I) -> list: + def get_cols(self, col_set=DataHandler.CS_ALL, data_key: str = DK_I) -> list: """ get the column names diff --git a/qlib/data/dataset/loader.py b/qlib/data/dataset/loader.py index b94280a83..e4f2f8619 100644 --- a/qlib/data/dataset/loader.py +++ b/qlib/data/dataset/loader.py @@ -8,44 +8,46 @@ from typing import Tuple class DataLoader(ABC): - ''' + """ DataLoader is designed for loading raw data from original data source. - ''' + """ + @abstractmethod def load(self, instruments, start_time=None, end_time=None) -> pd.DataFrame: """ - load the data as pd.DataFrame + load the data as pd.DataFrame - Parameters - ---------- - self : [TODO:type] - [TODO:description] - instruments : [TODO:type] - [TODO:description] - start_time : [TODO:type] - [TODO:description] - end_time : [TODO:type] - [TODO:description] + Parameters + ---------- + self : [TODO:type] + [TODO:description] + instruments : [TODO:type] + [TODO:description] + start_time : [TODO:type] + [TODO:description] + end_time : [TODO:type] + [TODO:description] - Returns - ------- - pd.DataFrame: - data load from the under layer source + Returns + ------- + pd.DataFrame: + data load from the under layer source - Example of the data: - The multi-index of the columns is optional. - feature label - $close $volume Ref($close, 1) Mean($close, 3) $high-$low LABEL0 - datetime instrument - 2010-01-04 SH600000 81.807068 17145150.0 83.737389 83.016739 2.741058 0.0032 - SH600004 13.313329 11800983.0 13.313329 13.317701 0.183632 0.0042 - SH600005 37.796539 12231662.0 38.258602 37.919757 0.970325 0.0289 + Example of the data: + The multi-index of the columns is optional. + feature label + $close $volume Ref($close, 1) Mean($close, 3) $high-$low LABEL0 + datetime instrument + 2010-01-04 SH600000 81.807068 17145150.0 83.737389 83.016739 2.741058 0.0032 + SH600004 13.313329 11800983.0 13.313329 13.317701 0.183632 0.0042 + SH600005 37.796539 12231662.0 38.258602 37.919757 0.970325 0.0289 """ pass class QlibDataLoader(DataLoader): - '''Same as QlibDataLoader. The fields can be define by config''' + """Same as QlibDataLoader. The fields can be define by config""" + def __init__(self, config: Tuple[list, tuple, dict], filter_pipe=None): """ Parameters @@ -65,7 +67,7 @@ class QlibDataLoader(DataLoader): Here is a few examples to describe the fields TODO: """ - self.is_group = isinstance(config, dict) + self.is_group = isinstance(config, dict) if self.is_group: self.fields = {grp: self._parse_fields_info(fields_info) for grp, fields_info in config.items()} @@ -88,6 +90,7 @@ class QlibDataLoader(DataLoader): df = D.features(D.instruments(instruments, filter_pipe=self.filter_pipe), exprs, start_time, end_time) df.columns = names return df + if self.is_group: df = pd.concat({grp: _get_df(exprs, names) for grp, (exprs, names) in self.fields.items()}, axis=1) else: diff --git a/qlib/data/dataset/processor.py b/qlib/data/dataset/processor.py index 2ab012de2..3fc91f52c 100644 --- a/qlib/data/dataset/processor.py +++ b/qlib/data/dataset/processor.py @@ -30,8 +30,7 @@ def get_group_columns(df: pd.DataFrame, group: str): class Processor(Serializable): - - def fit(self, df: pd.DataFrame=None): + def fit(self, df: pd.DataFrame = None): """ learn data processing parameters @@ -40,7 +39,7 @@ class Processor(Serializable): df : pd.DataFrame When we fit and process data with processor one by one. The fit function reiles on the output of previous processor, i.e. `df`. - + """ pass @@ -81,16 +80,17 @@ class DropnaProcessor(Processor): class DropnaLabel(DropnaProcessor): - def __init__(self, group='label'): + def __init__(self, group="label"): super().__init__(group=group) def is_for_infer(self) -> bool: - '''The samples are dropped according to label. So it is not usable for inference''' + """The samples are dropped according to label. So it is not usable for inference""" return False class ProcessInf(Processor): - '''Process infinity ''' + """Process infinity """ + def __call__(self, df): def replace_inf(data): def process_inf(df): @@ -102,6 +102,7 @@ class ProcessInf(Processor): data = data.groupby("datetime").apply(process_inf) data.sort_index(inplace=True) return data + return replace_inf(df) @@ -126,6 +127,7 @@ class MinMaxNorm(Processor): if not ignore[i]: x[i] = (x[i] - min_val) / (max_val - min_val) return x + df.loc(axis=1)[self.cols] = normalize(df[self.cols].values) return df @@ -151,17 +153,19 @@ class ZscoreNorm(Processor): if not ignore[i]: x[i] = (x[i] - mean_train) / std_train return x + df.loc(axis=1)[self.cols] = normalize(df[self.cols].values) return df class CSZScoreNorm(Processor): - '''Cross Sectional ZScore Normalization''' + """Cross Sectional ZScore Normalization""" + def __init__(self, fields_group=None): self.fields_group = fields_group def __call__(self, df): # try not modify original dataframe - cols = get_group_columns(df,self.fields_group) - df[cols] = df[cols].groupby('datetime').apply(lambda df: (df - df.mean()).div(df.std())) + cols = get_group_columns(df, self.fields_group) + df[cols] = df[cols].groupby("datetime").apply(lambda df: (df - df.mean()).div(df.std())) return df diff --git a/qlib/data/dataset/utils.py b/qlib/data/dataset/utils.py index c97256896..af0900867 100644 --- a/qlib/data/dataset/utils.py +++ b/qlib/data/dataset/utils.py @@ -24,9 +24,8 @@ def get_level_index(df: pd.DataFrame, level=Union[str, int]) -> int: return df.index.names.index(level) except (AttributeError, ValueError): # NOTE: If level index is not given in the data, the default level index will be ('datetime', 'instrument') - return ('datetime', 'instrument').index(level) + return ("datetime", "instrument").index(level) elif isinstance(level, int): return level else: raise NotImplementedError(f"This type of input is not supported") - diff --git a/qlib/model/base.py b/qlib/model/base.py index 11bd76d06..3a6ad504e 100644 --- a/qlib/model/base.py +++ b/qlib/model/base.py @@ -6,7 +6,7 @@ from ..data.dataset import Dataset class BaseModel(Serializable, metaclass=abc.ABCMeta): - '''Modeling things''' + """Modeling things""" @abc.abstractmethod def predict(self, *args, **kwargs) -> object: @@ -19,7 +19,7 @@ class BaseModel(Serializable, metaclass=abc.ABCMeta): class Model(BaseModel): - '''Learnable Models''' + """Learnable Models""" def fit(self, dataset: Dataset): """ diff --git a/qlib/utils/__init__.py b/qlib/utils/__init__.py index ef7bf63d6..87b43f456 100644 --- a/qlib/utils/__init__.py +++ b/qlib/utils/__init__.py @@ -165,7 +165,7 @@ def get_module_by_module_path(module_path): return module -def get_cls_kwargs(config: Union[dict, str], module) -> (type, dict): +def get_cls_kwargs(config: Union[dict, str], module) -> (type, dict): """ extract class and kwargs from config info @@ -184,8 +184,8 @@ def get_cls_kwargs(config: Union[dict, str], module) -> (type, dict): """ if isinstance(config, dict): # raise AttributeError - klass = getattr(module, config['class']) - kwargs = config['kwargs'] + klass = getattr(module, config["class"]) + kwargs = config["kwargs"] elif isinstance(config, str): klass = getattr(module, config) kwargs = {} @@ -194,7 +194,9 @@ def get_cls_kwargs(config: Union[dict, str], module) -> (type, dict): return klass, kwargs -def init_instance_by_config(config: Union[str, dict], module=None, accept_types: Union[type, Tuple[type]]=tuple([])) -> object: +def init_instance_by_config( + config: Union[str, dict], module=None, accept_types: Union[type, Tuple[type]] = tuple([]) +) -> object: """ get initialized instance with config @@ -647,4 +649,4 @@ def register_wrapper(wrapper, cls_or_obj): module = get_module_by_module_path("qlib.data") cls_or_obj = getattr(module, cls_or_obj) obj = cls_or_obj() if isinstance(cls_or_obj, type) else cls_or_obj - wrapper.register(obj) \ No newline at end of file + wrapper.register(obj) diff --git a/qlib/utils/objm.py b/qlib/utils/objm.py index d7c4f4cb1..eebd529c6 100644 --- a/qlib/utils/objm.py +++ b/qlib/utils/objm.py @@ -24,7 +24,7 @@ class ObjManager: def save_objs(self, obj_name_l): """ - save objects + save objects Parameters ---------- @@ -88,9 +88,10 @@ class ObjManager: class FileManager(ObjManager): - ''' + """ Use file system to manage objects - ''' + """ + def __init__(self, path=None): if path is None: self.path = Path(self.create_path()) @@ -99,12 +100,12 @@ class FileManager(ObjManager): def create_path(self) -> str: try: - return tempfile.mkdtemp(prefix=str(C['file_manager_path']) + os.sep) + return tempfile.mkdtemp(prefix=str(C["file_manager_path"]) + os.sep) except AttributeError: raise NotImplementedError(f"If path is not given, the `create_path` function should be implemented") def save_obj(self, obj, name): - with (self.path / name).open('wb') as f: + with (self.path / name).open("wb") as f: pickle.dump(obj, f) def save_objs(self, obj_name_l): @@ -112,7 +113,7 @@ class FileManager(ObjManager): self.save_obj(obj, name) def load_obj(self, name): - with (self.path / name).open('rb') as f: + with (self.path / name).open("rb") as f: return pickle.load(f) def exists(self, name): @@ -123,7 +124,7 @@ class FileManager(ObjManager): def remove(self, fname=None): if fname is None: - for fp in self.path.glob('*'): + for fp in self.path.glob("*"): fp.unlink() self.path.rmdir() else: diff --git a/qlib/utils/serial.py b/qlib/utils/serial.py index a4825615f..04781d655 100644 --- a/qlib/utils/serial.py +++ b/qlib/utils/serial.py @@ -6,17 +6,17 @@ import pickle class Serializable: - ''' + """ Serializable behaves like pickle. But it only save the state whose name starts with `_` - ''' + """ def __getstate__(self) -> dict: - return {k: v for k, v in self.__dict__.items() if k.startswith('_') } + return {k: v for k, v in self.__dict__.items() if k.startswith("_")} def __setstate__(self, state: dict): self.__dict__.update(state) def to_pickle(self, path: [Path, str]): - with Path(path).open('wb') as f: + with Path(path).open("wb") as f: pickle.dump(self, f) diff --git a/qlib/workflow/__init__.py b/qlib/workflow/__init__.py index 06a646c84..7c9c1928f 100644 --- a/qlib/workflow/__init__.py +++ b/qlib/workflow/__init__.py @@ -5,6 +5,7 @@ from contextlib import contextmanager from .expm import * from ..utils import Wrapper + class QlibRecorder: def __init__(self, exp_manager, default_uri, current_uri): self.exp_manager = exp_manager @@ -16,16 +17,16 @@ class QlibRecorder: run = self.start_exp(experiment_name, self.current_uri) yield run self.end_exp() - + def start_exp(self, experiment_name=None): - return self.exp_manager.start_exp(experiment_name, self.current_uri) + return self.exp_manager.start_exp(experiment_name, self.current_uri) def end_exp(self): self.exp_manager.end_exp() - + def search_records(self, experiment_ids, **kwargs): return self.exp_manager.search_records(experiment_ids, **kwargs) - + def get_exp(self, experiment_id=None, experiment_name=None): return self.exp_manager.get_exp(experiment_id, experiment_name) @@ -52,12 +53,13 @@ class QlibRecorder: def log_metrics(self, step=None, **kwargs): self.exp_manager.active_recorder.log_metrics(step, **kwargs) - + def set_tags(self, **kwargs): self.exp_manager.active_recorder.set_tags(**kwargs) def delete_tag(self, key): self.exp_manager.active_recorder.delete_tag(key) + # global record R = Wrapper() diff --git a/qlib/workflow/exp.py b/qlib/workflow/exp.py index 9e076aced..a63187e28 100644 --- a/qlib/workflow/exp.py +++ b/qlib/workflow/exp.py @@ -4,10 +4,12 @@ import mlflow from pathlib import Path + class Experiment: """ - Thie is the `Experiment` class for each experiment being run. The API is designed + Thie is the `Experiment` class for each experiment being run. The API is designed """ + def __init__(self): self.name = None self.id = None @@ -39,9 +41,10 @@ class MLflowExperiment(Experiment): """ Use mlflow to implement Experiment. """ + def search_records(self, **kwargs): - filter_string = '' if kwargs.get('filter_string') is None else kwargs.get('filter_string') - run_view_type = 1 if kwargs.get('run_view_type') is None else kwargs.get('run_view_type') - max_results = 100000 if kwargs.get('max_results') is None else kwargs.get('max_results') - order_by = kwargs.get('order_by') - return mlflow.search_runs([self.experiment_id], filter_string, run_view_type, max_results, order_by) \ No newline at end of file + filter_string = "" if kwargs.get("filter_string") is None else kwargs.get("filter_string") + run_view_type = 1 if kwargs.get("run_view_type") is None else kwargs.get("run_view_type") + max_results = 100000 if kwargs.get("max_results") is None else kwargs.get("max_results") + order_by = kwargs.get("order_by") + return mlflow.search_runs([self.experiment_id], filter_string, run_view_type, max_results, order_by) diff --git a/qlib/workflow/expm.py b/qlib/workflow/expm.py index 36a945f42..00d25da48 100644 --- a/qlib/workflow/expm.py +++ b/qlib/workflow/expm.py @@ -8,15 +8,17 @@ from contextlib import contextmanager from .exp import MLflowExperiment from .record import MLflowRecorder + class ExpManager: """ This is the `ExpManager` class for managing the experiments. The API is designed similar to mlflow. (The link: https://mlflow.org/docs/latest/python_api/mlflow.html) """ + def __init__(self): self.default_uri = None - self.active_recorder = None # only one recorder can running each time - self.experiments = dict() # store the experiment name --> Experiment object + self.active_recorder = None # only one recorder can running each time + self.experiments = dict() # store the experiment name --> Experiment object def start_exp(self, experiment_name=None, uri=None, **kwargs): """ @@ -88,7 +90,7 @@ class ExpManager: An experiment object. """ raise NotImplementedError(f"Please implement the `create_exp` method.") - + def get_exp(self, experiment_id=None, experiment_name=None): """ Retrieve an experiment by experiment_id from the backend store. @@ -111,7 +113,7 @@ class ExpManager: Parameters ---------- experiment_id : str - the experiment id. + the experiment id. """ raise NotImplementedError(f"Please implement the `create_exp` method.") @@ -142,12 +144,13 @@ class ExpManager: An Recorder object. """ raise NotImplementedError(f"Please implement the `get_recorder` method.") - + class MLflowExpManager(ExpManager): - ''' + """ Use mlflow to implement ExpManager. - ''' + """ + def __init__(self): super(MLflowExpManager, self).__init__() self.default_uri = None @@ -169,27 +172,31 @@ class MLflowExpManager(ExpManager): def end_exp(self): self.active_recorder.end_run() self.active_recorder = None - + def __create_exp(self, experiment_name=None, uri=None): # init experiment experiment = MLflowExperiment() # set the tracking uri if uri is None: - print('No tracking URI is provided. The default tracking URI is set as `mlruns` under the working directory.') + print( + "No tracking URI is provided. The default tracking URI is set as `mlruns` under the working directory." + ) else: self.current_uri = uri mlflow.set_tracking_uri(self.current_uri) # start the experiment if experiment_name is None: - print('No experiment name provided. The default experiment name is set as `experiment`.') - experiment_id = mlflow.create_experiment('experiment') + print("No experiment name provided. The default experiment name is set as `experiment`.") + experiment_id = mlflow.create_experiment("experiment") # set the active experiment - mlflow.set_experiment('experiment') - experiment_name = 'experiment' + mlflow.set_experiment("experiment") + experiment_name = "experiment" else: if experiment_name not in self.experiments: if mlflow.get_experiment_by_name(experiment_name) is not None: - raise Exception('The experiment has already been created before. Please pick another name or delete the files under uri.') + raise Exception( + "The experiment has already been created before. Please pick another name or delete the files under uri." + ) experiment_id = mlflow.create_experiment(experiment_name) else: experiment_id = self.experiments[experiment_name].id @@ -197,40 +204,42 @@ class MLflowExpManager(ExpManager): # set the active experiment mlflow.set_experiment(experiment_name) # set up experiment - experiment.id = experiment_id + experiment.id = experiment_id experiment.name = experiment_name return experiment - + def search_records(self, experiment_ids, **kwargs): - filter_string = '' if kwargs.get('filter_string') is None else kwargs.get('filter_string') - run_view_type = 1 if kwargs.get('run_view_type') is None else kwargs.get('run_view_type') - max_results = 100000 if kwargs.get('max_results') is None else kwargs.get('max_results') - order_by = kwargs.get('order_by') + filter_string = "" if kwargs.get("filter_string") is None else kwargs.get("filter_string") + run_view_type = 1 if kwargs.get("run_view_type") is None else kwargs.get("run_view_type") + max_results = 100000 if kwargs.get("max_results") is None else kwargs.get("max_results") + order_by = kwargs.get("order_by") return mlflow.search_runs(experiment_ids, filter_string, run_view_type, max_results, order_by) - + def get_exp(self, experiment_id=None, experiment_name=None): - assert experiment_id is not None or experiment_name is not None, 'Please provide at least one of the experiment id or name to retrieve an experiment.' + assert ( + experiment_id is not None or experiment_name is not None + ), "Please provide at least one of the experiment id or name to retrieve an experiment." if experiment_name is not None: return self.experiments[experiment_name] - elif: + elif experiment_id is not None: for name in self.experiments: if self.experiments[name].id == experiment_id: return self.experiments[name] else: - print('No valid experiment is found. Please make sure the id and name are correctly given.') + print("No valid experiment is found. Please make sure the id and name are correctly given.") def delete_exp(self, experiment_id): mlflow.delete_experiment(experiment_id) - self.experiments = {key:val for key, val in self.experiments.items() if val.id != experiment_id} + self.experiments = {key: val for key, val in self.experiments.items() if val.id != experiment_id} def get_uri(self, type): - if uri == 'default': + if uri == "default": return self.default_uri - elif uri == 'current': + elif uri == "current": return self.current_uri else: - raise ValueError('Input type is not supported. Please choose type default or current to get the uri.') + raise ValueError("Input type is not supported. Please choose type default or current to get the uri.") def get_recorder(self): - return self.active_recorder \ No newline at end of file + return self.active_recorder diff --git a/qlib/workflow/record.py b/qlib/workflow/record.py index 071c92691..e132710ca 100644 --- a/qlib/workflow/record.py +++ b/qlib/workflow/record.py @@ -6,6 +6,7 @@ import shutil, os, pickle, tempfile, codecs from pathlib import Path from ..utils.objm import FileManager + class Recorder: """ This is the `Recorder` class for logging the experiments. The API is designed similar to mlflow. @@ -16,7 +17,7 @@ class Recorder: self.experiment_id = experiment_id self.recorder_id = None self.recorder_name = None - + def set_recorder_name(self, rname): self.recorder_name = rname @@ -63,10 +64,9 @@ class Recorder: """ raise NotImplementedError(f"Please implement the `load_object` method.") - def start_run(self, run_id=None, experiment_id=None, - run_name=None, nested=False): + def start_run(self, run_id=None, experiment_id=None, run_name=None, nested=False): """ - Start running the Recorder. The return value can be used as a context manager within a `with` block; + Start running the Recorder. The return value can be used as a context manager within a `with` block; otherwise, you must call end_run() to terminate the current run. (See `ActiveRun` class in mlflow) Parameters @@ -85,7 +85,7 @@ class Recorder: An active running object (e.g. mlflow.ActiveRun object). """ raise NotImplementedError(f"Please implement the `start_run` method.") - + def end_run(self): """ End an active Recorder. @@ -138,19 +138,19 @@ class Recorder: class MLflowRecorder(Recorder): - ''' + """ Use mlflow to implement a Recorder. - Due to the fact that mlflow will only log artifact from a file or directory, we decide to + Due to the fact that mlflow will only log artifact from a file or directory, we decide to use file manager to help maintain the objects in the project. - ''' + """ + def __init__(self, experiment_id): super(MLflowRecorder, self).__init__(experiment_id) self.fm = None self.temp_dir = None - def start_run(self, run_id=None, experiment_id=None, - run_name=None, nested=False): + def start_run(self, run_id=None, experiment_id=None, run_name=None, nested=False): if run_id is None: run_id = self.recorder_id if experiment_id is None: @@ -166,7 +166,7 @@ class MLflowRecorder(Recorder): self.temp_dir = tempfile.mkdtemp() self.fm = FileManager(Path(self.temp_dir).absolute()) return run - + def end_run(self): mlflow.end_run() shutil.rmtree(self.temp_dir) @@ -194,13 +194,13 @@ class MLflowRecorder(Recorder): client = mlflow.tracking.MlflowClient() path = client.download_artifacts(self.recorder_id, name) try: - with Path(path).open('rb') as f: + with Path(path).open("rb") as f: f.seek(0) return pickle.load(f) except: - with codecs.open(path, mode="r", encoding='utf-8') as f: - return f.read() - + with codecs.open(path, mode="r", encoding="utf-8") as f: + return f.read() + def log_params(self, **kwargs): keys = list(kwargs.keys()) if len(keys) == 0: @@ -214,7 +214,7 @@ class MLflowRecorder(Recorder): mlflow.log_metric(keys[0], kwargs.get(keys[0])) else: mlflow.log_metrics(dict(kwargs)) - + def set_tags(self, **kwargs): keys = list(kwargs.keys()) if len(keys) == 0: @@ -228,4 +228,4 @@ class MLflowRecorder(Recorder): def get_artifact_uri(self, artifact_path=None): if self.artifact_uri is not None: return self.artifact_uri - return mlflow.get_artifact_uri(artifact_path) \ No newline at end of file + return mlflow.get_artifact_uri(artifact_path) From 72b5d9abfa091af88b0dbac5e9bd075969c6386d Mon Sep 17 00:00:00 2001 From: Dong Zhou Date: Fri, 30 Oct 2020 11:02:32 +0800 Subject: [PATCH 011/241] fix ops & EMA support alpha --- qlib/data/_libs/expanding.pyx | 12 ++++++------ qlib/data/ops.py | 17 +++++++++++++++-- 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/qlib/data/_libs/expanding.pyx b/qlib/data/_libs/expanding.pyx index 76b824c94..47bc49610 100644 --- a/qlib/data/_libs/expanding.pyx +++ b/qlib/data/_libs/expanding.pyx @@ -14,7 +14,7 @@ cdef class Expanding(object): cdef int na_count def __init__(self): self.na_count = 0 - + cdef double update(self, double val): pass @@ -25,7 +25,7 @@ cdef class Mean(Expanding): def __init__(self): super(Mean, self).__init__() self.vsum = 0 - + cdef double update(self, double val): self.barv.push_back(val) if isnan(val): @@ -62,7 +62,7 @@ cdef class Slope(Expanding): return (N*self.xy_sum - self.x_sum*self.y_sum) / \ (N*self.x2_sum - self.x_sum*self.x_sum) - + cdef class Resi(Expanding): """1-D array expanding residuals""" cdef double x_sum @@ -94,7 +94,7 @@ cdef class Resi(Expanding): interp = y_mean - slope*x_mean return val - (slope*size + interp) - + cdef class Rsquare(Expanding): """1-D array expanding rsquare""" cdef double x_sum @@ -117,7 +117,7 @@ cdef class Rsquare(Expanding): self.na_count += 1 else: self.x_sum += size - self.x2_sum += size + self.x2_sum += size * size self.y_sum += val self.y2_sum += val * val self.xy_sum += size * val @@ -126,7 +126,7 @@ cdef class Rsquare(Expanding): sqrt((N*self.x2_sum - self.x_sum*self.x_sum) * (N*self.y2_sum - self.y_sum*self.y_sum)) return rvalue * rvalue - + cdef np.ndarray[double, ndim=1] expanding(Expanding r, np.ndarray a): cdef int i cdef int N = len(a) diff --git a/qlib/data/ops.py b/qlib/data/ops.py index 9f66a88af..d9c657595 100644 --- a/qlib/data/ops.py +++ b/qlib/data/ops.py @@ -8,6 +8,8 @@ from __future__ import print_function import numpy as np import pandas as pd +from scipy.stats import percentileofscore + from .base import Expression, ExpressionOps from ..log import get_module_logger @@ -687,6 +689,8 @@ class Rolling(ExpressionOps): # isnull = series.isnull() # NOTE: isnull = NaN, inf is not null if self.N == 0: series = getattr(series.expanding(min_periods=1), self.func)() + elif 0 < self.N < 1: + series = series.ewm(alpha=self.N, min_periods=1).mean() else: series = getattr(series.rolling(self.N, min_periods=1), self.func)() # series.iloc[:self.N-1] = np.nan @@ -696,6 +700,8 @@ class Rolling(ExpressionOps): def get_longest_back_rolling(self): if self.N == 0: return np.inf + if 0 < self.N < 1: + return int(np.log(1e-6) / np.log(1 - self.N)) # (1 - N)**window == 1e-6 return self.feature.get_longest_back_rolling() + self.N - 1 def get_extended_window_size(self): @@ -704,6 +710,11 @@ class Rolling(ExpressionOps): # remove such support for N == 0? get_module_logger(self.__class__.__name__).warning("The Rolling(ATTR, 0) will not be accurately calculated") return self.feature.get_extended_window_size() + elif 0 < self.N < 1: + lft_etd, rght_etd = self.feature.get_extended_window_size() + size = int(np.log(1e-6) / np.log(1 - self.N)) + lft_etd = max(lft_etd + size - 1, lft_etd) + return lft_etd, rght_etd else: lft_etd, rght_etd = self.feature.get_extended_window_size() lft_etd = max(lft_etd + self.N - 1, lft_etd) @@ -1087,7 +1098,7 @@ class Rank(Rolling): x1 = x[~np.isnan(x)] if x1.shape[0] == 0: return np.nan - return (x1.argsort()[-1] + 1) / len(x1) + return percentileofscore(x1, x1[-1]) / len(x1) if self.N == 0: series = series.expanding(min_periods=1).apply(rank, raw=True) @@ -1273,7 +1284,7 @@ class EMA(Rolling): ---------- feature : Expression feature instance - N : int + N : int, float rolling window size Returns @@ -1296,6 +1307,8 @@ class EMA(Rolling): if self.N == 0: series = series.expanding(min_periods=1).apply(exp_weighted_mean, raw=True) + elif 0 < self.N < 1: + series = series.ewm(alpha=self.N, min_periods=1).mean() else: series = series.ewm(span=self.N, min_periods=1).mean() return series From 9dc357bc81f0f22f198aa4527063d70345ba0133 Mon Sep 17 00:00:00 2001 From: Dong Zhou Date: Fri, 30 Oct 2020 11:03:38 +0800 Subject: [PATCH 012/241] add portfolio module --- qlib/portfolio/__init__.py | 0 qlib/portfolio/optimizer.py | 265 ++++++++++++++++++++++++++++++++++++ 2 files changed, 265 insertions(+) create mode 100644 qlib/portfolio/__init__.py create mode 100644 qlib/portfolio/optimizer.py diff --git a/qlib/portfolio/__init__.py b/qlib/portfolio/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/qlib/portfolio/optimizer.py b/qlib/portfolio/optimizer.py new file mode 100644 index 000000000..4b06e25b3 --- /dev/null +++ b/qlib/portfolio/optimizer.py @@ -0,0 +1,265 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +import warnings +import numpy as np +import pandas as pd +import scipy.optimize as so + +from typing import Optional, Union, Callable, List + + +class PortfolioOptimizer(object): + """Portfolio Optimizer + + The following optimization algorithms are supported: + - `gmv`: Global Minimum Variance Portfolio + - `mvo`: Mean Variance Optimized Portfolio + - `rp`: Risk Parity + - `inv`: Inverse Volatility + + Note: + This optimizer always assumes full investment and no-shorting. + """ + + OPT_GMV = 'gmv' + OPT_MVO = 'mvo' + OPT_RP = 'rp' + OPT_INV = 'inv' + + def __init__(self, method: str = 'inv', lamb: float = 0, delta: float = 0, + alpha: float = 0.0, scale_alpha: bool = True, tol: float = 1e-8): + """ + Args: + method (str): portfolio optimization method + lamb (float): risk aversion parameter (larger `lamb` means more focus on return) + delta (float): turnover rate limit + alpha (float): l2 norm regularizer + tol (float): tolerance for optimization termination + """ + assert method in [self.OPT_GMV, self.OPT_MVO, self.OPT_RP, self.OPT_INV], \ + f'method `{method}` is not supported' + self.method = method + + assert lamb >= 0, f'risk aversion parameter `lamb` should be positive' + self.lamb = lamb + + assert delta >= 0, f'turnover limit `delta` should be positive' + self.delta = delta + + assert alpha >= 0, f'l2 norm regularizer `alpha` should be positive' + self.alpha = alpha + + self.tol = tol + + def __call__(self, S: Union[np.ndarray, pd.DataFrame], + u: Optional[Union[np.ndarray, pd.Series]] = None, + w0: Optional[Union[np.ndarray, pd.Series]] = None) -> Union[np.ndarray, pd.Series]: + """ + Args: + S (np.ndarray or pd.DataFrame): covariance matrix + u (np.ndarray or pd.Series): expected returns (a.k.a., alpha) + w0 (np.ndarray or pd.Series): initial weights (for turnover control) + + Returns: + np.ndarray or pd.Series: optimized portfolio allocation + """ + # transform dataframe into array + index = None + if isinstance(S, pd.DataFrame): + index = S.index + S = S.values + + # transform alpha + if u is not None: + assert len(u) == len(S), '`u` has mismatched shape' + if isinstance(u, pd.Series): + assert all(u.index == index), '`u` has mismatched index' + u = u.values + + # transform initial weights + if w0 is not None: + assert len(w0) == len(S), '`w0` has mismatched shape' + if isinstance(w0, pd.Series): + assert all(w0.index == index), '`w0` has mismatched index' + w0 = w0.values + + # scale alpha to match volatility + if u is not None: + u = u / u.std() + u *= np.mean(np.diag(S))**0.5 + + # optimize + w = self._optimize(S, u, w0) + + # restore index if needed + if index is not None: + w = pd.Series(w, index=index) + + return w + + def _optimize(self, S: np.ndarray, u: Optional[np.ndarray] = None, + w0: Optional[np.ndarray] = None) -> np.ndarray: + + # inverse volatility + if self.method == self.OPT_INV: + if u is not None: + warnings.warn('`u` is set but will not be used for `inv` portfolio') + if w0 is not None: + warnings.warn('`w0` is set but will not be used for `inv` portfolio') + return self._optimize_inv(S) + + # global minimum variance + if self.method == self.OPT_GMV: + if u is not None: + warnings.warn('`u` is set but will not be used for `gmv` portfolio') + return self._optimize_gmv(S, w0) + + # mean-variance + if self.method == self.OPT_MVO: + return self._optimize_mvo(S, u, w0) + + # risk parity + if self.method == self.OPT_RP: + if u is not None: + warnings.warn('`u` is set but will not be used for `rp` portfolio') + return self._optimize_rp(S, w0) + + def _optimize_inv(self, S: np.ndarray) -> np.ndarray: + """Inverse volatility""" + vola = np.diag(S)**0.5 + w = 1 / vola + w /= w.sum() + return w + + def _optimize_gmv(self, S: np.ndarray, w0: Optional[np.ndarray] = None) -> np.ndarray: + """optimize global minimum variance portfolio + + This method solves the following optimization problem + min_w w' S w + s.t. w >= 0, sum(w) == 1 + where `S` is the covariance matrix. + """ + return self._solve( + len(S), + self._get_objective_gmv(S), + *self._get_constrains(w0) + ) + + def _optimize_mvo(self, S: np.ndarray, u: Optional[np.ndarray] = None, + w0: Optional[np.ndarray] = None) -> np.ndarray: + """optimize mean-variance portfolio + + This method solves the following optimization problem + min_w - w' u + lamb * w' S w + s.t. w >= 0, sum(w) == 1 + where `S` is the covariance matrix, `u` is the expected returns, + and `lamb` is the risk aversion parameter. + """ + return self._solve( + len(S), + self._get_objective_mvo(S, u), + *self._get_constrains(w0) + ) + + def _optimize_rp(self, S: np.ndarray, w0: Optional[np.ndarray] = None) -> np.ndarray: + """optimize risk parity portfolio + + This method solves the following optimization problem + min_w sum_i [w_i - (w' S w) / ((S w)_i * N)]**2 + s.t. w >= 0, sum(w) == 1 + where `S` is the covariance matrix and `N` is the number of stocks. + """ + return self._solve( + len(S), + self._get_objective_rp(S), + *self._get_constrains(w0) + ) + + def _get_objective_gmv(self, S: np.ndarray) -> np.ndarray: + """global minimum variance optimization objective + + Optimization objective + min_w w' S w + """ + + def func(x): + return x @ S @ x + + return func + + def _get_objective_mvo(self, S: np.ndarray, u: np.ndarray = None) -> np.ndarray: + """mean-variance optimization objective + + Optimization objective + min_w - w' u + lamb * w' S w + """ + + def func(x): + risk = x @ S @ x + ret = x @ u + return -ret + self.lamb * risk + + return func + + def _get_objective_rp(self, S: np.ndarray) -> np.ndarray: + """risk-parity optimization objective + + Optimization objective + min_w sum_i [w_i - (w' S w) / ((S w)_i * N)]**2 + """ + + def func(x): + N = len(x) + Sx = S @ x + xSx = x @ Sx + return np.sum((x - xSx / Sx / N)**2) + + return func + + def _get_constrains(self, w0: Optional[np.ndarray] = None): + """optimization constraints + + Defines the following constraints: + - no shorting and leverage: 0 <= w <= 1 + - full investment: sum(w) == 1 + - turnover constraint: |w - w0| <= delta + """ + + # no shorting and leverage + bounds = so.Bounds(0.0, 1.0) + + # full investment constraint + cons = [ + {'type': 'eq', 'fun': lambda x: np.sum(x) - 1} # == 0 + ] + + # turnover constraint + if w0 is not None: + cons.append( + {'type': 'ineq', 'fun': lambda x: self.delta - np.sum(np.abs(x - w0))} # >= 0 + ) + + return bounds, cons + + def _solve(self, n: int, obj: Callable, bounds: so.Bounds, cons: List) -> np.ndarray: + """solve optimization + + Args: + n (int): number of parameters + obj (callable): optimization objective + bounds (Bounds): bounds of parameters + cons (list): optimization constraints + """ + # add l2 regularization + wrapped_obj = obj + if self.alpha > 0: + wrapped_obj = lambda x: obj(x) + self.alpha * np.sum(np.square(x)) + + # solve + x0 = np.ones(n) / n # init results + sol = so.minimize(wrapped_obj, x0, bounds=bounds, constraints=cons, tol=self.tol) + if not sol.success: + warnings.warn(f'optimization not success ({sol.status})') + + return sol.x From c59058b47d125b8d6d241a4ae5789fef8b6c8154 Mon Sep 17 00:00:00 2001 From: Dong Zhou Date: Fri, 30 Oct 2020 11:20:15 +0800 Subject: [PATCH 013/241] fix dataloader & add interface to datahandler --- qlib/data/dataset/handler.py | 62 ++++++++++++++++++++++++-- qlib/data/dataset/loader.py | 85 ++++++++++++++++++------------------ 2 files changed, 101 insertions(+), 46 deletions(-) diff --git a/qlib/data/dataset/handler.py b/qlib/data/dataset/handler.py index 04715c892..7f1dbd179 100644 --- a/qlib/data/dataset/handler.py +++ b/qlib/data/dataset/handler.py @@ -5,7 +5,7 @@ import abc import bisect import logging -from typing import Union, Tuple, List +from typing import Union, Tuple, List, Iterator, Optional import pandas as pd import numpy as np @@ -113,8 +113,7 @@ class DataHandler(Serializable): CS_ALL = "__all" def _fetch_df_by_col(self, df: pd.DataFrame, col_set: str) -> pd.DataFrame: - cln = len(df.columns.levels) - if cln == 1: + if not isinstance(df.columns, pd.MultiIndex): return df elif col_set == self.CS_ALL: return df.droplevel(axis=1, level=0) @@ -126,6 +125,7 @@ class DataHandler(Serializable): selector: Union[pd.Timestamp, slice, str], level: Union[str, int] = "datetime", col_set: Union[str, List[str]] = CS_ALL, + squeeze: bool = False ) -> pd.DataFrame: """ fetch data from underlying data source @@ -141,13 +141,22 @@ class DataHandler(Serializable): select a set of meaningful columns.(e.g. features, columns) if isinstance(col_set, List[str]): select several sets of meaningful columns, the returned data has multiple levels + squeeze : bool + whether squeeze columns and index Returns ------- pd.DataFrame: """ df = self._fetch_df_by_index(self._data, selector, level) - return self._fetch_df_by_col(df, col_set) + df = self._fetch_df_by_col(df, col_set) + if squeeze: + # squeeze columns + df = df.squeeze() + # squeeze index + if isinstance(selector, (str, pd.Timestamp)): + df = df.reset_index(level=level, drop=True) + return df def get_cols(self, col_set=CS_ALL) -> list: """ @@ -167,6 +176,51 @@ class DataHandler(Serializable): df = self._fetch_df_by_col(df, col_set) return df.columns.to_list() + def get_range_selector(self, cur_date: Union[pd.Timestamp, str], periods: int) -> slice: + """ + get range selector by number of periods + + Args: + cur_date (pd.Timestamp or str): current date + periods (int): number of periods + """ + trading_dates = self.get_unique_index('datetime') + cur_loc = trading_dates.get_loc(cur_date) + pre_loc = cur_loc - periods + 1 + if pre_loc < 0: + warnings.warn('`periods` is too large. the first date will be returned.') + pre_loc = 0 + ref_date = trading_dates[pre_loc] + return slice(ref_date, cur_date) + + def get_range_iterator(self, periods: int, min_periods: Optional[int] = None, + **kwargs) -> Iterator[Tuple[pd.Timestamp, pd.DataFrame]]: + """ + get a iterator of sliced data with given periods + + Args: + periods (int): number of periods + min_periods (int): minimum periods for sliced dataframe + kwargs (dict): will be passed to `self.fetch` + """ + trading_dates = self.get_unique_index('datetime') + if min_periods is None: + min_periods = periods + for cur_date in trading_dates[min_periods:]: + selector = self.get_range_selector(cur_date, periods) + yield cur_date, self.fetch(selector, **kwargs) + + def get_unique_index(self, level: Union[str, int] = 'datetime') -> pd.Index: + """ + get unique index by level id (int) or name (str) + + Args: + level (str or int): index level + """ + if self._data is None: + raise ValueError('data is not loaded!') + return self._data.index.unique(level=level) + class DataHandlerLP(DataHandler): """ diff --git a/qlib/data/dataset/loader.py b/qlib/data/dataset/loader.py index e4f2f8619..abfc695d9 100644 --- a/qlib/data/dataset/loader.py +++ b/qlib/data/dataset/loader.py @@ -1,78 +1,75 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. -from abc import ABC, abstractmethod +import abc +import warnings import pandas as pd -from qlib.data import D + from typing import Tuple +from qlib.data import D -class DataLoader(ABC): - """ +class DataLoader(abc.ABC): + ''' DataLoader is designed for loading raw data from original data source. - """ - - @abstractmethod + ''' + @abc.abstractmethod def load(self, instruments, start_time=None, end_time=None) -> pd.DataFrame: """ - load the data as pd.DataFrame + load the data as pd.DataFrame - Parameters - ---------- - self : [TODO:type] - [TODO:description] - instruments : [TODO:type] - [TODO:description] - start_time : [TODO:type] - [TODO:description] - end_time : [TODO:type] - [TODO:description] + Parameters + ---------- + self : [TODO:type] + [TODO:description] + instruments : [TODO:type] + [TODO:description] + start_time : [TODO:type] + [TODO:description] + end_time : [TODO:type] + [TODO:description] - Returns - ------- - pd.DataFrame: - data load from the under layer source + Returns + ------- + pd.DataFrame: + data load from the under layer source - Example of the data: - The multi-index of the columns is optional. - feature label - $close $volume Ref($close, 1) Mean($close, 3) $high-$low LABEL0 - datetime instrument - 2010-01-04 SH600000 81.807068 17145150.0 83.737389 83.016739 2.741058 0.0032 - SH600004 13.313329 11800983.0 13.313329 13.317701 0.183632 0.0042 - SH600005 37.796539 12231662.0 38.258602 37.919757 0.970325 0.0289 + Example of the data: + (The multi-index of the columns is optional.) + feature label + $close $volume Ref($close, 1) Mean($close, 3) $high-$low LABEL0 + datetime instrument + 2010-01-04 SH600000 81.807068 17145150.0 83.737389 83.016739 2.741058 0.0032 + SH600004 13.313329 11800983.0 13.313329 13.317701 0.183632 0.0042 + SH600005 37.796539 12231662.0 38.258602 37.919757 0.970325 0.0289 """ pass class QlibDataLoader(DataLoader): - """Same as QlibDataLoader. The fields can be define by config""" - + '''Same as QlibDataLoader. The fields can be define by config''' def __init__(self, config: Tuple[list, tuple, dict], filter_pipe=None): """ Parameters ---------- - config : Tuple[list ,tuple, dict] + config : Tuple[list, tuple, dict] Config will be used to describe the fields and column names := { "group_name1": "group_name2": } - + or := := ["expr", ...] | (["expr", ...], ["col_name", ...]) - - Here is a few examples to describe the fields - TODO: """ - self.is_group = isinstance(config, dict) + self.is_group = isinstance(config, dict) if self.is_group: self.fields = {grp: self._parse_fields_info(fields_info) for grp, fields_info in config.items()} else: - self.fields = self._parse_fields_info(fields_info) + self.fields = self._parse_fields_info(config) self.filter_pipe = filter_pipe @@ -86,14 +83,18 @@ class QlibDataLoader(DataLoader): return exprs, names def load(self, instruments, start_time=None, end_time=None) -> pd.DataFrame: + if isinstance(instruments, str): + instruments = D.instruments(instruments, filter_pipe=self.filter_pipe) + elif self.filter_pipe is not None: + warnings.warn('`filter_pipe` is not None, but it will not be used with `instruments` as list') def _get_df(exprs, names): - df = D.features(D.instruments(instruments, filter_pipe=self.filter_pipe), exprs, start_time, end_time) + df = D.features(instruments, exprs, start_time, end_time) df.columns = names return df - if self.is_group: df = pd.concat({grp: _get_df(exprs, names) for grp, (exprs, names) in self.fields.items()}, axis=1) else: + exprs, names = self.fields df = _get_df(exprs, names) - df = df.swaplevel().sort_index() + df = df.swaplevel().sort_index() # NOTE: always return return df From a12ae596ec224cf67c950b96c84a440de64f8df3 Mon Sep 17 00:00:00 2001 From: Dong Zhou Date: Fri, 30 Oct 2020 11:22:38 +0800 Subject: [PATCH 014/241] add riskmodel --- qlib/model/riskmodel.py | 455 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 455 insertions(+) create mode 100644 qlib/model/riskmodel.py diff --git a/qlib/model/riskmodel.py b/qlib/model/riskmodel.py new file mode 100644 index 000000000..e63b8d4a2 --- /dev/null +++ b/qlib/model/riskmodel.py @@ -0,0 +1,455 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +import warnings +import numpy as np +import pandas as pd + +from typing import Union + +from qlib.model.base import BaseModel + + +class RiskModel(BaseModel): + """Risk Model + + A risk model is used to estimate the covariance matrix of stock returns. + """ + + MASK_NAN = 'mask' + FILL_NAN = 'fill' + IGNORE_NAN = 'ignore' + + def __init__(self, nan_option: str = 'ignore', assume_centered: bool = False, scale_return: bool = True): + """ + Args: + nan_option (str): nan handling option (`ignore`/`mask`/`fill`) + assume_centered (bool): whether the data is assumed to be centered + scale_return (bool): whether scale returns as percentage + """ + # nan + assert nan_option in [self.MASK_NAN, self.FILL_NAN, self.IGNORE_NAN], \ + f'`nan_option={nan_option}` is not supported' + self.nan_option = nan_option + + self.assume_centered = assume_centered + self.scale_return = scale_return + + def predict(self, X: Union[pd.Series, pd.DataFrame, np.ndarray], + return_corr: bool = False, is_price: bool = True) -> Union[pd.DataFrame, np.ndarray]: + """ + Args: + X (pd.Series, pd.DataFrame or np.ndarray): data from which to estimate the covariance, + with variables as columns and observations as rows. + return_corr (bool): whether return the correlation matrix + is_price (bool): whether `X` contains price (if not assume stock returns) + + Returns: + pd.DataFrame or np.ndarray: estimated covariance (or correlation) + """ + # transform input into 2D array + if not isinstance(X, (pd.Series, pd.DataFrame)): + columns = None + else: + if isinstance(X.index, pd.MultiIndex): + if isinstance(X, pd.DataFrame): + X = X.iloc[:, 0].unstack(level='instrument') # always use the first column + else: + X = X.unstack(level='instrument') + else: + # X is 2D DataFrame + pass + columns = X.columns # will be used to restore dataframe + X = X.values + + # calculate pct_change + if is_price: + X = X[1:] / X[:-1] - 1 # NOTE: resulting `n - 1` rows + + # scale return + if self.scale_return: + X *= 100 + + # handle nan and centered + X = self._preprocess(X) + + # estimate covariance + S = self._predict(X) + + # return correlation if needed + if return_corr: + vola = np.sqrt(np.diag(S)) + corr = S / np.outer(vola, vola) + if columns is None: + return corr + return pd.DataFrame(corr, index=columns, columns=columns) + + # return covariance + if columns is None: + return S + return pd.DataFrame(S, index=columns, columns=columns) + + def _predict(self, X: np.ndarray) -> np.ndarray: + """covariance estimation implementation + + This method should be overridden by child classes. + + By default, this method implements the empirical covariance estimation. + + Args: + X (np.ndarray): data matrix containing multiple variables (columns) and observations (rows) + + Returns: + np.ndarray: covariance matrix + """ + xTx = np.asarray(X.T.dot(X)) + N = len(X) + if isinstance(X, np.ma.MaskedArray): + M = 1 - X.mask + N = M.T.dot(M) # each pair has distinct number of samples + return xTx / N + + def _preprocess(self, X: np.ndarray) -> Union[np.ndarray, np.ma.MaskedArray]: + """handle nan and centerize data + + Note: + if `nan_option='mask'` then the returned array will be `np.ma.MaskedArray` + """ + # handle nan + if self.nan_option == self.FILL_NAN: + X = np.nan_to_num(X) + elif self.nan_option == self.MASK_NAN: + X = np.ma.masked_invalid(X) + # centerize + if not self.assume_centered: + X = X - np.nanmean(X, axis=0) + return X + + +class ShrinkCovEstimator(RiskModel): + """Shrinkage Covariance Estimator + + This estimator will shrink the sample covariance matrix towards + an identify matrix: + S_hat = (1 - alpha) * S + alpha * F + where `alpha` is the shrink parameter and `F` is the shrinking target. + + The following shrinking parameters (`alpha`) are supported: + - `lw` [1][2][3]: use Ledoit-Wolf shrinking parameter + - `oas` [4]: use Oracle Approximating Shrinkage shrinking parameter + - float: directly specify the shrink parameter, should be between [0, 1] + + The following shrinking targets (`F`) are supported: + - `const_var` [1][4][5]: assume stocks have the same constant variance and zero correlation + - `const_corr` [2][6]: assume stocks have different variance but equal correlation + - `single_factor` [3][7]: assume single factor model as the shrinking target + - np.ndarray: provide the shrinking targets directly + + Note: + - The optimal shrinking parameter depends on the selection of the shrinking target. + Currently, `oas` is not supported for `const_corr` and `single_factor`. + - Remember to set `nan_option` to `fill` or `mask` if your data has missing values. + + References: + [1] Ledoit, O., & Wolf, M. (2004). A well-conditioned estimator for large-dimensional covariance matrices. + Journal of Multivariate Analysis, 88(2), 365–411. https://doi.org/10.1016/S0047-259X(03)00096-4 + [2] Ledoit, O., & Wolf, M. (2004). Honey, I shrunk the sample covariance matrix. + Journal of Portfolio Management, 30(4), 1–22. https://doi.org/10.3905/jpm.2004.110 + [3] Ledoit, O., & Wolf, M. (2003). Improved estimation of the covariance matrix of stock returns + with an application to portfolio selection. + Journal of Empirical Finance, 10(5), 603–621. https://doi.org/10.1016/S0927-5398(03)00007-0 + [4] Chen, Y., Wiesel, A., Eldar, Y. C., & Hero, A. O. (2010). Shrinkage algorithms for MMSE covariance estimation. + IEEE Transactions on Signal Processing, 58(10), 5016–5029. https://doi.org/10.1109/TSP.2010.2053029 + [5] https://www.econ.uzh.ch/dam/jcr:ffffffff-935a-b0d6-0000-00007f64e5b9/cov1para.m.zip + [6] https://www.econ.uzh.ch/dam/jcr:ffffffff-935a-b0d6-ffff-ffffde5e2d4e/covCor.m.zip + [7] https://www.econ.uzh.ch/dam/jcr:ffffffff-935a-b0d6-0000-0000648dfc98/covMarket.m.zip + """ + + SHR_LW = 'lw' + SHR_OAS = 'oas' + + TGT_CONST_VAR = 'const_var' + TGT_CONST_CORR = 'const_corr' + TGT_SINGLE_FACTOR = 'single_factor' + + def __init__(self, alpha: Union[str, float] = 0.0, target: Union[str, np.ndarray] = 'const_var', **kwargs): + """ + Args: + alpha (str or float): shrinking parameter or estimator (`lw`/`oas`) + target (str or np.ndarray): shrinking target (`const_var`/`const_corr`/`single_factor`) + kwargs: see `RiskModel` for more information + """ + super().__init__(**kwargs) + + # alpha + if isinstance(alpha, str): + assert alpha in [self.SHR_LW, self.SHR_OAS], \ + f'shrinking method `{alpha}` is not supported' + elif isinstance(alpha, (float, np.floating)): + assert 0 <= alpha <= 1, 'alpha should be between [0, 1]' + else: + raise TypeError('invalid argument type for `alpha`') + self.alpha = alpha + + # target + if isinstance(target, str): + assert target in [self.TGT_CONST_VAR, self.TGT_CONST_CORR, self.TGT_SINGLE_FACTOR], \ + f'shrinking target `{target} is not supported' + elif isinstance(target, np.ndarray): + pass + else: + raise TypeError('invalid argument type for `target`') + if alpha == self.SHR_OAS and target != self.TGT_CONST_VAR: + raise NotImplementedError('currently `oas` can only support `const_var` as target') + self.target = target + + def _predict(self, X: np.ndarray) -> np.ndarray: + # sample covariance + S = super()._predict(X) + + # shrinking target + F = self._get_shrink_target(X, S) + + # get shrinking parameter + alpha = self._get_shrink_param(X, S, F) + + # shrink covariance + if alpha > 0: + S *= (1 - alpha) + F *= alpha + S += F + + return S + + def _get_shrink_target(self, X: np.ndarray, S: np.ndarray) -> np.ndarray: + """get shrinking target `F`""" + if self.target == self.TGT_CONST_VAR: + return self._get_shrink_target_const_var(X, S) + if self.target == self.TGT_CONST_CORR: + return self._get_shrink_target_const_corr(X, S) + if self.target == self.TGT_SINGLE_FACTOR: + return self._get_shrink_target_single_factor(X, S) + + def _get_shrink_target_const_var(self, X: np.ndarray, S: np.ndarray) -> np.ndarray: + """get shrinking target with constant variance + + This target assumes zero pair-wise correlation and constant variance. + The constant variance is estimated by averaging all sample's variances. + """ + n = len(S) + F = np.eye(n) + np.fill_diagonal(F, np.mean(np.diag(S))) + return F + + def _get_shrink_target_const_corr(self, X: np.ndarray, S: np.ndarray) -> np.ndarray: + """get shrinking target with constant correlation + + This target assumes constant pair-wise correlation but keep the sample variance. + The constant correlation is estimated by averaging all pairwise correlations. + """ + n = len(S) + var = np.diag(S) + sqrt_var = np.sqrt(var) + covar = np.outer(sqrt_var, sqrt_var) + r_bar = (np.sum(S / covar) - n) / (n * (n - 1)) + F = r_bar * covar + np.fill_diagonal(F, var) + return F + + def _get_shrink_target_single_factor(self, X: np.ndarray, S: np.ndarray) -> np.ndarray: + """get shrinking target with single factor model""" + X_mkt = np.nanmean(X, axis=1) + cov_mkt = np.asarray(X.T.dot(X_mkt) / len(X)) + var_mkt = np.asarray(X_mkt.dot(X_mkt) / len(X)) + F = np.outer(cov_mkt, cov_mkt) / var_mkt + np.fill_diagonal(F, np.diag(S)) + return F + + def _get_shrink_param(self, X: np.ndarray, S: np.ndarray, F: np.ndarray) -> float: + """get shrinking parameter `alpha` + + Note: + The Ledoit-Wolf shrinking parameter estimator consists of three different + """ + if self.alpha == self.SHR_OAS: + return self._get_shrink_param_oas(X, S, F) + elif self.alpha == self.SHR_LW: + if self.target == self.TGT_CONST_VAR: + return self._get_shrink_param_lw_const_var(X, S, F) + if self.target == self.TGT_CONST_CORR: + return self._get_shrink_param_lw_const_corr(X, S, F) + if self.target == self.TGT_SINGLE_FACTOR: + return self._get_shrink_param_lw_single_factor(X, S, F) + return self.alpha + + def _get_shrink_param_oas(self, X: np.ndarray, S: np.ndarray, F: np.ndarray) -> float: + """Oracle Approximating Shrinkage Estimator + + This method uses the following formula to estimate the `alpha` + parameter for the shrink covariance estimator: + A = (1 - 2 / p) * trace(S^2) + trace^2(S) + B = (n + 1 - 2 / p) * (trace(S^2) - trace^2(S) / p) + alpha = A / B + where `n`, `p` are the dim of observations and variables respectively. + """ + trS2 = np.sum(S**2) + tr2S = np.trace(S)**2 + + n, p = X.shape + + A = (1 - 2 / p) * (trS2 + tr2S) + B = (n + 1 - 2 / p) * (trS2 + tr2S / p) + alpha = A / B + + return alpha + + def _get_shrink_param_lw_const_var(self, X: np.ndarray, S: np.ndarray, F: np.ndarray) -> float: + """Ledoit-Wolf Shrinkage Estimator (Constant Variance) + + This method shrinks the covariance matrix towards the constand variance target. + """ + t, n = X.shape + + y = X**2 + phi = np.sum(y.T.dot(y) / t - S**2) + + gamma = np.linalg.norm(S - F, 'fro')**2 + + kappa = phi / gamma + alpha = max(0, min(1, kappa / t)) + + return alpha + + def _get_shrink_param_lw_const_corr(self, X: np.ndarray, S: np.ndarray, F: np.ndarray) -> float: + """Ledoit-Wolf Shrinkage Estimator (Constant Correlation) + + This method shrinks the covariance matrix towards the constand correlation target. + """ + t, n = X.shape + + var = np.diag(S) + sqrt_var = np.sqrt(var) + r_bar = (np.sum(S / np.outer(sqrt_var, sqrt_var)) - n) / (n * (n - 1)) + + y = X**2 + phi_mat = y.T.dot(y) / t - S**2 + phi = np.sum(phi_mat) + + theta_mat = (X**3).T.dot(X) / t - var[:, None] * S + np.fill_diagonal(theta_mat, 0) + rho = np.sum(np.diag(phi_mat)) + r_bar * np.sum(np.outer(1 / sqrt_var, sqrt_var) * theta_mat) + + gamma = np.linalg.norm(S - F, 'fro')**2 + + kappa = (phi - rho) / gamma + alpha = max(0, min(1, kappa / t)) + + return alpha + + def _get_shrink_param_lw_single_factor(self, X: np.ndarray, S: np.ndarray, F: np.ndarray) -> float: + """Ledoit-Wolf Shrinkage Estimator (Single Factor Model) + + This method shrinks the covariance matrix towards the single factor model target. + """ + t, n = X.shape + + X_mkt = np.nanmean(X, axis=1) + cov_mkt = np.asarray(X.T.dot(X_mkt) / len(X)) + var_mkt = np.asarray(X_mkt.dot(X_mkt) / len(X)) + + y = X**2 + phi = np.sum(y.T.dot(y)) / t - np.sum(S**2) + + rdiag = np.sum(y**2) / t - np.sum(np.diag(S)**2) + z = X * X_mkt[:, None] + v1 = y.T.dot(z) / t - cov_mkt[:, None] * S + roff1 = np.sum(v1 * cov_mkt[:, None].T) / var_mkt - np.sum(np.diag(v1) * cov_mkt) / var_mkt + v3 = z.T.dot(z) / t - var_mkt * S + roff3 = np.sum(v3 * np.outer(cov_mkt, cov_mkt)) / var_mkt**2 - np.sum(np.diag(v3) * cov_mkt**2) / var_mkt**2 + roff = 2 * roff1 - roff3 + rho = rdiag + roff + + gamma = np.linalg.norm(S - F, 'fro')**2 + + kappa = (phi - rho) / gamma + alpha = max(0, min(1, kappa / t)) + + return alpha + + +class POETCovEstimator(RiskModel): + """Principal Orthogonal Complement Thresholding Estimator (POET) + + Reference: + [1] Fan, J., Liao, Y., & Mincheva, M. (2013). Large covariance estimation by thresholding principal orthogonal complements. + Journal of the Royal Statistical Society. Series B: Statistical Methodology, 75(4), 603–680. https://doi.org/10.1111/rssb.12016 + [2] http://econweb.rutgers.edu/yl1114/papers/poet/POET.m + """ + + THRESH_SOFT = 'soft' + THRESH_HARD = 'hard' + THRESH_SCAD = 'scad' + + def __init__(self, num_factors: int = 0, thresh: float = 1.0, thresh_method: str = 'soft', **kwargs): + """ + Args: + num_factors (int): number of factors (if set to zero, no factor model will be used) + thresh (float): the positive constant for thresholding + thresh_method (str): thresholding method, which can be + - 'soft': soft thresholding + - 'hard': hard thresholding + - 'scad': scad thresholding + kwargs: see `RiskModel` for more information + """ + super().__init__(**kwargs) + + assert num_factors >= 0, '`num_factors` requires a positive integer' + self.num_factors = num_factors + + assert thresh >= 0, '`thresh` requires a positive float number' + self.thresh = thresh + + assert thresh_method in [self.THRESH_HARD, self.THRESH_SOFT, self.THRESH_SCAD], \ + '`thresh_method` should be `soft`/`hard`/`scad`' + self.thresh_method = thresh_method + + def _predict(self, X: np.ndarray) -> np.ndarray: + + Y = X.T # NOTE: to match POET's implementation + p, n = Y.shape + + if self.num_factors > 0: + Dd, V = np.linalg.eig(Y.T.dot(Y)) + V = V[:, np.argsort(Dd)] + F = V[:, -self.num_factors:][:, ::-1] * np.sqrt(n) + LamPCA = Y.dot(F) / n + uhat = np.asarray(Y - LamPCA.dot(F.T)) + Lowrank = np.asarray(LamPCA.dot(LamPCA.T)) + rate = 1 / np.sqrt(p) + np.sqrt(np.log(p) / n) + else: + uhat = np.asarray(Y) + rate = np.sqrt(np.log(p) / n) + Lowrank = 0 + + lamb = rate * self.thresh + SuPCA = uhat.dot(uhat.T) / n + SuDiag = np.diag(np.diag(SuPCA)) + R = np.linalg.inv(SuDiag**0.5).dot(SuPCA).dot(np.linalg.inv(SuDiag**0.5)) + + if self.thresh_method == self.THRESH_HARD: + M = R * (np.abs(R) > lamb) + elif self.thresh_method == self.THRESH_SOFT: + res = (np.abs(R) - lamb) + res = (res + np.abs(res)) / 2 + M = np.sign(R) * res + else: + M1 = (np.abs(R) < 2 * lamb) * np.sign(R) * (np.abs(R) - lamb) * (np.abs(R) > lamb) + M2 = (np.abs(R) < 3.7 * lamb) * (np.abs(R) >= 2 * lamb) * (2.7 * R - 3.7 * np.sign(R) * lamb) / 1.7 + M3 = (np.abs(R) >= 3.7 * lamb) * R + M = M1 + M2 + M3 + + Rthresh = M - np.diag(np.diag(M)) + np.eye(p) + SigmaU = (SuDiag**0.5).dot(Rthresh).dot(SuDiag**0.5) + SigmaY = SigmaU + Lowrank + + return SigmaY From 91019edcb81ffcf867eb833a926d2c7c274c3214 Mon Sep 17 00:00:00 2001 From: Dong Zhou Date: Fri, 30 Oct 2020 13:22:41 +0800 Subject: [PATCH 015/241] remove abundant api --- qlib/data/dataset/handler.py | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/qlib/data/dataset/handler.py b/qlib/data/dataset/handler.py index 7f1dbd179..b8751830d 100644 --- a/qlib/data/dataset/handler.py +++ b/qlib/data/dataset/handler.py @@ -184,7 +184,7 @@ class DataHandler(Serializable): cur_date (pd.Timestamp or str): current date periods (int): number of periods """ - trading_dates = self.get_unique_index('datetime') + trading_dates = self._data.index.unique(level='datetime') cur_loc = trading_dates.get_loc(cur_date) pre_loc = cur_loc - periods + 1 if pre_loc < 0: @@ -203,24 +203,13 @@ class DataHandler(Serializable): min_periods (int): minimum periods for sliced dataframe kwargs (dict): will be passed to `self.fetch` """ - trading_dates = self.get_unique_index('datetime') + trading_dates = self._data.index.unique(level='datetime') if min_periods is None: min_periods = periods for cur_date in trading_dates[min_periods:]: selector = self.get_range_selector(cur_date, periods) yield cur_date, self.fetch(selector, **kwargs) - def get_unique_index(self, level: Union[str, int] = 'datetime') -> pd.Index: - """ - get unique index by level id (int) or name (str) - - Args: - level (str or int): index level - """ - if self._data is None: - raise ValueError('data is not loaded!') - return self._data.index.unique(level=level) - class DataHandlerLP(DataHandler): """ From 5f9c8be33d3237937951e0b23e6a84f4090603ff Mon Sep 17 00:00:00 2001 From: Jactus Date: Mon, 2 Nov 2020 11:05:40 +0800 Subject: [PATCH 016/241] Add RecordTemp & update --- qlib/__init__.py | 7 +- qlib/config.py | 2 +- qlib/data/data.py | 40 ++++--- qlib/utils/__init__.py | 15 +-- qlib/workflow/__init__.py | 23 ++-- qlib/workflow/exp.py | 39 ++++++- qlib/workflow/expm.py | 42 +++---- qlib/workflow/record_temp.py | 137 +++++++++++++++++++++++ qlib/workflow/{record.py => recorder.py} | 37 ++++-- requirements.txt | 1 + setup.py | 1 + 11 files changed, 263 insertions(+), 81 deletions(-) create mode 100644 qlib/workflow/record_temp.py rename qlib/workflow/{record.py => recorder.py} (79%) diff --git a/qlib/__init__.py b/qlib/__init__.py index 154d4ea08..8620acdb7 100644 --- a/qlib/__init__.py +++ b/qlib/__init__.py @@ -82,12 +82,11 @@ def init(default_conf="client", **kwargs): LOG.info(f"flask_server={C['flask_server']}, flask_port={C['flask_port']}") # set up QlibRecorder - default_uri = str(Path(os.getcwd()).resolve() / "mlruns") - current_uri = C["exp_uri"] if C["exp_uri"] is not None else default_uri + uri = C["exp_uri"] # exp manager module - module = get_module_by_module_path("qlib.workflow") + module = get_module_by_module_path("qlib.workflow.expm") exp_manager = init_instance_by_config(C["exp_manager"], module) - qr = QlibRecorder(exp_manager, default_uri, current_uri) + qr = QlibRecorder(exp_manager, uri) R.register(qr) diff --git a/qlib/config.py b/qlib/config.py index 0e2a264af..2bd77feb8 100644 --- a/qlib/config.py +++ b/qlib/config.py @@ -126,7 +126,7 @@ _default_config = { }, # Defatult config for experiment manager "exp_manager": {"class": "MLflowExpManager", "kwargs": {}}, - "exp_uri": None, + "exp_uri": str(Path(os.getcwd()).resolve() / "mlruns"), } MODE_CONF = { diff --git a/qlib/data/data.py b/qlib/data/data.py index 476cc9682..8eae9f01c 100644 --- a/qlib/data/data.py +++ b/qlib/data/data.py @@ -24,7 +24,7 @@ from ..log import get_module_logger from ..utils import parse_field, read_bin, hash_args, normalize_cache_fields from .base import Feature from .cache import DiskDatasetCache, DiskExpressionCache -from ..utils import Wrapper, get_provider_obj, register_wrapper +from ..utils import Wrapper, init_instance_by_config, register_wrapper, get_module_by_module_path class CalendarProvider(abc.ABC): @@ -1031,34 +1031,44 @@ D = Wrapper() def register_all_wrappers(): """register_all_wrappers""" logger = get_module_logger("data") - - _calendar_provider = get_provider_obj(C.calendar_provider) + module = get_module_by_module_path("qlib.data") + + _calendar_provider = init_instance_by_config(C.calendar_provider, module) if getattr(C, "calendar_cache", None) is not None: - _calendar_provider = get_provider_obj(C.calendar_cache, provider=_calendar_provider) - register_wrapper(Cal, _calendar_provider) + _calendar_cache_config = {} + _calendar_cache_config.update(C.calendar_cache) + _calendar_cache_config['kwargs'].update(provider=_calendar_provider) + _calendar_provider = init_instance_by_config(_calendar_cache_config, module) + register_wrapper(Cal, _calendar_provider, "qlib.data") logger.debug(f"registering Cal {C.calendar_provider}-{C.calenar_cache}") - register_wrapper(Inst, C.instrument_provider) + register_wrapper(Inst, C.instrument_provider, "qlib.data") logger.debug(f"registering Inst {C.instrument_provider}") if getattr(C, "feature_provider", None) is not None: - feature_provider = get_provider_obj(C.feature_provider) - register_wrapper(FeatureD, feature_provider) + feature_provider = init_instance_by_config(C.feature_provider, module) + register_wrapper(FeatureD, feature_provider, "qlib.data") logger.debug(f"registering FeatureD {C.feature_provider}") if getattr(C, "expression_provider", None) is not None: # This provider is unnecessary in client provider - _eprovider = get_provider_obj(C.expression_provider) + _eprovider = init_instance_by_config(C.expression_provider, module) if getattr(C, "expression_cache", None) is not None: - _eprovider = get_provider_obj(C.expression_cache, provider=_eprovider) - register_wrapper(ExpressionD, _eprovider) + _expression_cache_config = {} + _expression_cache_config.update(C.expression_cache) + _expression_cache_config['kwargs'].update(provider=_eprovider) + _eprovider = init_instance_by_config(C.expression_cache, module) + register_wrapper(ExpressionD, _eprovider, "qlib.data") logger.debug(f"registering ExpressioneD {C.expression_provider}-{C.expression_cache}") - _dprovider = get_provider_obj(C.dataset_provider) + _dprovider = init_instance_by_config(C.dataset_provider, module) if getattr(C, "dataset_cache", None) is not None: - _dprovider = get_provider_obj(C.dataset_cache, provider=_dprovider) - register_wrapper(DatasetD, _dprovider) + _dataset_cache_config = {} + _dataset_cache_config.update(C.dataset_cache) + _dataset_cache_config['kwargs'].update(provider=_dprovider) + _dprovider = init_instance_by_config(_dataset_cache_config, module) + register_wrapper(DatasetD, _dprovider, "qlib.data") logger.debug(f"registering DataseteD {C.dataset_provider}-{C.dataset_cache}") - register_wrapper(D, C.provider) + register_wrapper(D, C.provider, "qlib.data") logger.debug(f"registering D {C.provider}") diff --git a/qlib/utils/__init__.py b/qlib/utils/__init__.py index 87b43f456..ca0ff4c28 100644 --- a/qlib/utils/__init__.py +++ b/qlib/utils/__init__.py @@ -632,21 +632,14 @@ class Wrapper(object): return getattr(self._provider, key) -def get_provider_obj(config, **params): - module = get_module_by_module_path("qlib.data") - klass, kwargs = get_cls_kwargs(config, module) - kwargs.update(params) - return klass(**kwargs) - - -def register_wrapper(wrapper, cls_or_obj): +def register_wrapper(wrapper, cls_or_obj, module_path=None): """register_wrapper - :param wrapper: A wrapper of all kinds of providers - :param cls_or_obj: A class or class name or object instance in data/data.py + :param wrapper: A wrapper. + :param cls_or_obj: A class or class name or object instance. """ if isinstance(cls_or_obj, str): - module = get_module_by_module_path("qlib.data") + module = get_module_by_module_path(module_path) cls_or_obj = getattr(module, cls_or_obj) obj = cls_or_obj() if isinstance(cls_or_obj, type) else cls_or_obj wrapper.register(obj) diff --git a/qlib/workflow/__init__.py b/qlib/workflow/__init__.py index 7c9c1928f..31b9ae2d7 100644 --- a/qlib/workflow/__init__.py +++ b/qlib/workflow/__init__.py @@ -2,24 +2,29 @@ # Licensed under the MIT License. from contextlib import contextmanager -from .expm import * +from .expm import MLflowExpManager from ..utils import Wrapper class QlibRecorder: - def __init__(self, exp_manager, default_uri, current_uri): + """ + A global system that helps to manage the experiments. + """ + def __init__(self, exp_manager, uri): self.exp_manager = exp_manager - self.default_uri = default_uri - self.current_uri = current_uri + self.uri = uri @contextmanager def start(self, experiment_name): - run = self.start_exp(experiment_name, self.current_uri) - yield run + run = self.start_exp(experiment_name, self.uri) + try: + yield run + except: + self.end_exp() # end the experiment if something went wrong self.end_exp() def start_exp(self, experiment_name=None): - return self.exp_manager.start_exp(experiment_name, self.current_uri) + return self.exp_manager.start_exp(experiment_name, self.uri) def end_exp(self): self.exp_manager.end_exp() @@ -33,8 +38,8 @@ class QlibRecorder: def delete_exp(self, experiment_id): self.exp_manager.delete_exp(experiment_id) - def get_uri(self, type): - return self.exp_manager.get_uri(type) + def get_uri(self): + return self.exp_manager.get_uri() def get_recorder(self): return self.exp_manager.active_recorder diff --git a/qlib/workflow/exp.py b/qlib/workflow/exp.py index a63187e28..335dd338b 100644 --- a/qlib/workflow/exp.py +++ b/qlib/workflow/exp.py @@ -3,7 +3,7 @@ import mlflow from pathlib import Path - +from .recorder import MLflowRecorder class Experiment: """ @@ -15,6 +15,19 @@ class Experiment: self.id = None self.recorders = list() + def create_recorder(self): + """ + Create a recorder for each experiment. + + Parameters + ---------- + + Returns + ------- + A recorder instance. + """ + raise NotImplementedError(f"Please implement the `create_recorder` method.") + def search_records(self, **kwargs): """ Get a pandas DataFrame of records that fit the search criteria of the experiment. @@ -36,15 +49,39 @@ class Experiment: """ raise NotImplementedError(f"Please implement the `search_records` method.") + def delete_recorder(self, rid): + """ + Create a recorder for each experiment. + + Parameters + ---------- + rid : str + the id of the recorder to be deleted. + + Returns + ------- + A recorder instance. + """ + raise NotImplementedError(f"Please implement the `delete_recorder` method.") + class MLflowExperiment(Experiment): """ Use mlflow to implement Experiment. """ + def create_recorder(self): + recorder = MLflowRecorder(self.id) + self.recorders.append(recorder) + return recorders + def search_records(self, **kwargs): filter_string = "" if kwargs.get("filter_string") is None else kwargs.get("filter_string") run_view_type = 1 if kwargs.get("run_view_type") is None else kwargs.get("run_view_type") max_results = 100000 if kwargs.get("max_results") is None else kwargs.get("max_results") order_by = kwargs.get("order_by") return mlflow.search_runs([self.experiment_id], filter_string, run_view_type, max_results, order_by) + + def delete_recorder(self, rid): + mlflow.delete_run(rid) + self.recorders = [r for r in self.recorders if r.recorder_id == rid] \ No newline at end of file diff --git a/qlib/workflow/expm.py b/qlib/workflow/expm.py index 00d25da48..3c633e3bb 100644 --- a/qlib/workflow/expm.py +++ b/qlib/workflow/expm.py @@ -6,8 +6,10 @@ import os from pathlib import Path from contextlib import contextmanager from .exp import MLflowExperiment -from .record import MLflowRecorder +from .recorder import MLflowRecorder +from ..log import get_module_logger +logger = get_module_logger('workflow', 'Warning') class ExpManager: """ @@ -16,7 +18,7 @@ class ExpManager: """ def __init__(self): - self.default_uri = None + self.uri = None self.active_recorder = None # only one recorder can running each time self.experiments = dict() # store the experiment name --> Experiment object @@ -117,20 +119,18 @@ class ExpManager: """ raise NotImplementedError(f"Please implement the `create_exp` method.") - def get_uri(self, type): + def get_uri(self): """ Get the default tracking URI or current URI. Parameters ---------- - type : str - the type of the tracking URI one wants to retrieve. Returns ------- The tracking URI string. """ - raise NotImplementedError(f"Please implement the `create_exp` method.") + return self.uri def get_recorder(self): """ @@ -143,7 +143,7 @@ class ExpManager: ------- An Recorder object. """ - raise NotImplementedError(f"Please implement the `get_recorder` method.") + return self.active_recorder class MLflowExpManager(ExpManager): @@ -153,17 +153,14 @@ class MLflowExpManager(ExpManager): def __init__(self): super(MLflowExpManager, self).__init__() - self.default_uri = None - self.current_uri = None + self.uri = None def start_exp(self, experiment_name=None, uri=None): # create experiment experiment = self.__create_exp(experiment_name, uri) # set up recorder - recorder = MLflowRecorder(experiment.id) + recorder = experiment.create_recorder() self.active_recorder = recorder - # store the recorder - experiment.recorders.append(self.active_recorder) # store the experiment self.experiments[experiment_name] = experiment @@ -178,15 +175,15 @@ class MLflowExpManager(ExpManager): experiment = MLflowExperiment() # set the tracking uri if uri is None: - print( + logger.warning( "No tracking URI is provided. The default tracking URI is set as `mlruns` under the working directory." ) else: - self.current_uri = uri - mlflow.set_tracking_uri(self.current_uri) + self.uri = uri + mlflow.set_tracking_uri(self.uri) # start the experiment if experiment_name is None: - print("No experiment name provided. The default experiment name is set as `experiment`.") + logger.warning("No experiment name provided. The default experiment name is set as `experiment`.") experiment_id = mlflow.create_experiment("experiment") # set the active experiment mlflow.set_experiment("experiment") @@ -227,19 +224,8 @@ class MLflowExpManager(ExpManager): if self.experiments[name].id == experiment_id: return self.experiments[name] else: - print("No valid experiment is found. Please make sure the id and name are correctly given.") + raise Exception("No valid experiment is found. Please make sure the id and name are correctly given.") def delete_exp(self, experiment_id): mlflow.delete_experiment(experiment_id) self.experiments = {key: val for key, val in self.experiments.items() if val.id != experiment_id} - - def get_uri(self, type): - if uri == "default": - return self.default_uri - elif uri == "current": - return self.current_uri - else: - raise ValueError("Input type is not supported. Please choose type default or current to get the uri.") - - def get_recorder(self): - return self.active_recorder diff --git a/qlib/workflow/record_temp.py b/qlib/workflow/record_temp.py new file mode 100644 index 000000000..62ee14405 --- /dev/null +++ b/qlib/workflow/record_temp.py @@ -0,0 +1,137 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +import pandas as pd +from pathlib import Path +from ..contrib.evaluate import ( + backtest as normal_backtest, + risk_analysis, +) +from ..utils import init_instance_by_config, get_module_by_module_path + + +class RecordTemp: + def __init__(self, *args, **kwargs): + pass + + def generate(self, **kwargs): + """ + Generate certain records such as IC, backtest etc., and save them. + + Parameters + ---------- + kwargs + + Return + ------ + The generated records. + """ + raise NotImplementedError(f"Please implement the `generate` method.") + + def check(self, **kwargs): + """ + Check if the records is properly generated and saved. + + Parameters + ---------- + kwargs + """ + raise NotImplementedError(f"Please implement the `check` method.") + + +# TODO: this can only be run under R's running experiment. +class SignalRecord(RecordTemp): + def __init__(self, model, dataset, recorder, **kwargs): + super(SignalRecord, self).__init__() + self.model = model + self.dataset = dataset + self.recorder = recorder + + def generate(self, **kwargs): + # generate prediciton + pred = self.model.predict(self.dataset) + self.recorder.save_object(pred, 'pred.pkl') + + def load(self): + # try to load the saved object + try: + pred = self.recorder.load_object('pred.pkl') + return pred + except: + raise Exception('Something went wrong when loading the saved object.') + + def check(self, **kwargs): + return self.recorder.check('pred.pkl') + + +# TODO +class SigAnaRecord(SignalRecord): + def __init__(self, recorder, **kwargs): + + def generate(self): + pass + + def load(self): + pass + + def check(self): + pass + + +class PortAnaRecord(SignalRecord): + def __init__(self, recorder, STRATEGY_CONFIG, BACKTEST_CONFIG, **kwargs): + self.recorder = recorder + self.STRATEGY_CONFIG = STRATEGY_CONFIG + self.BACKTEST_CONFIG = BACKTEST_CONFIG + module = get_module_by_module_path("qlib.contrib.strategy") + self.strategy = init_instance_by_config(STRATEGY_CONFIG, module) + self.artifact_path = Path('portfolio_analysis').resolve() + + def generate(self, **kwargs): + """ + STRATEGY_CONFIG : dict + define the strategy class as well as the kwargs. + BACKTEST_CONFIG : dict + define the backtest kwargs. + """ + # check previously stored prediction results + assert super().check(), "Make sure the parent process is completed and store the data properly." + # custom strategy and get backtest + pred_score = super().load() + report_normal, positions_normal = normal_backtest(pred_score, strategy=self.strategy, **self.BACKTEST_CONFIG) + self.recorder.save_object(report_normal, 'report_normal.pkl', self.artifact_path) + self.recorder.save_object(positions_normal, 'positions_normal.pkl', self.artifact_path) + + # analysis + 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 + self.recorder.save_object(pred, 'port_analysis.pkl', self.artifact_path) + + def load(self): + # try to load the saved object + try: + pred = self.recorder.load_object(self.artifact_path / 'port_analysis.pkl'') + return pred + except: + raise Exception('Something went wrong when loading the saved object.') + + def check(self): + return self.recorder.check('port_analysis.pkl', self.artifact_path) + + + + + + + + + + + + + + diff --git a/qlib/workflow/record.py b/qlib/workflow/recorder.py similarity index 79% rename from qlib/workflow/record.py rename to qlib/workflow/recorder.py index e132710ca..042b052e0 100644 --- a/qlib/workflow/record.py +++ b/qlib/workflow/recorder.py @@ -21,7 +21,7 @@ class Recorder: def set_recorder_name(self, rname): self.recorder_name = rname - def save_object(self, data, name, local_path=None): + def save_object(self, data=None, name=None, local_path=None, artifact_path=None): """ Save object such as prediction file or model checkpoints to the artifact URI. @@ -33,10 +33,12 @@ class Recorder: name of the file to be saved. local_path : str if provided, them save the file or directory to the artifact URI. + artifact_path=None : str + the relative path for the artifact to be stored in the URI. """ raise NotImplementedError(f"Please implement the `save_object` method.") - def save_objects(self, data_name_list, local_path=None): + def save_objects(self, data_name_list=None, local_path=None, artifact_path=None): """ Save objects such as prediction file or model checkpoints to the artifact URI. @@ -46,6 +48,8 @@ class Recorder: list of (data, name) pairs local_path : str if provided, them save the file or directory to the artifact URI. + artifact_path=None : str + the relative path for the artifact to be stored in the URI. """ raise NotImplementedError(f"Please implement the `save_objects` method.") @@ -162,6 +166,7 @@ class MLflowRecorder(Recorder): # save the run id and artifact_uri self.recorder_id = run.info.run_id self.artifact_uri = run.info.artifact_uri + self._uri = mlflow.get_tracking_uri() # Fix!!! : this is not proper to have uri in recorder # set up file manager for saving objects self.temp_dir = tempfile.mkdtemp() self.fm = FileManager(Path(self.temp_dir).absolute()) @@ -171,27 +176,27 @@ class MLflowRecorder(Recorder): mlflow.end_run() shutil.rmtree(self.temp_dir) - def save_object(self, data, name, local_path=None): + def save_object(self, data=None, name=None, local_path=None, artifact_path=None): + client = mlflow.tracking.MlflowClient(tracking_uri=self._uri) if local_path is None: assert data is not None and name is not None, "Please provide data and name input." self.fm.save_obj(data, name) - mlflow.log_artifact(self.fm.path / name) - self.fm.remove(name) + client.log_artifact(self.recorder_id, self.fm.path / name, artifact_path) else: - mlflow.log_artifact(local_path) + assert local_path is not None, "Please provide a valid local path for the " + client.log_artifact(self.recorder_id, local_path, artifact_path) - def save_objects(self, data_name_list, local_path=None): + def save_objects(self, data_name_list=None, local_path=None, artifact_path=None): + client = mlflow.tracking.MlflowClient(tracking_uri=self._uri) if local_path is None: assert data_name_list is not None, "Please provide data_name_list input." self.fm.save_objs(data_name_list) - mlflow.log_artifacts(self.fm.path) - for obj, name in data_name_list: - self.fm.remove(name) + client.log_artifacts(self.recorder_id, self.fm.path, artifact_path) else: - mlflow.log_artifacts(local_path) + client.log_artifacts(self.recorder_id, local_path, artifact_path) def load_object(self, name): - client = mlflow.tracking.MlflowClient() + client = mlflow.tracking.MlflowClient(tracking_uri=self._uri) path = client.download_artifacts(self.recorder_id, name) try: with Path(path).open("rb") as f: @@ -229,3 +234,11 @@ class MLflowRecorder(Recorder): if self.artifact_uri is not None: return self.artifact_uri return mlflow.get_artifact_uri(artifact_path) + + def check(self, name, path=None): + client = mlflow.tracking.MlflowClient(tracking_uri=self._uri) + artifacts = client.list_artifacts(self.recorder_id, path) + for artifact in artifacts + if name in artifact.path: + return True + return False \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 165619920..f927ce5a2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -22,3 +22,4 @@ scikit_learn==0.23.2 torch==1.6.0 tqdm==4.49.0 yahooquery==2.2.7 +mlflow==1.11.0 \ No newline at end of file diff --git a/setup.py b/setup.py index 3a6237e5a..47ddceaf8 100644 --- a/setup.py +++ b/setup.py @@ -50,6 +50,7 @@ REQUIRED = [ "matplotlib==3.1.3", "tables>=3.6.1", "pyyaml>=5.3.1", + "mlflow>=1.10.0", "tqdm", "loguru", "lightgbm", From 661b3bffcc5407609aa0e15ed0ec650b51f320cd Mon Sep 17 00:00:00 2001 From: Jactus Date: Mon, 2 Nov 2020 11:09:24 +0800 Subject: [PATCH 017/241] Format with black --- qlib/data/data.py | 8 +-- qlib/data/dataset/handler.py | 13 ++-- qlib/data/dataset/loader.py | 17 +++-- qlib/data/ops.py | 2 +- qlib/model/riskmodel.py | 121 +++++++++++++++++++---------------- qlib/portfolio/optimizer.py | 97 +++++++++++++--------------- qlib/workflow/__init__.py | 3 +- qlib/workflow/exp.py | 5 +- qlib/workflow/expm.py | 3 +- qlib/workflow/record_temp.py | 39 ++++------- qlib/workflow/recorder.py | 6 +- 11 files changed, 157 insertions(+), 157 deletions(-) diff --git a/qlib/data/data.py b/qlib/data/data.py index 8eae9f01c..11dc62d91 100644 --- a/qlib/data/data.py +++ b/qlib/data/data.py @@ -1032,12 +1032,12 @@ def register_all_wrappers(): """register_all_wrappers""" logger = get_module_logger("data") module = get_module_by_module_path("qlib.data") - + _calendar_provider = init_instance_by_config(C.calendar_provider, module) if getattr(C, "calendar_cache", None) is not None: _calendar_cache_config = {} _calendar_cache_config.update(C.calendar_cache) - _calendar_cache_config['kwargs'].update(provider=_calendar_provider) + _calendar_cache_config["kwargs"].update(provider=_calendar_provider) _calendar_provider = init_instance_by_config(_calendar_cache_config, module) register_wrapper(Cal, _calendar_provider, "qlib.data") logger.debug(f"registering Cal {C.calendar_provider}-{C.calenar_cache}") @@ -1056,7 +1056,7 @@ def register_all_wrappers(): if getattr(C, "expression_cache", None) is not None: _expression_cache_config = {} _expression_cache_config.update(C.expression_cache) - _expression_cache_config['kwargs'].update(provider=_eprovider) + _expression_cache_config["kwargs"].update(provider=_eprovider) _eprovider = init_instance_by_config(C.expression_cache, module) register_wrapper(ExpressionD, _eprovider, "qlib.data") logger.debug(f"registering ExpressioneD {C.expression_provider}-{C.expression_cache}") @@ -1065,7 +1065,7 @@ def register_all_wrappers(): if getattr(C, "dataset_cache", None) is not None: _dataset_cache_config = {} _dataset_cache_config.update(C.dataset_cache) - _dataset_cache_config['kwargs'].update(provider=_dprovider) + _dataset_cache_config["kwargs"].update(provider=_dprovider) _dprovider = init_instance_by_config(_dataset_cache_config, module) register_wrapper(DatasetD, _dprovider, "qlib.data") logger.debug(f"registering DataseteD {C.dataset_provider}-{C.dataset_cache}") diff --git a/qlib/data/dataset/handler.py b/qlib/data/dataset/handler.py index b8751830d..3e295e3ca 100644 --- a/qlib/data/dataset/handler.py +++ b/qlib/data/dataset/handler.py @@ -125,7 +125,7 @@ class DataHandler(Serializable): selector: Union[pd.Timestamp, slice, str], level: Union[str, int] = "datetime", col_set: Union[str, List[str]] = CS_ALL, - squeeze: bool = False + squeeze: bool = False, ) -> pd.DataFrame: """ fetch data from underlying data source @@ -184,17 +184,18 @@ class DataHandler(Serializable): cur_date (pd.Timestamp or str): current date periods (int): number of periods """ - trading_dates = self._data.index.unique(level='datetime') + trading_dates = self._data.index.unique(level="datetime") cur_loc = trading_dates.get_loc(cur_date) pre_loc = cur_loc - periods + 1 if pre_loc < 0: - warnings.warn('`periods` is too large. the first date will be returned.') + warnings.warn("`periods` is too large. the first date will be returned.") pre_loc = 0 ref_date = trading_dates[pre_loc] return slice(ref_date, cur_date) - def get_range_iterator(self, periods: int, min_periods: Optional[int] = None, - **kwargs) -> Iterator[Tuple[pd.Timestamp, pd.DataFrame]]: + def get_range_iterator( + self, periods: int, min_periods: Optional[int] = None, **kwargs + ) -> Iterator[Tuple[pd.Timestamp, pd.DataFrame]]: """ get a iterator of sliced data with given periods @@ -203,7 +204,7 @@ class DataHandler(Serializable): min_periods (int): minimum periods for sliced dataframe kwargs (dict): will be passed to `self.fetch` """ - trading_dates = self._data.index.unique(level='datetime') + trading_dates = self._data.index.unique(level="datetime") if min_periods is None: min_periods = periods for cur_date in trading_dates[min_periods:]: diff --git a/qlib/data/dataset/loader.py b/qlib/data/dataset/loader.py index abfc695d9..816cf1c4a 100644 --- a/qlib/data/dataset/loader.py +++ b/qlib/data/dataset/loader.py @@ -9,10 +9,12 @@ from typing import Tuple from qlib.data import D + class DataLoader(abc.ABC): - ''' + """ DataLoader is designed for loading raw data from original data source. - ''' + """ + @abc.abstractmethod def load(self, instruments, start_time=None, end_time=None) -> pd.DataFrame: """ @@ -47,7 +49,8 @@ class DataLoader(abc.ABC): class QlibDataLoader(DataLoader): - '''Same as QlibDataLoader. The fields can be define by config''' + """Same as QlibDataLoader. The fields can be define by config""" + def __init__(self, config: Tuple[list, tuple, dict], filter_pipe=None): """ Parameters @@ -64,7 +67,7 @@ class QlibDataLoader(DataLoader): := ["expr", ...] | (["expr", ...], ["col_name", ...]) """ - self.is_group = isinstance(config, dict) + self.is_group = isinstance(config, dict) if self.is_group: self.fields = {grp: self._parse_fields_info(fields_info) for grp, fields_info in config.items()} @@ -86,15 +89,17 @@ class QlibDataLoader(DataLoader): if isinstance(instruments, str): instruments = D.instruments(instruments, filter_pipe=self.filter_pipe) elif self.filter_pipe is not None: - warnings.warn('`filter_pipe` is not None, but it will not be used with `instruments` as list') + warnings.warn("`filter_pipe` is not None, but it will not be used with `instruments` as list") + def _get_df(exprs, names): df = D.features(instruments, exprs, start_time, end_time) df.columns = names return df + if self.is_group: df = pd.concat({grp: _get_df(exprs, names) for grp, (exprs, names) in self.fields.items()}, axis=1) else: exprs, names = self.fields df = _get_df(exprs, names) - df = df.swaplevel().sort_index() # NOTE: always return + df = df.swaplevel().sort_index() # NOTE: always return return df diff --git a/qlib/data/ops.py b/qlib/data/ops.py index d9c657595..fb4b40784 100644 --- a/qlib/data/ops.py +++ b/qlib/data/ops.py @@ -701,7 +701,7 @@ class Rolling(ExpressionOps): if self.N == 0: return np.inf if 0 < self.N < 1: - return int(np.log(1e-6) / np.log(1 - self.N)) # (1 - N)**window == 1e-6 + return int(np.log(1e-6) / np.log(1 - self.N)) # (1 - N)**window == 1e-6 return self.feature.get_longest_back_rolling() + self.N - 1 def get_extended_window_size(self): diff --git a/qlib/model/riskmodel.py b/qlib/model/riskmodel.py index e63b8d4a2..42c2b710f 100644 --- a/qlib/model/riskmodel.py +++ b/qlib/model/riskmodel.py @@ -16,11 +16,11 @@ class RiskModel(BaseModel): A risk model is used to estimate the covariance matrix of stock returns. """ - MASK_NAN = 'mask' - FILL_NAN = 'fill' - IGNORE_NAN = 'ignore' + MASK_NAN = "mask" + FILL_NAN = "fill" + IGNORE_NAN = "ignore" - def __init__(self, nan_option: str = 'ignore', assume_centered: bool = False, scale_return: bool = True): + def __init__(self, nan_option: str = "ignore", assume_centered: bool = False, scale_return: bool = True): """ Args: nan_option (str): nan handling option (`ignore`/`mask`/`fill`) @@ -28,15 +28,19 @@ class RiskModel(BaseModel): scale_return (bool): whether scale returns as percentage """ # nan - assert nan_option in [self.MASK_NAN, self.FILL_NAN, self.IGNORE_NAN], \ - f'`nan_option={nan_option}` is not supported' + assert nan_option in [ + self.MASK_NAN, + self.FILL_NAN, + self.IGNORE_NAN, + ], f"`nan_option={nan_option}` is not supported" self.nan_option = nan_option self.assume_centered = assume_centered self.scale_return = scale_return - def predict(self, X: Union[pd.Series, pd.DataFrame, np.ndarray], - return_corr: bool = False, is_price: bool = True) -> Union[pd.DataFrame, np.ndarray]: + def predict( + self, X: Union[pd.Series, pd.DataFrame, np.ndarray], return_corr: bool = False, is_price: bool = True + ) -> Union[pd.DataFrame, np.ndarray]: """ Args: X (pd.Series, pd.DataFrame or np.ndarray): data from which to estimate the covariance, @@ -53,18 +57,18 @@ class RiskModel(BaseModel): else: if isinstance(X.index, pd.MultiIndex): if isinstance(X, pd.DataFrame): - X = X.iloc[:, 0].unstack(level='instrument') # always use the first column + X = X.iloc[:, 0].unstack(level="instrument") # always use the first column else: - X = X.unstack(level='instrument') + X = X.unstack(level="instrument") else: # X is 2D DataFrame pass - columns = X.columns # will be used to restore dataframe + columns = X.columns # will be used to restore dataframe X = X.values # calculate pct_change if is_price: - X = X[1:] / X[:-1] - 1 # NOTE: resulting `n - 1` rows + X = X[1:] / X[:-1] - 1 # NOTE: resulting `n - 1` rows # scale return if self.scale_return: @@ -106,7 +110,7 @@ class RiskModel(BaseModel): N = len(X) if isinstance(X, np.ma.MaskedArray): M = 1 - X.mask - N = M.T.dot(M) # each pair has distinct number of samples + N = M.T.dot(M) # each pair has distinct number of samples return xTx / N def _preprocess(self, X: np.ndarray) -> Union[np.ndarray, np.ma.MaskedArray]: @@ -165,14 +169,14 @@ class ShrinkCovEstimator(RiskModel): [7] https://www.econ.uzh.ch/dam/jcr:ffffffff-935a-b0d6-0000-0000648dfc98/covMarket.m.zip """ - SHR_LW = 'lw' - SHR_OAS = 'oas' + SHR_LW = "lw" + SHR_OAS = "oas" - TGT_CONST_VAR = 'const_var' - TGT_CONST_CORR = 'const_corr' - TGT_SINGLE_FACTOR = 'single_factor' + TGT_CONST_VAR = "const_var" + TGT_CONST_CORR = "const_corr" + TGT_SINGLE_FACTOR = "single_factor" - def __init__(self, alpha: Union[str, float] = 0.0, target: Union[str, np.ndarray] = 'const_var', **kwargs): + def __init__(self, alpha: Union[str, float] = 0.0, target: Union[str, np.ndarray] = "const_var", **kwargs): """ Args: alpha (str or float): shrinking parameter or estimator (`lw`/`oas`) @@ -183,24 +187,26 @@ class ShrinkCovEstimator(RiskModel): # alpha if isinstance(alpha, str): - assert alpha in [self.SHR_LW, self.SHR_OAS], \ - f'shrinking method `{alpha}` is not supported' + assert alpha in [self.SHR_LW, self.SHR_OAS], f"shrinking method `{alpha}` is not supported" elif isinstance(alpha, (float, np.floating)): - assert 0 <= alpha <= 1, 'alpha should be between [0, 1]' + assert 0 <= alpha <= 1, "alpha should be between [0, 1]" else: - raise TypeError('invalid argument type for `alpha`') + raise TypeError("invalid argument type for `alpha`") self.alpha = alpha # target if isinstance(target, str): - assert target in [self.TGT_CONST_VAR, self.TGT_CONST_CORR, self.TGT_SINGLE_FACTOR], \ - f'shrinking target `{target} is not supported' + assert target in [ + self.TGT_CONST_VAR, + self.TGT_CONST_CORR, + self.TGT_SINGLE_FACTOR, + ], f"shrinking target `{target} is not supported" elif isinstance(target, np.ndarray): pass else: - raise TypeError('invalid argument type for `target`') + raise TypeError("invalid argument type for `target`") if alpha == self.SHR_OAS and target != self.TGT_CONST_VAR: - raise NotImplementedError('currently `oas` can only support `const_var` as target') + raise NotImplementedError("currently `oas` can only support `const_var` as target") self.target = target def _predict(self, X: np.ndarray) -> np.ndarray: @@ -215,7 +221,7 @@ class ShrinkCovEstimator(RiskModel): # shrink covariance if alpha > 0: - S *= (1 - alpha) + S *= 1 - alpha F *= alpha S += F @@ -292,8 +298,8 @@ class ShrinkCovEstimator(RiskModel): alpha = A / B where `n`, `p` are the dim of observations and variables respectively. """ - trS2 = np.sum(S**2) - tr2S = np.trace(S)**2 + trS2 = np.sum(S ** 2) + tr2S = np.trace(S) ** 2 n, p = X.shape @@ -310,10 +316,10 @@ class ShrinkCovEstimator(RiskModel): """ t, n = X.shape - y = X**2 - phi = np.sum(y.T.dot(y) / t - S**2) + y = X ** 2 + phi = np.sum(y.T.dot(y) / t - S ** 2) - gamma = np.linalg.norm(S - F, 'fro')**2 + gamma = np.linalg.norm(S - F, "fro") ** 2 kappa = phi / gamma alpha = max(0, min(1, kappa / t)) @@ -331,15 +337,15 @@ class ShrinkCovEstimator(RiskModel): sqrt_var = np.sqrt(var) r_bar = (np.sum(S / np.outer(sqrt_var, sqrt_var)) - n) / (n * (n - 1)) - y = X**2 - phi_mat = y.T.dot(y) / t - S**2 + y = X ** 2 + phi_mat = y.T.dot(y) / t - S ** 2 phi = np.sum(phi_mat) - theta_mat = (X**3).T.dot(X) / t - var[:, None] * S + theta_mat = (X ** 3).T.dot(X) / t - var[:, None] * S np.fill_diagonal(theta_mat, 0) rho = np.sum(np.diag(phi_mat)) + r_bar * np.sum(np.outer(1 / sqrt_var, sqrt_var) * theta_mat) - gamma = np.linalg.norm(S - F, 'fro')**2 + gamma = np.linalg.norm(S - F, "fro") ** 2 kappa = (phi - rho) / gamma alpha = max(0, min(1, kappa / t)) @@ -357,19 +363,21 @@ class ShrinkCovEstimator(RiskModel): cov_mkt = np.asarray(X.T.dot(X_mkt) / len(X)) var_mkt = np.asarray(X_mkt.dot(X_mkt) / len(X)) - y = X**2 - phi = np.sum(y.T.dot(y)) / t - np.sum(S**2) + y = X ** 2 + phi = np.sum(y.T.dot(y)) / t - np.sum(S ** 2) - rdiag = np.sum(y**2) / t - np.sum(np.diag(S)**2) + rdiag = np.sum(y ** 2) / t - np.sum(np.diag(S) ** 2) z = X * X_mkt[:, None] v1 = y.T.dot(z) / t - cov_mkt[:, None] * S roff1 = np.sum(v1 * cov_mkt[:, None].T) / var_mkt - np.sum(np.diag(v1) * cov_mkt) / var_mkt v3 = z.T.dot(z) / t - var_mkt * S - roff3 = np.sum(v3 * np.outer(cov_mkt, cov_mkt)) / var_mkt**2 - np.sum(np.diag(v3) * cov_mkt**2) / var_mkt**2 + roff3 = ( + np.sum(v3 * np.outer(cov_mkt, cov_mkt)) / var_mkt ** 2 - np.sum(np.diag(v3) * cov_mkt ** 2) / var_mkt ** 2 + ) roff = 2 * roff1 - roff3 rho = rdiag + roff - gamma = np.linalg.norm(S - F, 'fro')**2 + gamma = np.linalg.norm(S - F, "fro") ** 2 kappa = (phi - rho) / gamma alpha = max(0, min(1, kappa / t)) @@ -386,11 +394,11 @@ class POETCovEstimator(RiskModel): [2] http://econweb.rutgers.edu/yl1114/papers/poet/POET.m """ - THRESH_SOFT = 'soft' - THRESH_HARD = 'hard' - THRESH_SCAD = 'scad' + THRESH_SOFT = "soft" + THRESH_HARD = "hard" + THRESH_SCAD = "scad" - def __init__(self, num_factors: int = 0, thresh: float = 1.0, thresh_method: str = 'soft', **kwargs): + def __init__(self, num_factors: int = 0, thresh: float = 1.0, thresh_method: str = "soft", **kwargs): """ Args: num_factors (int): number of factors (if set to zero, no factor model will be used) @@ -403,25 +411,28 @@ class POETCovEstimator(RiskModel): """ super().__init__(**kwargs) - assert num_factors >= 0, '`num_factors` requires a positive integer' + assert num_factors >= 0, "`num_factors` requires a positive integer" self.num_factors = num_factors - assert thresh >= 0, '`thresh` requires a positive float number' + assert thresh >= 0, "`thresh` requires a positive float number" self.thresh = thresh - assert thresh_method in [self.THRESH_HARD, self.THRESH_SOFT, self.THRESH_SCAD], \ - '`thresh_method` should be `soft`/`hard`/`scad`' + assert thresh_method in [ + self.THRESH_HARD, + self.THRESH_SOFT, + self.THRESH_SCAD, + ], "`thresh_method` should be `soft`/`hard`/`scad`" self.thresh_method = thresh_method def _predict(self, X: np.ndarray) -> np.ndarray: - Y = X.T # NOTE: to match POET's implementation + Y = X.T # NOTE: to match POET's implementation p, n = Y.shape if self.num_factors > 0: Dd, V = np.linalg.eig(Y.T.dot(Y)) V = V[:, np.argsort(Dd)] - F = V[:, -self.num_factors:][:, ::-1] * np.sqrt(n) + F = V[:, -self.num_factors :][:, ::-1] * np.sqrt(n) LamPCA = Y.dot(F) / n uhat = np.asarray(Y - LamPCA.dot(F.T)) Lowrank = np.asarray(LamPCA.dot(LamPCA.T)) @@ -434,12 +445,12 @@ class POETCovEstimator(RiskModel): lamb = rate * self.thresh SuPCA = uhat.dot(uhat.T) / n SuDiag = np.diag(np.diag(SuPCA)) - R = np.linalg.inv(SuDiag**0.5).dot(SuPCA).dot(np.linalg.inv(SuDiag**0.5)) + R = np.linalg.inv(SuDiag ** 0.5).dot(SuPCA).dot(np.linalg.inv(SuDiag ** 0.5)) if self.thresh_method == self.THRESH_HARD: M = R * (np.abs(R) > lamb) elif self.thresh_method == self.THRESH_SOFT: - res = (np.abs(R) - lamb) + res = np.abs(R) - lamb res = (res + np.abs(res)) / 2 M = np.sign(R) * res else: @@ -449,7 +460,7 @@ class POETCovEstimator(RiskModel): M = M1 + M2 + M3 Rthresh = M - np.diag(np.diag(M)) + np.eye(p) - SigmaU = (SuDiag**0.5).dot(Rthresh).dot(SuDiag**0.5) + SigmaU = (SuDiag ** 0.5).dot(Rthresh).dot(SuDiag ** 0.5) SigmaY = SigmaU + Lowrank return SigmaY diff --git a/qlib/portfolio/optimizer.py b/qlib/portfolio/optimizer.py index 4b06e25b3..534a66e2d 100644 --- a/qlib/portfolio/optimizer.py +++ b/qlib/portfolio/optimizer.py @@ -22,13 +22,20 @@ class PortfolioOptimizer(object): This optimizer always assumes full investment and no-shorting. """ - OPT_GMV = 'gmv' - OPT_MVO = 'mvo' - OPT_RP = 'rp' - OPT_INV = 'inv' + OPT_GMV = "gmv" + OPT_MVO = "mvo" + OPT_RP = "rp" + OPT_INV = "inv" - def __init__(self, method: str = 'inv', lamb: float = 0, delta: float = 0, - alpha: float = 0.0, scale_alpha: bool = True, tol: float = 1e-8): + def __init__( + self, + method: str = "inv", + lamb: float = 0, + delta: float = 0, + alpha: float = 0.0, + scale_alpha: bool = True, + tol: float = 1e-8, + ): """ Args: method (str): portfolio optimization method @@ -37,24 +44,26 @@ class PortfolioOptimizer(object): alpha (float): l2 norm regularizer tol (float): tolerance for optimization termination """ - assert method in [self.OPT_GMV, self.OPT_MVO, self.OPT_RP, self.OPT_INV], \ - f'method `{method}` is not supported' + assert method in [self.OPT_GMV, self.OPT_MVO, self.OPT_RP, self.OPT_INV], f"method `{method}` is not supported" self.method = method - assert lamb >= 0, f'risk aversion parameter `lamb` should be positive' + assert lamb >= 0, f"risk aversion parameter `lamb` should be positive" self.lamb = lamb - assert delta >= 0, f'turnover limit `delta` should be positive' + assert delta >= 0, f"turnover limit `delta` should be positive" self.delta = delta - assert alpha >= 0, f'l2 norm regularizer `alpha` should be positive' + assert alpha >= 0, f"l2 norm regularizer `alpha` should be positive" self.alpha = alpha self.tol = tol - def __call__(self, S: Union[np.ndarray, pd.DataFrame], - u: Optional[Union[np.ndarray, pd.Series]] = None, - w0: Optional[Union[np.ndarray, pd.Series]] = None) -> Union[np.ndarray, pd.Series]: + def __call__( + self, + S: Union[np.ndarray, pd.DataFrame], + u: Optional[Union[np.ndarray, pd.Series]] = None, + w0: Optional[Union[np.ndarray, pd.Series]] = None, + ) -> Union[np.ndarray, pd.Series]: """ Args: S (np.ndarray or pd.DataFrame): covariance matrix @@ -72,22 +81,22 @@ class PortfolioOptimizer(object): # transform alpha if u is not None: - assert len(u) == len(S), '`u` has mismatched shape' + assert len(u) == len(S), "`u` has mismatched shape" if isinstance(u, pd.Series): - assert all(u.index == index), '`u` has mismatched index' + assert all(u.index == index), "`u` has mismatched index" u = u.values # transform initial weights if w0 is not None: - assert len(w0) == len(S), '`w0` has mismatched shape' + assert len(w0) == len(S), "`w0` has mismatched shape" if isinstance(w0, pd.Series): - assert all(w0.index == index), '`w0` has mismatched index' + assert all(w0.index == index), "`w0` has mismatched index" w0 = w0.values # scale alpha to match volatility if u is not None: u = u / u.std() - u *= np.mean(np.diag(S))**0.5 + u *= np.mean(np.diag(S)) ** 0.5 # optimize w = self._optimize(S, u, w0) @@ -98,21 +107,20 @@ class PortfolioOptimizer(object): return w - def _optimize(self, S: np.ndarray, u: Optional[np.ndarray] = None, - w0: Optional[np.ndarray] = None) -> np.ndarray: + def _optimize(self, S: np.ndarray, u: Optional[np.ndarray] = None, w0: Optional[np.ndarray] = None) -> np.ndarray: # inverse volatility if self.method == self.OPT_INV: if u is not None: - warnings.warn('`u` is set but will not be used for `inv` portfolio') + warnings.warn("`u` is set but will not be used for `inv` portfolio") if w0 is not None: - warnings.warn('`w0` is set but will not be used for `inv` portfolio') + warnings.warn("`w0` is set but will not be used for `inv` portfolio") return self._optimize_inv(S) # global minimum variance if self.method == self.OPT_GMV: if u is not None: - warnings.warn('`u` is set but will not be used for `gmv` portfolio') + warnings.warn("`u` is set but will not be used for `gmv` portfolio") return self._optimize_gmv(S, w0) # mean-variance @@ -122,12 +130,12 @@ class PortfolioOptimizer(object): # risk parity if self.method == self.OPT_RP: if u is not None: - warnings.warn('`u` is set but will not be used for `rp` portfolio') + warnings.warn("`u` is set but will not be used for `rp` portfolio") return self._optimize_rp(S, w0) def _optimize_inv(self, S: np.ndarray) -> np.ndarray: """Inverse volatility""" - vola = np.diag(S)**0.5 + vola = np.diag(S) ** 0.5 w = 1 / vola w /= w.sum() return w @@ -140,14 +148,11 @@ class PortfolioOptimizer(object): s.t. w >= 0, sum(w) == 1 where `S` is the covariance matrix. """ - return self._solve( - len(S), - self._get_objective_gmv(S), - *self._get_constrains(w0) - ) + return self._solve(len(S), self._get_objective_gmv(S), *self._get_constrains(w0)) - def _optimize_mvo(self, S: np.ndarray, u: Optional[np.ndarray] = None, - w0: Optional[np.ndarray] = None) -> np.ndarray: + def _optimize_mvo( + self, S: np.ndarray, u: Optional[np.ndarray] = None, w0: Optional[np.ndarray] = None + ) -> np.ndarray: """optimize mean-variance portfolio This method solves the following optimization problem @@ -156,11 +161,7 @@ class PortfolioOptimizer(object): where `S` is the covariance matrix, `u` is the expected returns, and `lamb` is the risk aversion parameter. """ - return self._solve( - len(S), - self._get_objective_mvo(S, u), - *self._get_constrains(w0) - ) + return self._solve(len(S), self._get_objective_mvo(S, u), *self._get_constrains(w0)) def _optimize_rp(self, S: np.ndarray, w0: Optional[np.ndarray] = None) -> np.ndarray: """optimize risk parity portfolio @@ -170,11 +171,7 @@ class PortfolioOptimizer(object): s.t. w >= 0, sum(w) == 1 where `S` is the covariance matrix and `N` is the number of stocks. """ - return self._solve( - len(S), - self._get_objective_rp(S), - *self._get_constrains(w0) - ) + return self._solve(len(S), self._get_objective_rp(S), *self._get_constrains(w0)) def _get_objective_gmv(self, S: np.ndarray) -> np.ndarray: """global minimum variance optimization objective @@ -213,7 +210,7 @@ class PortfolioOptimizer(object): N = len(x) Sx = S @ x xSx = x @ Sx - return np.sum((x - xSx / Sx / N)**2) + return np.sum((x - xSx / Sx / N) ** 2) return func @@ -230,15 +227,11 @@ class PortfolioOptimizer(object): bounds = so.Bounds(0.0, 1.0) # full investment constraint - cons = [ - {'type': 'eq', 'fun': lambda x: np.sum(x) - 1} # == 0 - ] + cons = [{"type": "eq", "fun": lambda x: np.sum(x) - 1}] # == 0 # turnover constraint if w0 is not None: - cons.append( - {'type': 'ineq', 'fun': lambda x: self.delta - np.sum(np.abs(x - w0))} # >= 0 - ) + cons.append({"type": "ineq", "fun": lambda x: self.delta - np.sum(np.abs(x - w0))}) # >= 0 return bounds, cons @@ -257,9 +250,9 @@ class PortfolioOptimizer(object): wrapped_obj = lambda x: obj(x) + self.alpha * np.sum(np.square(x)) # solve - x0 = np.ones(n) / n # init results + x0 = np.ones(n) / n # init results sol = so.minimize(wrapped_obj, x0, bounds=bounds, constraints=cons, tol=self.tol) if not sol.success: - warnings.warn(f'optimization not success ({sol.status})') + warnings.warn(f"optimization not success ({sol.status})") return sol.x diff --git a/qlib/workflow/__init__.py b/qlib/workflow/__init__.py index 31b9ae2d7..3898dfd35 100644 --- a/qlib/workflow/__init__.py +++ b/qlib/workflow/__init__.py @@ -10,6 +10,7 @@ class QlibRecorder: """ A global system that helps to manage the experiments. """ + def __init__(self, exp_manager, uri): self.exp_manager = exp_manager self.uri = uri @@ -20,7 +21,7 @@ class QlibRecorder: try: yield run except: - self.end_exp() # end the experiment if something went wrong + self.end_exp() # end the experiment if something went wrong self.end_exp() def start_exp(self, experiment_name=None): diff --git a/qlib/workflow/exp.py b/qlib/workflow/exp.py index 335dd338b..672a77d93 100644 --- a/qlib/workflow/exp.py +++ b/qlib/workflow/exp.py @@ -5,6 +5,7 @@ import mlflow from pathlib import Path from .recorder import MLflowRecorder + class Experiment: """ Thie is the `Experiment` class for each experiment being run. The API is designed @@ -21,7 +22,7 @@ class Experiment: Parameters ---------- - + Returns ------- A recorder instance. @@ -84,4 +85,4 @@ class MLflowExperiment(Experiment): def delete_recorder(self, rid): mlflow.delete_run(rid) - self.recorders = [r for r in self.recorders if r.recorder_id == rid] \ No newline at end of file + self.recorders = [r for r in self.recorders if r.recorder_id == rid] diff --git a/qlib/workflow/expm.py b/qlib/workflow/expm.py index 3c633e3bb..34a76e61d 100644 --- a/qlib/workflow/expm.py +++ b/qlib/workflow/expm.py @@ -9,7 +9,8 @@ from .exp import MLflowExperiment from .recorder import MLflowRecorder from ..log import get_module_logger -logger = get_module_logger('workflow', 'Warning') +logger = get_module_logger("workflow", "Warning") + class ExpManager: """ diff --git a/qlib/workflow/record_temp.py b/qlib/workflow/record_temp.py index 62ee14405..e45ef47b6 100644 --- a/qlib/workflow/record_temp.py +++ b/qlib/workflow/record_temp.py @@ -50,23 +50,24 @@ class SignalRecord(RecordTemp): def generate(self, **kwargs): # generate prediciton pred = self.model.predict(self.dataset) - self.recorder.save_object(pred, 'pred.pkl') - + self.recorder.save_object(pred, "pred.pkl") + def load(self): # try to load the saved object try: - pred = self.recorder.load_object('pred.pkl') + pred = self.recorder.load_object("pred.pkl") return pred except: - raise Exception('Something went wrong when loading the saved object.') + raise Exception("Something went wrong when loading the saved object.") def check(self, **kwargs): - return self.recorder.check('pred.pkl') + return self.recorder.check("pred.pkl") # TODO class SigAnaRecord(SignalRecord): def __init__(self, recorder, **kwargs): + pass def generate(self): pass @@ -85,7 +86,7 @@ class PortAnaRecord(SignalRecord): self.BACKTEST_CONFIG = BACKTEST_CONFIG module = get_module_by_module_path("qlib.contrib.strategy") self.strategy = init_instance_by_config(STRATEGY_CONFIG, module) - self.artifact_path = Path('portfolio_analysis').resolve() + self.artifact_path = Path("portfolio_analysis").resolve() def generate(self, **kwargs): """ @@ -99,8 +100,8 @@ 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) - self.recorder.save_object(report_normal, 'report_normal.pkl', self.artifact_path) - self.recorder.save_object(positions_normal, 'positions_normal.pkl', self.artifact_path) + self.recorder.save_object(report_normal, "report_normal.pkl", self.artifact_path) + self.recorder.save_object(positions_normal, "positions_normal.pkl", self.artifact_path) # analysis analysis = dict() @@ -109,29 +110,15 @@ class PortAnaRecord(SignalRecord): report_normal["return"] - report_normal["bench"] - report_normal["cost"] ) analysis_df = pd.concat(analysis) # type: pd.DataFrame - self.recorder.save_object(pred, 'port_analysis.pkl', self.artifact_path) + self.recorder.save_object(pred, "port_analysis.pkl", self.artifact_path) def load(self): # try to load the saved object try: - pred = self.recorder.load_object(self.artifact_path / 'port_analysis.pkl'') + pred = self.recorder.load_object(self.artifact_path / "port_analysis.pkl") return pred except: - raise Exception('Something went wrong when loading the saved object.') + raise Exception("Something went wrong when loading the saved object.") def check(self): - return self.recorder.check('port_analysis.pkl', self.artifact_path) - - - - - - - - - - - - - - + return self.recorder.check("port_analysis.pkl", self.artifact_path) diff --git a/qlib/workflow/recorder.py b/qlib/workflow/recorder.py index 042b052e0..307a740b6 100644 --- a/qlib/workflow/recorder.py +++ b/qlib/workflow/recorder.py @@ -166,7 +166,7 @@ class MLflowRecorder(Recorder): # save the run id and artifact_uri self.recorder_id = run.info.run_id self.artifact_uri = run.info.artifact_uri - self._uri = mlflow.get_tracking_uri() # Fix!!! : this is not proper to have uri in recorder + self._uri = mlflow.get_tracking_uri() # Fix!!! : this is not proper to have uri in recorder # set up file manager for saving objects self.temp_dir = tempfile.mkdtemp() self.fm = FileManager(Path(self.temp_dir).absolute()) @@ -238,7 +238,7 @@ class MLflowRecorder(Recorder): def check(self, name, path=None): client = mlflow.tracking.MlflowClient(tracking_uri=self._uri) artifacts = client.list_artifacts(self.recorder_id, path) - for artifact in artifacts + for artifact in artifacts: if name in artifact.path: return True - return False \ No newline at end of file + return False From 371da2a74c25913f6e794528dfe6dad63479b353 Mon Sep 17 00:00:00 2001 From: Jactus Date: Mon, 2 Nov 2020 16:09:21 +0800 Subject: [PATCH 018/241] Fix --- qlib/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qlib/config.py b/qlib/config.py index 2bd77feb8..878888a84 100644 --- a/qlib/config.py +++ b/qlib/config.py @@ -13,7 +13,7 @@ Two modes are supported import copy from pathlib import Path -import re +import re, os class Config: From 8a0135d79a601391454bd56f18ba97c959f3feac Mon Sep 17 00:00:00 2001 From: Young Date: Mon, 2 Nov 2020 09:42:07 +0000 Subject: [PATCH 019/241] fix data provider init bug --- qlib/__init__.py | 1 + qlib/config.py | 3 ++- qlib/data/data.py | 15 +++------------ qlib/model/base.py | 2 +- qlib/utils/__init__.py | 6 +++--- qlib/workflow/expm.py | 2 +- 6 files changed, 11 insertions(+), 18 deletions(-) diff --git a/qlib/__init__.py b/qlib/__init__.py index 8620acdb7..83c36dbd3 100644 --- a/qlib/__init__.py +++ b/qlib/__init__.py @@ -35,6 +35,7 @@ def init(default_conf="client", **kwargs): if _logging_config: set_log_with_config(_logging_config) + # FIXME: this logger ignored the level in config LOG = get_module_logger("Initialization", level=logging.INFO) LOG.info(f"default_conf: {default_conf}.") diff --git a/qlib/config.py b/qlib/config.py index 878888a84..31acfc535 100644 --- a/qlib/config.py +++ b/qlib/config.py @@ -13,7 +13,8 @@ Two modes are supported import copy from pathlib import Path -import re, os +import re +import os class Config: diff --git a/qlib/data/data.py b/qlib/data/data.py index 11dc62d91..eb92425a8 100644 --- a/qlib/data/data.py +++ b/qlib/data/data.py @@ -1035,10 +1035,7 @@ def register_all_wrappers(): _calendar_provider = init_instance_by_config(C.calendar_provider, module) if getattr(C, "calendar_cache", None) is not None: - _calendar_cache_config = {} - _calendar_cache_config.update(C.calendar_cache) - _calendar_cache_config["kwargs"].update(provider=_calendar_provider) - _calendar_provider = init_instance_by_config(_calendar_cache_config, module) + _calendar_provider = init_instance_by_config(C.calendar_cache, module, provide=_calendar_provider) register_wrapper(Cal, _calendar_provider, "qlib.data") logger.debug(f"registering Cal {C.calendar_provider}-{C.calenar_cache}") @@ -1054,19 +1051,13 @@ def register_all_wrappers(): # This provider is unnecessary in client provider _eprovider = init_instance_by_config(C.expression_provider, module) if getattr(C, "expression_cache", None) is not None: - _expression_cache_config = {} - _expression_cache_config.update(C.expression_cache) - _expression_cache_config["kwargs"].update(provider=_eprovider) - _eprovider = init_instance_by_config(C.expression_cache, module) + _eprovider = init_instance_by_config(C.expression_cache, module, provider=_eprovider) register_wrapper(ExpressionD, _eprovider, "qlib.data") logger.debug(f"registering ExpressioneD {C.expression_provider}-{C.expression_cache}") _dprovider = init_instance_by_config(C.dataset_provider, module) if getattr(C, "dataset_cache", None) is not None: - _dataset_cache_config = {} - _dataset_cache_config.update(C.dataset_cache) - _dataset_cache_config["kwargs"].update(provider=_dprovider) - _dprovider = init_instance_by_config(_dataset_cache_config, module) + _dprovider = init_instance_by_config(C.dataset_cache, module, provider=_dprovider) register_wrapper(DatasetD, _dprovider, "qlib.data") logger.debug(f"registering DataseteD {C.dataset_provider}-{C.dataset_cache}") diff --git a/qlib/model/base.py b/qlib/model/base.py index 3a6ad504e..02333bfb6 100644 --- a/qlib/model/base.py +++ b/qlib/model/base.py @@ -14,7 +14,7 @@ class BaseModel(Serializable, metaclass=abc.ABCMeta): pass def __call__(self, *args, **kwargs) -> object: - """ levarge Python syntactic sugar to make the models' behaviors like functions """ + """ leverage Python syntactic sugar to make the models' behaviors like functions """ return self.predict(*args, **kwargs) diff --git a/qlib/utils/__init__.py b/qlib/utils/__init__.py index ca0ff4c28..dcf2473ef 100644 --- a/qlib/utils/__init__.py +++ b/qlib/utils/__init__.py @@ -195,7 +195,7 @@ def get_cls_kwargs(config: Union[dict, str], module) -> (type, dict): def init_instance_by_config( - config: Union[str, dict], module=None, accept_types: Union[type, Tuple[type]] = tuple([]) + config: Union[str, dict], module=None, accept_types: Union[type, Tuple[type]] = tuple([]), **kwargs ) -> object: """ get initialized instance with config @@ -229,8 +229,8 @@ def init_instance_by_config( if module is None: module = get_module_by_module_path(config["module_path"]) - klass, kwargs = get_cls_kwargs(config, module) - return klass(**kwargs) + klass, cls_kwargs = get_cls_kwargs(config, module) + return klass(**cls_kwargs, **kwargs) def compare_dict_value(src_data: dict, dst_data: dict): diff --git a/qlib/workflow/expm.py b/qlib/workflow/expm.py index 34a76e61d..0f27c200e 100644 --- a/qlib/workflow/expm.py +++ b/qlib/workflow/expm.py @@ -9,7 +9,7 @@ from .exp import MLflowExperiment from .recorder import MLflowRecorder from ..log import get_module_logger -logger = get_module_logger("workflow", "Warning") +logger = get_module_logger("workflow", "WARN") class ExpManager: From 918a2b8a3825a167aebeae61e3403baa8b77a29d Mon Sep 17 00:00:00 2001 From: Jactus Date: Thu, 5 Nov 2020 15:28:31 +0800 Subject: [PATCH 020/241] Fix few exp bugs --- qlib/workflow/__init__.py | 2 +- qlib/workflow/exp.py | 2 +- qlib/workflow/expm.py | 7 ++++--- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/qlib/workflow/__init__.py b/qlib/workflow/__init__.py index 3898dfd35..91880b281 100644 --- a/qlib/workflow/__init__.py +++ b/qlib/workflow/__init__.py @@ -17,7 +17,7 @@ class QlibRecorder: @contextmanager def start(self, experiment_name): - run = self.start_exp(experiment_name, self.uri) + run = self.start_exp(experiment_name) try: yield run except: diff --git a/qlib/workflow/exp.py b/qlib/workflow/exp.py index 672a77d93..9b5517471 100644 --- a/qlib/workflow/exp.py +++ b/qlib/workflow/exp.py @@ -74,7 +74,7 @@ class MLflowExperiment(Experiment): def create_recorder(self): recorder = MLflowRecorder(self.id) self.recorders.append(recorder) - return recorders + return recorder def search_records(self, **kwargs): filter_string = "" if kwargs.get("filter_string") is None else kwargs.get("filter_string") diff --git a/qlib/workflow/expm.py b/qlib/workflow/expm.py index 0f27c200e..d5c0c247e 100644 --- a/qlib/workflow/expm.py +++ b/qlib/workflow/expm.py @@ -9,7 +9,7 @@ from .exp import MLflowExperiment from .recorder import MLflowRecorder from ..log import get_module_logger -logger = get_module_logger("workflow", "WARN") +logger = get_module_logger("workflow", "WARNING") class ExpManager: @@ -168,8 +168,9 @@ class MLflowExpManager(ExpManager): return self.active_recorder.start_run(experiment_id=experiment.id) def end_exp(self): - self.active_recorder.end_run() - self.active_recorder = None + if self.active_recorder is not None: + self.active_recorder.end_run() + self.active_recorder = None def __create_exp(self, experiment_name=None, uri=None): # init experiment From e37950d636600029d3b8b894ca3819289fa72714 Mon Sep 17 00:00:00 2001 From: Young Date: Thu, 5 Nov 2020 10:22:42 +0000 Subject: [PATCH 021/241] update handler & fix some bugs --- qlib/contrib/backtest/position.py | 2 +- qlib/data/client.py | 4 +- qlib/data/data.py | 2 +- qlib/data/dataset/handler.py | 25 ++++++----- qlib/data/dataset/loader.py | 75 ++++++++++++++++++++++++------- qlib/utils/__init__.py | 3 +- 6 files changed, 78 insertions(+), 33 deletions(-) diff --git a/qlib/contrib/backtest/position.py b/qlib/contrib/backtest/position.py index b614c08d0..28b2aa7c9 100644 --- a/qlib/contrib/backtest/position.py +++ b/qlib/contrib/backtest/position.py @@ -160,7 +160,7 @@ class Position: def save_position(self, path, last_trade_date): path = pathlib.Path(path) p = copy.deepcopy(self.position) - cash = pd.Series() + cash = pd.Series(dtype=np.float) cash["init_cash"] = self.init_cash cash["cash"] = p["cash"] cash["today_account_value"] = p["today_account_value"] diff --git a/qlib/data/client.py b/qlib/data/client.py index 2e83726d1..928faaa72 100644 --- a/qlib/data/client.py +++ b/qlib/data/client.py @@ -7,7 +7,7 @@ from __future__ import print_function import socketio -from .. import __version__ +import qlib from ..log import get_module_logger import pickle @@ -59,7 +59,7 @@ class Client(object): msg_queue: Queue The queue to pass the messsage after callback """ - head_info = {"version": __version__} + head_info = {"version": qlib.__version__} def request_callback(*args): """callback_wrapper diff --git a/qlib/data/data.py b/qlib/data/data.py index eb92425a8..6298cfa85 100644 --- a/qlib/data/data.py +++ b/qlib/data/data.py @@ -640,7 +640,7 @@ class LocalFeatureProvider(FeatureProvider): uri_data = self._uri_data.format(instrument.lower(), field, freq) if not os.path.exists(uri_data): get_module_logger("data").warning("WARN: data not found for %s.%s" % (instrument, field)) - return pd.Series() + return pd.Series(dtype=np.float32) # raise ValueError('uri_data not found: ' + uri_data) # load series = read_bin(uri_data, start_index, end_index) diff --git a/qlib/data/dataset/handler.py b/qlib/data/dataset/handler.py index 3e295e3ca..c955a6fe5 100644 --- a/qlib/data/dataset/handler.py +++ b/qlib/data/dataset/handler.py @@ -5,6 +5,7 @@ import abc import bisect import logging +import warnings from typing import Union, Tuple, List, Iterator, Optional import pandas as pd @@ -61,7 +62,9 @@ class DataHandler(Serializable): # Setup data loader assert data_loader is not None # to make start_time end_time could have None default value - self.data_loader = init_instance_by_config(data_loader, data_loader_module, accept_types=DataLoader) + self.data_loader = init_instance_by_config(data_loader, + None if 'module_path' in data_loader else data_loader_module, + accept_types=DataLoader) self.instruments = instruments self.start_time = start_time @@ -224,12 +227,12 @@ class DataHandlerLP(DataHandler): # process type PTYPE_I = "independent" - # - _proc_infer_df will processed by infer_processors - # - _proc_learn_df will be processed by learn_processors + # - self._infer will processed by infer_processors + # - self._learn will be processed by learn_processors PTYPE_A = "append" - # - _proc_infer_df will processed by infer_processors - # - _proc_learn_df will be processed by infer_processors + learn_processors - # - (e.g. _proc_infer_df processed by learn_processors ) + # - self._infer will processed by infer_processors + # - self._learn will be processed by infer_processors + learn_processors + # - (e.g. self._infer processed by learn_processors ) def __init__( self, @@ -265,12 +268,12 @@ class DataHandlerLP(DataHandler): process_type: str PTYPE_I = 'independent' - - _proc_infer_df will processed by infer_processors - - _proc_learn_df will be processed by learn_processors + - self._infer will processed by infer_processors + - self._learn will be processed by learn_processors PTYPE_A = 'append' - - _proc_infer_df will processed by infer_processors - - _proc_learn_df will be processed by infer_processors + learn_processors - - (e.g. _proc_infer_df processed by learn_processors ) + - self._infer will processed by infer_processors + - self._learn will be processed by infer_processors + learn_processors + - (e.g. self._infer processed by learn_processors ) """ # Setup preprocessor diff --git a/qlib/data/dataset/loader.py b/qlib/data/dataset/loader.py index 816cf1c4a..0d1e7be2e 100644 --- a/qlib/data/dataset/loader.py +++ b/qlib/data/dataset/loader.py @@ -14,7 +14,6 @@ class DataLoader(abc.ABC): """ DataLoader is designed for loading raw data from original data source. """ - @abc.abstractmethod def load(self, instruments, start_time=None, end_time=None) -> pd.DataFrame: """ @@ -48,10 +47,13 @@ class DataLoader(abc.ABC): pass -class QlibDataLoader(DataLoader): - """Same as QlibDataLoader. The fields can be define by config""" +class DLWParser(DataLoader): + """ + (D)ata(L)oader (W)ith (P)arser for features and names - def __init__(self, config: Tuple[list, tuple, dict], filter_pipe=None): + Extracting this class so that QlibDataLoader and other dataloaders(such as QdbDataLoader) can share the fields + """ + def __init__(self, config: Tuple[list, tuple, dict]): """ Parameters ---------- @@ -74,8 +76,6 @@ class QlibDataLoader(DataLoader): else: self.fields = self._parse_fields_info(config) - self.filter_pipe = filter_pipe - def _parse_fields_info(self, fields_info: Tuple[list, tuple]) -> Tuple[list, list]: if isinstance(fields_info, list): exprs = names = fields_info @@ -85,21 +85,62 @@ class QlibDataLoader(DataLoader): raise NotImplementedError(f"This type of input is not supported") return exprs, names - def load(self, instruments, start_time=None, end_time=None) -> pd.DataFrame: + @abc.abstractmethod + def load_group_df(self, instruments, exprs: list, names: list, start_time=None, end_time=None) -> pd.DataFrame: + """ + load the dataframe for specific group + + Parameters + ---------- + instruments : + the instruments + exprs : list + The expressions to describe the content of the data + names : list + The name of the data + + Returns + ------- + pd.DataFrame: + the queried dataframe + """ + pass + + def load(self, instruments=None, start_time=None, end_time=None) -> pd.DataFrame: + if self.is_group: + df = pd.concat( + { + grp: self.load_group_df(instruments, exprs, names, start_time, end_time) + for grp, (exprs, names) in self.fields.items() + }, + axis=1) + else: + exprs, names = self.fields + df = self.load_group_df(instruments, exprs, names, start_time, end_time) + return df + + +class QlibDataLoader(DLWParser): + """Same as QlibDataLoader. The fields can be define by config""" + def __init__(self, config: Tuple[list, tuple, dict], filter_pipe=None): + """ + Parameters + ---------- + config : Tuple[list, tuple, dict] + Please refer to the doc of DLWParser + filter_pipe : + Filter pipe for the instruments + """ + self.filter_pipe = filter_pipe + super().__init__(config) + + def load_group_df(self, instruments, exprs: list, names: list, start_time=None, end_time=None) -> pd.DataFrame: if isinstance(instruments, str): instruments = D.instruments(instruments, filter_pipe=self.filter_pipe) elif self.filter_pipe is not None: warnings.warn("`filter_pipe` is not None, but it will not be used with `instruments` as list") - def _get_df(exprs, names): - df = D.features(instruments, exprs, start_time, end_time) - df.columns = names - return df - - if self.is_group: - df = pd.concat({grp: _get_df(exprs, names) for grp, (exprs, names) in self.fields.items()}, axis=1) - else: - exprs, names = self.fields - df = _get_df(exprs, names) + df = D.features(instruments, exprs, start_time, end_time) + df.columns = names df = df.swaplevel().sort_index() # NOTE: always return return df diff --git a/qlib/utils/__init__.py b/qlib/utils/__init__.py index dcf2473ef..d0b47d3cd 100644 --- a/qlib/utils/__init__.py +++ b/qlib/utils/__init__.py @@ -44,7 +44,7 @@ def read_bin(file_path, start_index, end_index): ref_start_index = int(np.frombuffer(f.read(4), dtype=" end_index: - return pd.Series() + return pd.Series(np.float32) # calculate offset f.seek(4 * (si - ref_start_index) + 4) # read nbytes @@ -213,6 +213,7 @@ def init_instance_by_config( "ClassName": getattr(module, config)() will be used. module : Python module Optional. It should be a python module. + NOTE: the "module_path" will be override by `module` arguments accept_types: Union[type, Tuple[type]] Optional. If the config is a instance of specific type, return the config directly. From 65a009acb2850abcae59ad6633c265c3c4bcfe2c Mon Sep 17 00:00:00 2001 From: Young Date: Thu, 5 Nov 2020 12:30:43 +0000 Subject: [PATCH 022/241] remove vwap related feature --- qlib/contrib/data/handler.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/qlib/contrib/data/handler.py b/qlib/contrib/data/handler.py index 2e7f2febc..bd07fff7e 100644 --- a/qlib/contrib/data/handler.py +++ b/qlib/contrib/data/handler.py @@ -90,7 +90,7 @@ class Alpha158(DataHandlerLP): "kbar": {}, "price": { "windows": [0], - "feature": ["OPEN", "HIGH", "LOW", "VWAP"], + "feature": ["OPEN", "HIGH", "LOW"], }, "rolling": {}, } @@ -281,5 +281,17 @@ class Alpha158(DataHandlerLP): class Alpha158vwap(Alpha158): + + def get_feature_config(self): + conf = { + "kbar": {}, + "price": { + "windows": [0], + "feature": ["OPEN", "HIGH", "LOW", "VWAP"], + }, + "rolling": {}, + } + return self.parse_config_to_fields(conf) + def get_label_config(self): return (["Ref($vwap, -2)/Ref($vwap, -1) - 1"], ["LABEL0"]) From 1556be6798e08bec4b38a0bf59e8dbe36572ae49 Mon Sep 17 00:00:00 2001 From: Haoyu Wang Date: Fri, 6 Nov 2020 16:18:56 +0800 Subject: [PATCH 023/241] Adding Catboost as a model --- qlib/contrib/model/catboost_model.py | 72 ++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 qlib/contrib/model/catboost_model.py diff --git a/qlib/contrib/model/catboost_model.py b/qlib/contrib/model/catboost_model.py new file mode 100644 index 000000000..ee9d5a4f8 --- /dev/null +++ b/qlib/contrib/model/catboost_model.py @@ -0,0 +1,72 @@ +import numpy as np +import pandas as pd +from catboost import Pool, CatBoost +from catboost.utils import get_gpu_device_count + +from ...model.base import Model +from ...data.dataset import DatasetH +from ...data.dataset.handler import DataHandlerLP + + +class CatBoostModel(Model): + """CatBoost Model""" + + def __init__(self, loss="RMSE", **kwargs): + # There are more options + if loss not in {"RMSE", "Logloss"}: + raise NotImplementedError + self._params = {"loss_function": loss} + self._params.update(kwargs) + self.model = None + + def fit( + self, + dataset: DatasetH, + num_boost_round=1000, + early_stopping_rounds=50, + verbose_eval=20, + evals_result=dict(), + **kwargs + ): + df_train, df_valid = dataset.prepare( + ["train", "valid"], col_set=["feature", "label"], data_key=DataHandlerLP.DK_L + ) + x_train, y_train = df_train["feature"], df_train["label"] + x_valid, y_valid = df_valid["feature"], df_valid["label"] + + # CatBoost needs 1D array as its label + if y_train.values.ndim == 2 and y_train.values.shape[1] == 1: + y_train_1d, y_valid_1d = np.squeeze(y_train.values), np.squeeze(y_valid.values) + else: + raise ValueError("CatBoost doesn't support multi-label training") + + train_pool = Pool(data = x_train, label = y_train_1d) + valid_pool = Pool(data = x_valid, label = y_valid_1d) + + #Initialize the catboost model + self._params['iterations'] = num_boost_round + self._params['early_stopping_rounds'] = early_stopping_rounds + self._params['verbose_eval'] = verbose_eval + self._params['task_type'] = "GPU" if get_gpu_device_count() > 0 else "CPU" + self.model = CatBoost(self._params, **kwargs) + + #train the model + self.model.fit( + train_pool, + eval_set = valid_pool, + use_best_model = True + ) + + evals_result["train"] = list(self.model.get_evals_result().values())[0] + evals_result["valid"] = self.model.get_test_eval() + + + def predict(self, dataset): + if self.model is None: + raise ValueError("model is not fitted yet!") + x_test = dataset.prepare("test", col_set="feature") + return pd.Series(self.model.predict(np.squeeze(x_test.values)), index=x_test.index) + + +if __name__ == '__main__': + cat = CatBoostModel() \ No newline at end of file From eead71fcb55479a896eb880ec08529ba2b465a80 Mon Sep 17 00:00:00 2001 From: Haoyu Wang Date: Fri, 6 Nov 2020 17:47:43 +0800 Subject: [PATCH 024/241] minor fix --- qlib/contrib/model/catboost_model.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/qlib/contrib/model/catboost_model.py b/qlib/contrib/model/catboost_model.py index ee9d5a4f8..8a55d0385 100644 --- a/qlib/contrib/model/catboost_model.py +++ b/qlib/contrib/model/catboost_model.py @@ -54,11 +54,13 @@ class CatBoostModel(Model): self.model.fit( train_pool, eval_set = valid_pool, - use_best_model = True + use_best_model = True, + **kwargs ) - - evals_result["train"] = list(self.model.get_evals_result().values())[0] - evals_result["valid"] = self.model.get_test_eval() + + evals_result = self.model.get_evals_result() + evals_result["train"] = list(evals_result["learn"].values())[0] + evals_result["valid"] = list(evals_result["validation"].values())[0] def predict(self, dataset): From 9a826eefa325d528713d0a83c8043e19332311fa Mon Sep 17 00:00:00 2001 From: Young Date: Fri, 6 Nov 2020 10:24:21 +0000 Subject: [PATCH 025/241] add parallel processor --- qlib/data/dataset/handler.py | 23 ++++++++++++--------- qlib/data/dataset/processor.py | 3 ++- qlib/log.py | 8 ++++---- qlib/utils/paral.py | 37 ++++++++++++++++++++++++++++++++++ setup.py | 1 + 5 files changed, 58 insertions(+), 14 deletions(-) create mode 100644 qlib/utils/paral.py diff --git a/qlib/data/dataset/handler.py b/qlib/data/dataset/handler.py index c955a6fe5..6ead332d2 100644 --- a/qlib/data/dataset/handler.py +++ b/qlib/data/dataset/handler.py @@ -70,7 +70,8 @@ class DataHandler(Serializable): self.start_time = start_time self.end_time = end_time if init_data: - self.init() + with TimeInspector.logt("Init data"): + self.init() super().__init__() def init(self, enable_cache: bool = True): @@ -91,7 +92,8 @@ class DataHandler(Serializable): """ # Setup data. # _data may be with multiple column index level. The outer level indicates the feature set name - self._data = self.data_loader.load(self.instruments, self.start_time, self.end_time) + with TimeInspector.logt("Loading data"): + self._data = self.data_loader.load(self.instruments, self.start_time, self.end_time) # TODO: cache def _fetch_df_by_index( @@ -293,7 +295,8 @@ class DataHandlerLP(DataHandler): def fit(self): for proc in self.get_all_processors(): - proc.fit(self._data) + with TimeInspector.logt(f"{proc.__class__.__name__}"): + proc.fit(self._data) def fit_process_data(self): """ @@ -320,9 +323,10 @@ class DataHandlerLP(DataHandler): for proc in self.infer_processors: if not proc.is_for_infer(): raise TypeError("Only processors usable for inference can be used in `infer_processors` ") - if with_fit: - proc.fit(_infer_df) - _infer_df = proc(_infer_df) + with TimeInspector.logt(f"{proc.__class__.__name__}"): + if with_fit: + proc.fit(_infer_df) + _infer_df = proc(_infer_df) self._infer = _infer_df # data for learning @@ -337,9 +341,10 @@ class DataHandlerLP(DataHandler): if len(self.learn_processors) > 0: # avoid modifying the original data _learn_df = _learn_df.copy() for proc in self.learn_processors: - if with_fit: - proc.fit(_learn_df) - _learn_df = proc(_learn_df) + with TimeInspector.logt(f"{proc.__class__.__name__}"): + if with_fit: + proc.fit(_learn_df) + _learn_df = proc(_learn_df) self._learn = _learn_df # init type diff --git a/qlib/data/dataset/processor.py b/qlib/data/dataset/processor.py index 3fc91f52c..b52426d16 100644 --- a/qlib/data/dataset/processor.py +++ b/qlib/data/dataset/processor.py @@ -8,6 +8,7 @@ import copy from ...log import TimeInspector from ...utils.serial import Serializable +from ...utils.paral import datetime_groupby_apply EPS = 1e-12 @@ -99,7 +100,7 @@ class ProcessInf(Processor): df[col] = df[col].replace([np.inf, -np.inf], df[col][~np.isinf(df[col])].mean()) return df - data = data.groupby("datetime").apply(process_inf) + data = datetime_groupby_apply(data, process_inf) data.sort_index(inplace=True) return data diff --git a/qlib/log.py b/qlib/log.py index 1f06f87f5..422a4c00b 100644 --- a/qlib/log.py +++ b/qlib/log.py @@ -78,10 +78,10 @@ class TimeInspector(object): Info that will be log into stdout. """ cost_time = time() - cls.time_marks.pop() - cls.timer_logger.info("Time cost: {0:.5f} | {1}".format(cost_time, info)) + cls.timer_logger.info("Time cost: {0:.3f}s | {1}".format(cost_time, info)) - @contextmanager @classmethod + @contextmanager def logt(cls, name="", show_start=False): """logt. Log the time of the inside code @@ -94,13 +94,13 @@ class TimeInspector(object): show_start """ if show_start: - cls.timer_logger.info(f"Begin {name}") + cls.timer_logger.info(f"{name} Begin") cls.set_time_mark() try: yield None finally: pass - cls.log_cost_time() + cls.log_cost_time(info=f"{name} Done") def set_log_with_config(log_config: dict): diff --git a/qlib/utils/paral.py b/qlib/utils/paral.py new file mode 100644 index 000000000..c709047b9 --- /dev/null +++ b/qlib/utils/paral.py @@ -0,0 +1,37 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +from joblib import Parallel, delayed +import pandas as pd + + +def datetime_groupby_apply(df, apply_func, axis=0, level='datetime', resample_rule="M", n_jobs=-1, skip_group=False): + """ datetime_groupby_apply + This function will apply the `apply_func` on the datetime level index. + + Parameters + ---------- + df : + DataFrame for processing + apply_func : + apply_func for processing the data + axis : + which axis is the datetime level located + level : + which level is the datetime level + resample_rule : + How to resample the data to calculating parallel + n_jobs : + n_jobs for joblib + Returns: + pd.DataFrame + """ + def _naive_group_apply(df): + return df.groupby(axis=axis, level=level).apply(apply_func) + + if n_jobs != 1: + dfs = Parallel(n_jobs=n_jobs)(delayed(_naive_group_apply)(sub_df) + for idx, sub_df in df.resample(resample_rule, axis=axis, level=level)) + return pd.concat(dfs, axis=axis).sort_index() + else: + return _naive_group_apply(df) diff --git a/setup.py b/setup.py index 47ddceaf8..7e1bc1583 100644 --- a/setup.py +++ b/setup.py @@ -55,6 +55,7 @@ REQUIRED = [ "loguru", "lightgbm", "tornado", + "joblib>=0.17.0" ] # Numpy include From 853410c16eb79708371b87a4af37de18840e03f2 Mon Sep 17 00:00:00 2001 From: Jactus Date: Mon, 9 Nov 2020 16:42:21 +0800 Subject: [PATCH 026/241] Update exp related and pytorch_nn --- qlib/contrib/model/pytorch_nn.py | 54 ++++++++---- qlib/workflow/__init__.py | 28 +++---- qlib/workflow/exp.py | 114 ++++++++++++++++++++++--- qlib/workflow/expm.py | 61 ++++++-------- qlib/workflow/record_temp.py | 57 ++++++++++--- qlib/workflow/recorder.py | 140 ++++++++++++++++--------------- 6 files changed, 297 insertions(+), 157 deletions(-) diff --git a/qlib/contrib/model/pytorch_nn.py b/qlib/contrib/model/pytorch_nn.py index b5bf91472..1acb5c843 100644 --- a/qlib/contrib/model/pytorch_nn.py +++ b/qlib/contrib/model/pytorch_nn.py @@ -6,18 +6,20 @@ from __future__ import division from __future__ import print_function import os +import logging import numpy as np import pandas as pd from sklearn.metrics import roc_auc_score, mean_squared_error -import logging -from ...utils import unpack_archive_with_buffer, save_multiple_parts_file, create_save_path, drop_nan_by_y_index -from ...log import get_module_logger, TimeInspector import torch import torch.nn as nn import torch.optim as optim from ...model.base import Model +from ...data.dataset import DatasetH +from ...data.dataset.handler import DataHandlerLP +from ...utils import unpack_archive_with_buffer, save_multiple_parts_file, create_save_path, drop_nan_by_y_index +from ...log import get_module_logger, TimeInspector class DNNModelPytorch(Model): @@ -144,20 +146,25 @@ class DNNModelPytorch(Model): def fit( self, - x_train, - y_train, - x_valid, - y_valid, - w_train=None, - w_valid=None, + dataset: DatasetH, evals_result=dict(), verbose=True, save_path=None, ): - if w_train is None: + df_train, df_valid = dataset.prepare( + ["train", "valid"], col_set=["feature", "label"], data_key=DataHandlerLP.DK_L + ) + x_train, y_train = df_train["feature"], df_train["label"] + x_valid, y_valid = df_valid["feature"], df_valid["label"] + + try: + wdf_train, wdf_valid = dataset.prepare( + ["train", "valid"], col_set=["weight"], data_key=DataHandlerLP.DK_L + ) + w_train, w_valid = wdf_train["weight"], wdf_valid["weight"] + except: w_train = pd.DataFrame(np.ones_like(y_train.values), index=y_train.index) - if w_valid is None: w_valid = pd.DataFrame(np.ones_like(y_valid.values), index=y_valid.index) save_path = create_save_path(save_path) @@ -188,6 +195,7 @@ class DNNModelPytorch(Model): w_val_auto = w_val_auto.cuda() for step in range(self.max_steps): + self.logger.info(step) if stop_steps >= self.early_stop_rounds: if verbose: self.logger.info("\tearly stop") @@ -195,6 +203,7 @@ class DNNModelPytorch(Model): loss = AverageMeter() self.dnn_model.train() self.train_optimizer.zero_grad() + self.logger.info("INIT") choice = np.random.choice(train_num, self.batch_size) x_batch_auto = x_train_values[choice] @@ -264,10 +273,11 @@ class DNNModelPytorch(Model): else: raise NotImplementedError("loss {} is not supported!".format(loss_type)) - def predict(self, x_test): + def predict(self, dataset): if not self._fitted: raise ValueError("model is not fitted yet!") - x_test = torch.from_numpy(x_test.values).float() + x_test_pd = dataset.prepare("test", col_set="feature") + x_test = torch.from_numpy(x_test_pd.values).float() if self.use_gpu: x_test = x_test.cuda() self.dnn_model.eval() @@ -277,13 +287,20 @@ class DNNModelPytorch(Model): preds = self.dnn_model(x_test).detach().cpu().numpy() else: preds = self.dnn_model(x_test).detach().numpy() - return preds + return pd.Series(np.squeeze(preds), index=x_test_pd.index) def score(self, x_test, y_test, w_test=None): # Remove rows from x, y and w, which contain Nan in any columns in y_test. + df_test = dataset.prepare("test", col_set=["feature", "label"]) + x_test, y_test = df_test["feature"], df_test["label"] x_test, y_test, w_test = drop_nan_by_y_index(x_test, y_test, w_test) preds = self.predict(x_test) - w_test_weight = None if w_test is None else w_test.values + try: + df_test = dataset.prepare("test", col_set=["weight"]) + w_test = df_test["weight"] + w_test_weight = w_test.values + except: + w_test_weight = None return self._scorer(y_test.values, preds, sample_weight=w_test_weight) def save(self, filename, **kwargs): @@ -303,7 +320,12 @@ class DNNModelPytorch(Model): self.dnn_model.load_state_dict(torch.load(_model_path)) self._fitted = True - def finetune(self, x_train, y_train, x_valid, y_valid, w_train=None, w_valid=None, **kwargs): + def finetune(self, dataset, w_train=None, w_valid=None, **kwargs): + df_train, df_valid = dataset.prepare( + ["train", "valid"], col_set=["feature", "label"], data_key=DataHandlerLP.DK_L + ) + x_train, y_train = df_train["feature"], df_train["label"] + x_valid, y_valid = df_valid["feature"], df_valid["label"] self.fit(x_train, y_train, x_valid, y_valid, w_train=w_train, w_valid=w_valid, **kwargs) diff --git a/qlib/workflow/__init__.py b/qlib/workflow/__init__.py index 91880b281..a941ed7cf 100644 --- a/qlib/workflow/__init__.py +++ b/qlib/workflow/__init__.py @@ -4,31 +4,32 @@ from contextlib import contextmanager from .expm import MLflowExpManager from ..utils import Wrapper - +from ..config import C class QlibRecorder: """ A global system that helps to manage the experiments. """ - def __init__(self, exp_manager, uri): + def __init__(self, exp_manager): self.exp_manager = exp_manager - self.uri = uri + self.uri = C["exp_uri"] @contextmanager def start(self, experiment_name): run = self.start_exp(experiment_name) try: yield run - except: - self.end_exp() # end the experiment if something went wrong - self.end_exp() + except Exception as e: + self.end_exp("FAILED") # end the experiment if something went wrong + raise e + self.end_exp("FINISHED") def start_exp(self, experiment_name=None): return self.exp_manager.start_exp(experiment_name, self.uri) - def end_exp(self): - self.exp_manager.end_exp() + def end_exp(self, status): + self.exp_manager.end_exp(status) def search_records(self, experiment_ids, **kwargs): return self.exp_manager.search_records(experiment_ids, **kwargs) @@ -45,11 +46,8 @@ class QlibRecorder: def get_recorder(self): return self.exp_manager.active_recorder - def save_object(self, data=None, name=None, local_path=None): - self.exp_manager.active_recorder.save_object(data, name, local_path) - - def save_objects(self, data_name_list=None, local_path=None): - self.exp_manager.active_recorder.save_objects(data_name_list, local_path) + def save_objects(self, local_path=None, artifact_path=None, **kwargs): + self.exp_manager.active_recorder.save_objects(local_path, artifact_path, **kwargs) def load_object(self, name): return self.exp_manager.active_recorder.load_object(name) @@ -63,8 +61,8 @@ class QlibRecorder: def set_tags(self, **kwargs): self.exp_manager.active_recorder.set_tags(**kwargs) - def delete_tag(self, key): - self.exp_manager.active_recorder.delete_tag(key) + def delete_tag(self, *key): + self.exp_manager.active_recorder.delete_tag(*key) # global record diff --git a/qlib/workflow/exp.py b/qlib/workflow/exp.py index 9b5517471..86163c0ea 100644 --- a/qlib/workflow/exp.py +++ b/qlib/workflow/exp.py @@ -14,7 +14,47 @@ class Experiment: def __init__(self): self.name = None self.id = None - self.recorders = list() + self.active_recorder = None # only one recorder can running each time + self.recorders = dict() # recorder id -> object + + def __repr__(self): + return str(self.info) + + def __str__(self): + return str(self.info) + + @property + def info(self): + output = dict() + output['class'] = "Experiment" + output['id'] = self.id + output['name'] = self.name + output['active_recorder'] = self.active_recorder.id + output['recorders'] = list(self.recorders.keys()) + + def start(self): + """ + Start the experiment. + + Parameters + ---------- + + Returns + ------- + A running recorder instance. + """ + raise NotImplementedError(f"Please implement the `start` method.") + + def end(self, status): + """ + End the experiment. + + Parameters + ---------- + status : str + the status the recorder to be set with when ending (SCHEDULED, RUNNING, FINISHED, FAILED). + """ + raise NotImplementedError(f"Please implement the `end` method.") def create_recorder(self): """ @@ -25,7 +65,7 @@ class Experiment: Returns ------- - A recorder instance. + A recorder object. """ raise NotImplementedError(f"Please implement the `create_recorder` method.") @@ -46,24 +86,40 @@ class Experiment: Returns ------- - A pandas.DataFrame of records. + A pandas.DataFrame of records, where each metric, parameter, and tag + are expanded into their own columns named metrics.*, params.*, and tags.* + respectively. For records that don't have a particular metric, parameter, or tag, their + value will be (NumPy) Nan, None, or None respectively. """ raise NotImplementedError(f"Please implement the `search_records` method.") - def delete_recorder(self, rid): + def delete_recorder(self, recorder_id): """ Create a recorder for each experiment. Parameters ---------- - rid : str + recorder_id : str the id of the recorder to be deleted. + """ + raise NotImplementedError(f"Please implement the `delete_recorder` method.") + + def get_recorder(self, recorder_id=None, recorder_name=None): + """ + Get the current active Recorder. + + Parameters + ---------- + recorder_id : str + the id of the recorder to be deleted. + recorder_name : str + the name of the recorder to be deleted. Returns ------- - A recorder instance. + A recorder object. """ - raise NotImplementedError(f"Please implement the `delete_recorder` method.") + raise NotImplementedError(f"Please implement the `get_recorder` method.") class MLflowExperiment(Experiment): @@ -71,9 +127,26 @@ class MLflowExperiment(Experiment): Use mlflow to implement Experiment. """ + def start(self): + # set up recorder + recorder = self.create_recorder() + self.active_recorder = recorder + # start the recorder + run = self.active_recorder.start_run() + # store the recorder + self.recorders[self.active_recorder.id] = recorder + + return self.active_recorder + + def end(self, status): + if self.active_recorder is not None: + self.active_recorder.end_run(status) + self.active_recorder = None + def create_recorder(self): - recorder = MLflowRecorder(self.id) - self.recorders.append(recorder) + num = len(self.recorders) + name = "Recorder_{}".format(num+1) + recorder = MLflowRecorder(name, self.id) return recorder def search_records(self, **kwargs): @@ -81,8 +154,23 @@ class MLflowExperiment(Experiment): run_view_type = 1 if kwargs.get("run_view_type") is None else kwargs.get("run_view_type") max_results = 100000 if kwargs.get("max_results") is None else kwargs.get("max_results") order_by = kwargs.get("order_by") - return mlflow.search_runs([self.experiment_id], filter_string, run_view_type, max_results, order_by) + return mlflow.search_runs([self.id], filter_string, run_view_type, max_results, order_by) - def delete_recorder(self, rid): - mlflow.delete_run(rid) - self.recorders = [r for r in self.recorders if r.recorder_id == rid] + def delete_recorder(self, recorder_id): + mlflow.delete_run(recorder_id) + self.recorders = [r for r in self.recorders if r.id == recorder_id] + + def get_recorder(self, recorder_id=None, recorder_name=None): + if recorder_id is not None: + return self.recorders[recorder_id] + elif recorder_name is not None: + for rid in self.recorders: + if self.recorders[rid].name == recorder_name: + return self.recorders[rid] + elif self.active_recorder is None: + raise Exception('No valid active recorder exists. Please make sure the experiment is running.') + else: + logger.info( + "No experiment id or name is given. Return the current active experiment." + ) + return self.active_recorder \ No newline at end of file diff --git a/qlib/workflow/expm.py b/qlib/workflow/expm.py index d5c0c247e..e81da0fcb 100644 --- a/qlib/workflow/expm.py +++ b/qlib/workflow/expm.py @@ -9,7 +9,7 @@ from .exp import MLflowExperiment from .recorder import MLflowRecorder from ..log import get_module_logger -logger = get_module_logger("workflow", "WARNING") +logger = get_module_logger("workflow", "INFO") class ExpManager: @@ -20,7 +20,7 @@ class ExpManager: def __init__(self): self.uri = None - self.active_recorder = None # only one recorder can running each time + self.active_experiment = None # only one experiment can running each time self.experiments = dict() # store the experiment name --> Experiment object def start_exp(self, experiment_name=None, uri=None, **kwargs): @@ -39,7 +39,7 @@ class ExpManager: controls whether run is nested in parent run. Returns - An object wrapped by context manager. + An active recorder. """ raise NotImplementedError(f"Please implement the `start_exp` method.") @@ -73,11 +73,14 @@ class ExpManager: Returns ------- - A pandas.DataFrame of runs. + A pandas.DataFrame of records, where each metric, parameter, and tag + are expanded into their own columns named metrics.*, params.*, and tags.* + respectively. For records that don't have a particular metric, parameter, or tag, their + value will be (NumPy) Nan, None, or None respectively. """ raise NotImplementedError(f"Please implement the `search_records` method.") - def __create_exp(self, experiment_name, artifact_location=None): + def create_exp(self, experiment_name, artifact_location=None): """ Create an experiment. @@ -133,19 +136,6 @@ class ExpManager: """ return self.uri - def get_recorder(self): - """ - Get the current active Recorder. - - Parameters - ---------- - - Returns - ------- - An Recorder object. - """ - return self.active_recorder - class MLflowExpManager(ExpManager): """ @@ -158,26 +148,27 @@ class MLflowExpManager(ExpManager): def start_exp(self, experiment_name=None, uri=None): # create experiment - experiment = self.__create_exp(experiment_name, uri) - # set up recorder - recorder = experiment.create_recorder() - self.active_recorder = recorder + experiment = self.create_exp(experiment_name, uri) + # set up active experiment + self.active_experiment = experiment # store the experiment self.experiments[experiment_name] = experiment + # start the experiment + self.active_experiment.start() - return self.active_recorder.start_run(experiment_id=experiment.id) + return self.active_experiment - def end_exp(self): - if self.active_recorder is not None: - self.active_recorder.end_run() - self.active_recorder = None + def end_exp(self, status): + if self.active_experiment is not None: + self.active_experiment.end(status) + self.active_experiment = None - def __create_exp(self, experiment_name=None, uri=None): + def create_exp(self, experiment_name=None, uri=None): # init experiment experiment = MLflowExperiment() # set the tracking uri if uri is None: - logger.warning( + logger.info( "No tracking URI is provided. The default tracking URI is set as `mlruns` under the working directory." ) else: @@ -185,7 +176,7 @@ class MLflowExpManager(ExpManager): mlflow.set_tracking_uri(self.uri) # start the experiment if experiment_name is None: - logger.warning("No experiment name provided. The default experiment name is set as `experiment`.") + logger.info("No experiment name provided. The default experiment name is set as `experiment`.") experiment_id = mlflow.create_experiment("experiment") # set the active experiment mlflow.set_experiment("experiment") @@ -216,17 +207,19 @@ class MLflowExpManager(ExpManager): return mlflow.search_runs(experiment_ids, filter_string, run_view_type, max_results, order_by) def get_exp(self, experiment_id=None, experiment_name=None): - assert ( - experiment_id is not None or experiment_name is not None - ), "Please provide at least one of the experiment id or name to retrieve an experiment." if experiment_name is not None: return self.experiments[experiment_name] elif experiment_id is not None: for name in self.experiments: if self.experiments[name].id == experiment_id: return self.experiments[name] + elif self.active_experiment is None: + raise Exception('No valid active experiment exists. Please make sure experiment manager is running.') else: - raise Exception("No valid experiment is found. Please make sure the id and name are correctly given.") + logger.info( + "No experiment id or name is given. Return the current active experiment." + ) + return self.active_experiment def delete_exp(self, experiment_id): mlflow.delete_experiment(experiment_id) diff --git a/qlib/workflow/record_temp.py b/qlib/workflow/record_temp.py index e45ef47b6..cf3a86f7f 100644 --- a/qlib/workflow/record_temp.py +++ b/qlib/workflow/record_temp.py @@ -11,6 +11,11 @@ from ..utils import init_instance_by_config, get_module_by_module_path class RecordTemp: + """ + This is the Records Template class that enables user to generate experiment results such as IC and + backtest in a certain format. + """ + def __init__(self, *args, **kwargs): pass @@ -24,10 +29,23 @@ class RecordTemp: Return ------ - The generated records. """ raise NotImplementedError(f"Please implement the `generate` method.") + def load(self, **kwargs): + """ + Load the stored records. + + Parameters + ---------- + kwargs + + Return + ------ + The stored records. + """ + raise NotImplementedError(f"Please implement the `load` method.") + def check(self, **kwargs): """ Check if the records is properly generated and saved. @@ -35,12 +53,20 @@ class RecordTemp: Parameters ---------- kwargs + + Return + ------ + Boolean: whether the records are stored properly. """ raise NotImplementedError(f"Please implement the `check` method.") # TODO: this can only be run under R's running experiment. class SignalRecord(RecordTemp): + """ + This is the Signal Record class that generates the signal prediction. + """ + def __init__(self, model, dataset, recorder, **kwargs): super(SignalRecord, self).__init__() self.model = model @@ -61,12 +87,16 @@ class SignalRecord(RecordTemp): raise Exception("Something went wrong when loading the saved object.") def check(self, **kwargs): - return self.recorder.check("pred.pkl") + artifacts = self.recorder.list_artifacts() + for artifact in artifacts: + if "pred.pkl" in artifact.path: + return True + return False # TODO class SigAnaRecord(SignalRecord): - def __init__(self, recorder, **kwargs): + def __init__(self, recorder, config, **kwargs): pass def generate(self): @@ -80,13 +110,16 @@ class SigAnaRecord(SignalRecord): class PortAnaRecord(SignalRecord): - def __init__(self, recorder, STRATEGY_CONFIG, BACKTEST_CONFIG, **kwargs): + """ + This is the Portfolio Analysis Record class that generates the results such as those of backtest. + """ + + def __init__(self, recorder, config, **kwargs): self.recorder = recorder - self.STRATEGY_CONFIG = STRATEGY_CONFIG - self.BACKTEST_CONFIG = BACKTEST_CONFIG - module = get_module_by_module_path("qlib.contrib.strategy") - self.strategy = init_instance_by_config(STRATEGY_CONFIG, module) - self.artifact_path = Path("portfolio_analysis").resolve() + self.strategy_config = config['strategy'] + self.backtest_config = config['backtest'] + self.strategy = init_instance_by_config(self.strategy_config) + self.artifact_path = "portfolio_analysis" def generate(self, **kwargs): """ @@ -121,4 +154,8 @@ class PortAnaRecord(SignalRecord): raise Exception("Something went wrong when loading the saved object.") def check(self): - return self.recorder.check("port_analysis.pkl", self.artifact_path) + artifacts = self.recorder.list_artifacts(self.artifact_path) + for artifact in artifacts: + if "port_analysis.pkl" in artifact.path: + return True + return False diff --git a/qlib/workflow/recorder.py b/qlib/workflow/recorder.py index 307a740b6..157e29347 100644 --- a/qlib/workflow/recorder.py +++ b/qlib/workflow/recorder.py @@ -11,19 +11,37 @@ class Recorder: """ This is the `Recorder` class for logging the experiments. The API is designed similar to mlflow. (The link: https://mlflow.org/docs/latest/python_api/mlflow.html) + + The status of the recorder can be SCHEDULED, RUNNING, FINISHED, FAILED. """ - def __init__(self, experiment_id): + def __init__(self, name, experiment_id): + self.id = None + self.name = name self.experiment_id = experiment_id - self.recorder_id = None - self.recorder_name = None + self.status = "SCHEDULED" + + def __repr__(self): + return str(self.info) + + def __str__(self): + return str(self.info) + + @property + def info(self): + output = dict() + output['class'] = "Recorder" + output['id'] = self.id + output['name'] = self.name + output['experiment_id'] = self.experiment_id + output['status'] = self.status def set_recorder_name(self, rname): self.recorder_name = rname - def save_object(self, data=None, name=None, local_path=None, artifact_path=None): + def save_objects(self, local_path=None, artifact_path=None, **kwargs): """ - Save object such as prediction file or model checkpoints to the artifact URI. + Save objects such as prediction file or model checkpoints to the artifact URI. Parameters ---------- @@ -31,19 +49,6 @@ class Recorder: the data to be saved. name : str name of the file to be saved. - local_path : str - if provided, them save the file or directory to the artifact URI. - artifact_path=None : str - the relative path for the artifact to be stored in the URI. - """ - raise NotImplementedError(f"Please implement the `save_object` method.") - - def save_objects(self, data_name_list=None, local_path=None, artifact_path=None): - """ - Save objects such as prediction file or model checkpoints to the artifact URI. - - Parameters - ---------- data_name_list : list list of (data, name) pairs local_path : str @@ -68,21 +73,13 @@ class Recorder: """ raise NotImplementedError(f"Please implement the `load_object` method.") - def start_run(self, run_id=None, experiment_id=None, run_name=None, nested=False): + def start_run(self): """ - Start running the Recorder. The return value can be used as a context manager within a `with` block; + Start running or resuming the Recorder. The return value can be used as a context manager within a `with` block; otherwise, you must call end_run() to terminate the current run. (See `ActiveRun` class in mlflow) Parameters ---------- - run_id : str - id of the active Recorder. - experiment_id : str - id of the active experiment. - run_name : str - name of the Recorder. - nested : boolean - controls whether run is nested in parent run. Returns ------- @@ -127,18 +124,33 @@ class Recorder: keyword arguments key, value pair to be logged as tags. """ - raise NotImplementedError(f"Please implement the `log_tags` method.") + raise NotImplementedError(f"Please implement the `set_tags` method.") - def delete_tag(self, key): + def delete_tags(self, *keys): """ - Delete a tag from a run. + Delete some tags from a run. Parameters ---------- - key : str - the name of the tag to be deleted. + keys : series of strs of the keys + all the name of the tag to be deleted. """ - raise NotImplementedError(f"Please implement the `delete_tag` method.") + raise NotImplementedError(f"Please implement the `delete_tags` method.") + + def list_artifacts(self, artifact_path=None): + """ + Delete some tags from a run. + + Parameters + ---------- + artifact_path=None : str + the relative path for the artifact to be stored in the URI. + + Returns + ------- + A list of artifacts information (name, path, etc.) that being stored. + """ + raise NotImplementedError(f"Please implement the `list_artifacts` method.") class MLflowRecorder(Recorder): @@ -149,51 +161,43 @@ class MLflowRecorder(Recorder): use file manager to help maintain the objects in the project. """ - def __init__(self, experiment_id): - super(MLflowRecorder, self).__init__(experiment_id) + def __init__(self, name, experiment_id): + super(MLflowRecorder, self).__init__(name, experiment_id) self.fm = None self.temp_dir = None - def start_run(self, run_id=None, experiment_id=None, run_name=None, nested=False): - if run_id is None: - run_id = self.recorder_id - if experiment_id is None: - experiment_id = self.experiment_id - if run_name is None: - run_name = self.recorder_name + def start_run(self): # start the run - run = mlflow.start_run(run_id, experiment_id, run_name, nested) + run = mlflow.start_run(self.id, self.experiment_id, self.name) # save the run id and artifact_uri - self.recorder_id = run.info.run_id + self.id = run.info.run_id self.artifact_uri = run.info.artifact_uri self._uri = mlflow.get_tracking_uri() # Fix!!! : this is not proper to have uri in recorder # set up file manager for saving objects self.temp_dir = tempfile.mkdtemp() self.fm = FileManager(Path(self.temp_dir).absolute()) + self.status = "RUNNING" return run - def end_run(self): - mlflow.end_run() + def end_run(self, status): + mlflow.end_run(status) + self.status = status shutil.rmtree(self.temp_dir) - def save_object(self, data=None, name=None, local_path=None, artifact_path=None): + def save_objects(self, data_name_list=None, local_path=None, artifact_path=None, **kwargs): client = mlflow.tracking.MlflowClient(tracking_uri=self._uri) - if local_path is None: - assert data is not None and name is not None, "Please provide data and name input." + if local_path is not None: + client.log_artifacts(self.id, local_path, artifact_path) + elif kwargs.get('data') is not None and kwargs.get('name') is not None: + data, name = kwargs.get('data'), kwargs.get('name') self.fm.save_obj(data, name) - client.log_artifact(self.recorder_id, self.fm.path / name, artifact_path) - else: - assert local_path is not None, "Please provide a valid local path for the " - client.log_artifact(self.recorder_id, local_path, artifact_path) - - def save_objects(self, data_name_list=None, local_path=None, artifact_path=None): - client = mlflow.tracking.MlflowClient(tracking_uri=self._uri) - if local_path is None: - assert data_name_list is not None, "Please provide data_name_list input." + client.log_artifact(self.id, self.fm.path / name, artifact_path) + elif kwargs.get('data_name_list') is not None: + data_name_list = kwargs.get('data_name_list') self.fm.save_objs(data_name_list) - client.log_artifacts(self.recorder_id, self.fm.path, artifact_path) + client.log_artifacts(self.id, self.fm.path, artifact_path) else: - client.log_artifacts(self.recorder_id, local_path, artifact_path) + raise Exception('Please provide valid arguments in order to save object properly.') def load_object(self, name): client = mlflow.tracking.MlflowClient(tracking_uri=self._uri) @@ -227,18 +231,16 @@ class MLflowRecorder(Recorder): else: mlflow.set_tags(dict(kwargs)) - def delete_tag(self, key): - mlflow.delete_tag(key) + def delete_tags(self, *keys): + for count, key in enumerate(keys): + mlflow.delete_tag(key) def get_artifact_uri(self, artifact_path=None): if self.artifact_uri is not None: return self.artifact_uri return mlflow.get_artifact_uri(artifact_path) - def check(self, name, path=None): + def list_artifacts(self, artifact_path=None): client = mlflow.tracking.MlflowClient(tracking_uri=self._uri) - artifacts = client.list_artifacts(self.recorder_id, path) - for artifact in artifacts: - if name in artifact.path: - return True - return False + artifacts = client.list_artifacts(self.id, path) + return artifacts From 633af182f49336296d55045fb89d3f9a3c247d71 Mon Sep 17 00:00:00 2001 From: Jactus Date: Mon, 9 Nov 2020 22:15:41 +0800 Subject: [PATCH 027/241] Fix read_bin bug --- qlib/utils/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qlib/utils/__init__.py b/qlib/utils/__init__.py index d0b47d3cd..900d9e115 100644 --- a/qlib/utils/__init__.py +++ b/qlib/utils/__init__.py @@ -44,7 +44,7 @@ def read_bin(file_path, start_index, end_index): ref_start_index = int(np.frombuffer(f.read(4), dtype=" end_index: - return pd.Series(np.float32) + return pd.Series(dtype=np.float32) # calculate offset f.seek(4 * (si - ref_start_index) + 4) # read nbytes From b1c6b216fe3aa6bb0fe5052a32cac06ad0ceb49c Mon Sep 17 00:00:00 2001 From: Young Date: Mon, 9 Nov 2020 15:17:08 +0000 Subject: [PATCH 028/241] fix Recorder init bug --- qlib/__init__.py | 4 +--- qlib/data/dataset/processor.py | 2 ++ 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/qlib/__init__.py b/qlib/__init__.py index 83c36dbd3..aef070d37 100644 --- a/qlib/__init__.py +++ b/qlib/__init__.py @@ -82,12 +82,10 @@ def init(default_conf="client", **kwargs): if "flask_server" in C: LOG.info(f"flask_server={C['flask_server']}, flask_port={C['flask_port']}") - # set up QlibRecorder - uri = C["exp_uri"] # exp manager module module = get_module_by_module_path("qlib.workflow.expm") exp_manager = init_instance_by_config(C["exp_manager"], module) - qr = QlibRecorder(exp_manager, uri) + qr = QlibRecorder(exp_manager) R.register(qr) diff --git a/qlib/data/dataset/processor.py b/qlib/data/dataset/processor.py index b52426d16..ead8707db 100644 --- a/qlib/data/dataset/processor.py +++ b/qlib/data/dataset/processor.py @@ -109,6 +109,7 @@ class ProcessInf(Processor): class MinMaxNorm(Processor): def __init__(self, fit_start_time, fit_end_time, fields_group=None): + # FIXME: time is not used self.fit_start_time = fit_start_time self.fit_end_time = fit_end_time self.fields_group = fields_group @@ -135,6 +136,7 @@ class MinMaxNorm(Processor): class ZscoreNorm(Processor): def __init__(self, fit_start_time, fit_end_time, fields_group=None): + # FIXME: time is not used self.fit_start_time = fit_start_time self.fit_end_time = fit_end_time self.fields_group = fields_group From 004e2f557b3711289bcab953e5e4a413b6332fb0 Mon Sep 17 00:00:00 2001 From: Jactus Date: Tue, 10 Nov 2020 00:32:18 +0800 Subject: [PATCH 029/241] Fix recorder related bugs --- qlib/__init__.py | 4 +--- qlib/workflow/__init__.py | 16 ++++++++-------- qlib/workflow/exp.py | 2 ++ qlib/workflow/expm.py | 8 +++++--- qlib/workflow/record_temp.py | 10 +++++----- qlib/workflow/recorder.py | 4 ++-- 6 files changed, 23 insertions(+), 21 deletions(-) diff --git a/qlib/__init__.py b/qlib/__init__.py index 83c36dbd3..b26ac986d 100644 --- a/qlib/__init__.py +++ b/qlib/__init__.py @@ -83,11 +83,9 @@ def init(default_conf="client", **kwargs): LOG.info(f"flask_server={C['flask_server']}, flask_port={C['flask_port']}") # set up QlibRecorder - uri = C["exp_uri"] - # exp manager module module = get_module_by_module_path("qlib.workflow.expm") exp_manager = init_instance_by_config(C["exp_manager"], module) - qr = QlibRecorder(exp_manager, uri) + qr = QlibRecorder(exp_manager) R.register(qr) diff --git a/qlib/workflow/__init__.py b/qlib/workflow/__init__.py index a941ed7cf..5ac673a30 100644 --- a/qlib/workflow/__init__.py +++ b/qlib/workflow/__init__.py @@ -43,26 +43,26 @@ class QlibRecorder: def get_uri(self): return self.exp_manager.get_uri() - def get_recorder(self): - return self.exp_manager.active_recorder + def get_recorder(self, recorder_id=None, recorder_name=None): + return self.exp_manager.active_experiment.get_recorder(recorder_id, recorder_name) def save_objects(self, local_path=None, artifact_path=None, **kwargs): - self.exp_manager.active_recorder.save_objects(local_path, artifact_path, **kwargs) + self.exp_manager.active_experiment.active_recorder.save_objects(local_path, artifact_path, **kwargs) def load_object(self, name): - return self.exp_manager.active_recorder.load_object(name) + return self.exp_manager.active_experiment.active_recorder.load_object(name) def log_params(self, **kwargs): - self.exp_manager.active_recorder.log_params(**kwargs) + self.exp_manager.active_experiment.active_recorder.log_params(**kwargs) def log_metrics(self, step=None, **kwargs): - self.exp_manager.active_recorder.log_metrics(step, **kwargs) + self.exp_manager.active_experiment.active_recorder.log_metrics(step, **kwargs) def set_tags(self, **kwargs): - self.exp_manager.active_recorder.set_tags(**kwargs) + self.exp_manager.active_experiment.active_recorder.set_tags(**kwargs) def delete_tag(self, *key): - self.exp_manager.active_recorder.delete_tag(*key) + self.exp_manager.active_experiment.active_recorder.delete_tag(*key) # global record diff --git a/qlib/workflow/exp.py b/qlib/workflow/exp.py index 86163c0ea..e4ef6d8a6 100644 --- a/qlib/workflow/exp.py +++ b/qlib/workflow/exp.py @@ -4,7 +4,9 @@ import mlflow from pathlib import Path from .recorder import MLflowRecorder +from ..log import get_module_logger +logger = get_module_logger("workflow", "INFO") class Experiment: """ diff --git a/qlib/workflow/expm.py b/qlib/workflow/expm.py index e81da0fcb..2afdee279 100644 --- a/qlib/workflow/expm.py +++ b/qlib/workflow/expm.py @@ -184,10 +184,12 @@ class MLflowExpManager(ExpManager): else: if experiment_name not in self.experiments: if mlflow.get_experiment_by_name(experiment_name) is not None: - raise Exception( - "The experiment has already been created before. Please pick another name or delete the files under uri." + logger.info( + "The experiment has already been created before. Try to resume the experiment..." ) - experiment_id = mlflow.create_experiment(experiment_name) + experiment_id = mlflow.get_experiment_by_name(experiment_name).experiment_id + else: + experiment_id = mlflow.create_experiment(experiment_name) else: experiment_id = self.experiments[experiment_name].id experiment = self.experiments[experiment_name] diff --git a/qlib/workflow/record_temp.py b/qlib/workflow/record_temp.py index cf3a86f7f..d92f836a8 100644 --- a/qlib/workflow/record_temp.py +++ b/qlib/workflow/record_temp.py @@ -76,7 +76,7 @@ class SignalRecord(RecordTemp): def generate(self, **kwargs): # generate prediciton pred = self.model.predict(self.dataset) - self.recorder.save_object(pred, "pred.pkl") + self.recorder.save_objects(data=pred, name="pred.pkl") def load(self): # try to load the saved object @@ -132,9 +132,9 @@ class PortAnaRecord(SignalRecord): assert super().check(), "Make sure the parent process is completed and store the data properly." # custom strategy and get backtest pred_score = super().load() - report_normal, positions_normal = normal_backtest(pred_score, strategy=self.strategy, **self.BACKTEST_CONFIG) - self.recorder.save_object(report_normal, "report_normal.pkl", self.artifact_path) - self.recorder.save_object(positions_normal, "positions_normal.pkl", self.artifact_path) + report_normal, positions_normal = normal_backtest(pred_score, strategy=self.strategy, **self.backtest_config) + self.recorder.save_objects(data=report_normal, name="report_normal.pkl", artifact_path=self.artifact_path) + self.recorder.save_objects(data=positions_normal, name="positions_normal.pkl", artifact_path=self.artifact_path) # analysis analysis = dict() @@ -143,7 +143,7 @@ class PortAnaRecord(SignalRecord): report_normal["return"] - report_normal["bench"] - report_normal["cost"] ) analysis_df = pd.concat(analysis) # type: pd.DataFrame - self.recorder.save_object(pred, "port_analysis.pkl", self.artifact_path) + self.recorder.save_objects(data=analysis_df, name="port_analysis.pkl", artifact_path=self.artifact_path) def load(self): # try to load the saved object diff --git a/qlib/workflow/recorder.py b/qlib/workflow/recorder.py index 157e29347..89b16e9f1 100644 --- a/qlib/workflow/recorder.py +++ b/qlib/workflow/recorder.py @@ -201,7 +201,7 @@ class MLflowRecorder(Recorder): def load_object(self, name): client = mlflow.tracking.MlflowClient(tracking_uri=self._uri) - path = client.download_artifacts(self.recorder_id, name) + path = client.download_artifacts(self.id, name) try: with Path(path).open("rb") as f: f.seek(0) @@ -242,5 +242,5 @@ class MLflowRecorder(Recorder): def list_artifacts(self, artifact_path=None): client = mlflow.tracking.MlflowClient(tracking_uri=self._uri) - artifacts = client.list_artifacts(self.id, path) + artifacts = client.list_artifacts(self.id, artifact_path) return artifacts From 21a24f989385912f6f41a0424c74360acbac891f Mon Sep 17 00:00:00 2001 From: Young Date: Tue, 10 Nov 2020 01:45:34 +0000 Subject: [PATCH 030/241] fix data_loader dict type bug --- qlib/data/dataset/handler.py | 8 +++++--- qlib/utils/__init__.py | 18 ++++++++++-------- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/qlib/data/dataset/handler.py b/qlib/data/dataset/handler.py index 6ead332d2..78d19d005 100644 --- a/qlib/data/dataset/handler.py +++ b/qlib/data/dataset/handler.py @@ -62,9 +62,11 @@ class DataHandler(Serializable): # Setup data loader assert data_loader is not None # to make start_time end_time could have None default value - self.data_loader = init_instance_by_config(data_loader, - None if 'module_path' in data_loader else data_loader_module, - accept_types=DataLoader) + + self.data_loader = init_instance_by_config( + data_loader, + None if (isinstance(data_loader, dict) and 'module_path' in data_loader) else data_loader_module, + accept_types=DataLoader) self.instruments = instruments self.start_time = start_time diff --git a/qlib/utils/__init__.py b/qlib/utils/__init__.py index 900d9e115..8d16798d6 100644 --- a/qlib/utils/__init__.py +++ b/qlib/utils/__init__.py @@ -195,22 +195,24 @@ def get_cls_kwargs(config: Union[dict, str], module) -> (type, dict): def init_instance_by_config( - config: Union[str, dict], module=None, accept_types: Union[type, Tuple[type]] = tuple([]), **kwargs + config: Union[str, dict, object], module=None, accept_types: Union[type, Tuple[type]] = tuple([]), **kwargs ) -> object: """ get initialized instance with config Parameters ---------- - config : Union[str, dict] + config : Union[str, dict, object] dict example. - { - 'class': 'ClassName', - 'kwargs': dict, # It is optional. {} will be used if not given - 'model_path': path, # It is optional if module is given - } + { + 'class': 'ClassName', + 'kwargs': dict, # It is optional. {} will be used if not given + 'model_path': path, # It is optional if module is given + } str example. - "ClassName": getattr(module, config)() will be used. + "ClassName": getattr(module, config)() will be used. + object example: + instance of accept_types module : Python module Optional. It should be a python module. NOTE: the "module_path" will be override by `module` arguments From 1a8ef55dc747fffa56e64e7f3e6f17bdfbbc946b Mon Sep 17 00:00:00 2001 From: Dong Zhou Date: Tue, 10 Nov 2020 09:47:59 +0800 Subject: [PATCH 031/241] fix riskmodel --- qlib/model/riskmodel.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/qlib/model/riskmodel.py b/qlib/model/riskmodel.py index 42c2b710f..b5275213b 100644 --- a/qlib/model/riskmodel.py +++ b/qlib/model/riskmodel.py @@ -235,6 +235,7 @@ class ShrinkCovEstimator(RiskModel): return self._get_shrink_target_const_corr(X, S) if self.target == self.TGT_SINGLE_FACTOR: return self._get_shrink_target_single_factor(X, S) + return self.target def _get_shrink_target_const_var(self, X: np.ndarray, S: np.ndarray) -> np.ndarray: """get shrinking target with constant variance @@ -275,7 +276,7 @@ class ShrinkCovEstimator(RiskModel): """get shrinking parameter `alpha` Note: - The Ledoit-Wolf shrinking parameter estimator consists of three different + The Ledoit-Wolf shrinking parameter estimator consists of three different methods. """ if self.alpha == self.SHR_OAS: return self._get_shrink_param_oas(X, S, F) From 722655ad1377bf052be0eee25a9f8d5f1388b791 Mon Sep 17 00:00:00 2001 From: Jactus Date: Wed, 11 Nov 2020 09:34:10 +0800 Subject: [PATCH 032/241] Update black formatter --- .github/workflows/test.yml | 2 +- examples/workflow_by_code.py | 34 ++++++----- qlib/contrib/data/handler.py | 1 - qlib/contrib/model/catboost_model.py | 88 +++++++++++++--------------- qlib/contrib/model/pytorch_nn.py | 4 +- qlib/data/dataset/handler.py | 5 +- qlib/data/dataset/loader.py | 6 +- qlib/utils/__init__.py | 2 +- qlib/utils/paral.py | 10 ++-- qlib/workflow/__init__.py | 1 + qlib/workflow/exp.py | 28 ++++----- qlib/workflow/expm.py | 10 +--- qlib/workflow/record_temp.py | 6 +- qlib/workflow/recorder.py | 26 ++++---- setup.py | 2 +- 15 files changed, 111 insertions(+), 114 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 547991159..d5db7940a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -43,7 +43,7 @@ jobs: - name: Lint with Black run: | cd .. - python -m black qlib -l 120 + python -m black qlib -l 120 --check - name: Unit tests with Pytest run: | diff --git a/examples/workflow_by_code.py b/examples/workflow_by_code.py index bc5f9337e..a959d6ea1 100644 --- a/examples/workflow_by_code.py +++ b/examples/workflow_by_code.py @@ -22,7 +22,6 @@ from qlib.utils import init_instance_by_config if __name__ == "__main__": - # use default data provider_uri = "~/.qlib/qlib_data/cn_data" # target_dir if not exists_qlib_data(provider_uri): @@ -37,15 +36,14 @@ if __name__ == "__main__": MARKET = "csi300" BENCHMARK = "SH000300" - ################################### # train model ################################### DATA_HANDLER_CONFIG = { "start_time": "2008-01-01", "end_time": "2020-08-01", - "fit_start_time":"2008-01-01", - "fit_end_time":"2014-12-31", + "fit_start_time": "2008-01-01", + "fit_end_time": "2014-12-31", "instruments": MARKET, } @@ -72,31 +70,37 @@ if __name__ == "__main__": "max_depth": 8, "num_leaves": 210, "num_threads": 20, - } + }, }, "dataset": { "class": "DatasetH", "module_path": "qlib.data.dataset", "kwargs": { - 'handler': { + "handler": { "class": "Alpha158", "module_path": "qlib.contrib.data.handler", - "kwargs": DATA_HANDLER_CONFIG + "kwargs": DATA_HANDLER_CONFIG, }, - 'segments': { - 'train': ("2008-01-01", "2014-12-31"), - 'valid': ("2015-01-01", "2016-12-31",), - 'test': ("2017-01-01", "2020-08-01",), - } - } + "segments": { + "train": ("2008-01-01", "2014-12-31"), + "valid": ( + "2015-01-01", + "2016-12-31", + ), + "test": ( + "2017-01-01", + "2020-08-01", + ), + }, + }, } # You shoud record the data in specific sequence # "record": ['SignalRecord', 'SigAnaRecord', 'PortAnaRecord'], } # model = train_model(task) - model = init_instance_by_config(task['model']) - dataset = init_instance_by_config(task['dataset']) + model = init_instance_by_config(task["model"]) + dataset = init_instance_by_config(task["dataset"]) model.fit(dataset) diff --git a/qlib/contrib/data/handler.py b/qlib/contrib/data/handler.py index bd07fff7e..b2fd0515d 100644 --- a/qlib/contrib/data/handler.py +++ b/qlib/contrib/data/handler.py @@ -281,7 +281,6 @@ class Alpha158(DataHandlerLP): class Alpha158vwap(Alpha158): - def get_feature_config(self): conf = { "kbar": {}, diff --git a/qlib/contrib/model/catboost_model.py b/qlib/contrib/model/catboost_model.py index 8a55d0385..d53a6db41 100644 --- a/qlib/contrib/model/catboost_model.py +++ b/qlib/contrib/model/catboost_model.py @@ -9,17 +9,17 @@ from ...data.dataset.handler import DataHandlerLP class CatBoostModel(Model): - """CatBoost Model""" + """CatBoost Model""" - def __init__(self, loss="RMSE", **kwargs): - # There are more options - if loss not in {"RMSE", "Logloss"}: - raise NotImplementedError - self._params = {"loss_function": loss} - self._params.update(kwargs) - self.model = None + def __init__(self, loss="RMSE", **kwargs): + # There are more options + if loss not in {"RMSE", "Logloss"}: + raise NotImplementedError + self._params = {"loss_function": loss} + self._params.update(kwargs) + self.model = None - def fit( + def fit( self, dataset: DatasetH, num_boost_round=1000, @@ -27,48 +27,42 @@ class CatBoostModel(Model): verbose_eval=20, evals_result=dict(), **kwargs - ): - df_train, df_valid = dataset.prepare( - ["train", "valid"], col_set=["feature", "label"], data_key=DataHandlerLP.DK_L + ): + df_train, df_valid = dataset.prepare( + ["train", "valid"], col_set=["feature", "label"], data_key=DataHandlerLP.DK_L ) - x_train, y_train = df_train["feature"], df_train["label"] - x_valid, y_valid = df_valid["feature"], df_valid["label"] + x_train, y_train = df_train["feature"], df_train["label"] + x_valid, y_valid = df_valid["feature"], df_valid["label"] - # CatBoost needs 1D array as its label - if y_train.values.ndim == 2 and y_train.values.shape[1] == 1: - y_train_1d, y_valid_1d = np.squeeze(y_train.values), np.squeeze(y_valid.values) - else: - raise ValueError("CatBoost doesn't support multi-label training") + # CatBoost needs 1D array as its label + if y_train.values.ndim == 2 and y_train.values.shape[1] == 1: + y_train_1d, y_valid_1d = np.squeeze(y_train.values), np.squeeze(y_valid.values) + else: + raise ValueError("CatBoost doesn't support multi-label training") - train_pool = Pool(data = x_train, label = y_train_1d) - valid_pool = Pool(data = x_valid, label = y_valid_1d) + train_pool = Pool(data=x_train, label=y_train_1d) + valid_pool = Pool(data=x_valid, label=y_valid_1d) - #Initialize the catboost model - self._params['iterations'] = num_boost_round - self._params['early_stopping_rounds'] = early_stopping_rounds - self._params['verbose_eval'] = verbose_eval - self._params['task_type'] = "GPU" if get_gpu_device_count() > 0 else "CPU" - self.model = CatBoost(self._params, **kwargs) + # Initialize the catboost model + self._params["iterations"] = num_boost_round + self._params["early_stopping_rounds"] = early_stopping_rounds + self._params["verbose_eval"] = verbose_eval + self._params["task_type"] = "GPU" if get_gpu_device_count() > 0 else "CPU" + self.model = CatBoost(self._params, **kwargs) - #train the model - self.model.fit( - train_pool, - eval_set = valid_pool, - use_best_model = True, - **kwargs - ) - - evals_result = self.model.get_evals_result() - evals_result["train"] = list(evals_result["learn"].values())[0] - evals_result["valid"] = list(evals_result["validation"].values())[0] + # train the model + self.model.fit(train_pool, eval_set=valid_pool, use_best_model=True, **kwargs) + + evals_result = self.model.get_evals_result() + evals_result["train"] = list(evals_result["learn"].values())[0] + evals_result["valid"] = list(evals_result["validation"].values())[0] + + def predict(self, dataset): + if self.model is None: + raise ValueError("model is not fitted yet!") + x_test = dataset.prepare("test", col_set="feature") + return pd.Series(self.model.predict(np.squeeze(x_test.values)), index=x_test.index) - def predict(self, dataset): - if self.model is None: - raise ValueError("model is not fitted yet!") - x_test = dataset.prepare("test", col_set="feature") - return pd.Series(self.model.predict(np.squeeze(x_test.values)), index=x_test.index) - - -if __name__ == '__main__': - cat = CatBoostModel() \ No newline at end of file +if __name__ == "__main__": + cat = CatBoostModel() diff --git a/qlib/contrib/model/pytorch_nn.py b/qlib/contrib/model/pytorch_nn.py index 1acb5c843..1835fb617 100644 --- a/qlib/contrib/model/pytorch_nn.py +++ b/qlib/contrib/model/pytorch_nn.py @@ -159,9 +159,7 @@ class DNNModelPytorch(Model): x_valid, y_valid = df_valid["feature"], df_valid["label"] try: - wdf_train, wdf_valid = dataset.prepare( - ["train", "valid"], col_set=["weight"], data_key=DataHandlerLP.DK_L - ) + wdf_train, wdf_valid = dataset.prepare(["train", "valid"], col_set=["weight"], data_key=DataHandlerLP.DK_L) w_train, w_valid = wdf_train["weight"], wdf_valid["weight"] except: w_train = pd.DataFrame(np.ones_like(y_train.values), index=y_train.index) diff --git a/qlib/data/dataset/handler.py b/qlib/data/dataset/handler.py index 78d19d005..13d3465c7 100644 --- a/qlib/data/dataset/handler.py +++ b/qlib/data/dataset/handler.py @@ -65,8 +65,9 @@ class DataHandler(Serializable): self.data_loader = init_instance_by_config( data_loader, - None if (isinstance(data_loader, dict) and 'module_path' in data_loader) else data_loader_module, - accept_types=DataLoader) + None if (isinstance(data_loader, dict) and "module_path" in data_loader) else data_loader_module, + accept_types=DataLoader, + ) self.instruments = instruments self.start_time = start_time diff --git a/qlib/data/dataset/loader.py b/qlib/data/dataset/loader.py index 0d1e7be2e..564a7e5d5 100644 --- a/qlib/data/dataset/loader.py +++ b/qlib/data/dataset/loader.py @@ -14,6 +14,7 @@ class DataLoader(abc.ABC): """ DataLoader is designed for loading raw data from original data source. """ + @abc.abstractmethod def load(self, instruments, start_time=None, end_time=None) -> pd.DataFrame: """ @@ -53,6 +54,7 @@ class DLWParser(DataLoader): Extracting this class so that QlibDataLoader and other dataloaders(such as QdbDataLoader) can share the fields """ + def __init__(self, config: Tuple[list, tuple, dict]): """ Parameters @@ -113,7 +115,8 @@ class DLWParser(DataLoader): grp: self.load_group_df(instruments, exprs, names, start_time, end_time) for grp, (exprs, names) in self.fields.items() }, - axis=1) + axis=1, + ) else: exprs, names = self.fields df = self.load_group_df(instruments, exprs, names, start_time, end_time) @@ -122,6 +125,7 @@ class DLWParser(DataLoader): class QlibDataLoader(DLWParser): """Same as QlibDataLoader. The fields can be define by config""" + def __init__(self, config: Tuple[list, tuple, dict], filter_pipe=None): """ Parameters diff --git a/qlib/utils/__init__.py b/qlib/utils/__init__.py index 8d16798d6..d9ae98bd5 100644 --- a/qlib/utils/__init__.py +++ b/qlib/utils/__init__.py @@ -195,7 +195,7 @@ def get_cls_kwargs(config: Union[dict, str], module) -> (type, dict): def init_instance_by_config( - config: Union[str, dict, object], module=None, accept_types: Union[type, Tuple[type]] = tuple([]), **kwargs + config: Union[str, dict, object], module=None, accept_types: Union[type, Tuple[type]] = tuple([]), **kwargs ) -> object: """ get initialized instance with config diff --git a/qlib/utils/paral.py b/qlib/utils/paral.py index c709047b9..a640b04ea 100644 --- a/qlib/utils/paral.py +++ b/qlib/utils/paral.py @@ -5,8 +5,8 @@ from joblib import Parallel, delayed import pandas as pd -def datetime_groupby_apply(df, apply_func, axis=0, level='datetime', resample_rule="M", n_jobs=-1, skip_group=False): - """ datetime_groupby_apply +def datetime_groupby_apply(df, apply_func, axis=0, level="datetime", resample_rule="M", n_jobs=-1, skip_group=False): + """datetime_groupby_apply This function will apply the `apply_func` on the datetime level index. Parameters @@ -26,12 +26,14 @@ def datetime_groupby_apply(df, apply_func, axis=0, level='datetime', resample_ru Returns: pd.DataFrame """ + def _naive_group_apply(df): return df.groupby(axis=axis, level=level).apply(apply_func) if n_jobs != 1: - dfs = Parallel(n_jobs=n_jobs)(delayed(_naive_group_apply)(sub_df) - for idx, sub_df in df.resample(resample_rule, axis=axis, level=level)) + dfs = Parallel(n_jobs=n_jobs)( + delayed(_naive_group_apply)(sub_df) for idx, sub_df in df.resample(resample_rule, axis=axis, level=level) + ) return pd.concat(dfs, axis=axis).sort_index() else: return _naive_group_apply(df) diff --git a/qlib/workflow/__init__.py b/qlib/workflow/__init__.py index 5ac673a30..b801da880 100644 --- a/qlib/workflow/__init__.py +++ b/qlib/workflow/__init__.py @@ -6,6 +6,7 @@ from .expm import MLflowExpManager from ..utils import Wrapper from ..config import C + class QlibRecorder: """ A global system that helps to manage the experiments. diff --git a/qlib/workflow/exp.py b/qlib/workflow/exp.py index e4ef6d8a6..432497fda 100644 --- a/qlib/workflow/exp.py +++ b/qlib/workflow/exp.py @@ -8,6 +8,7 @@ from ..log import get_module_logger logger = get_module_logger("workflow", "INFO") + class Experiment: """ Thie is the `Experiment` class for each experiment being run. The API is designed @@ -17,22 +18,22 @@ class Experiment: self.name = None self.id = None self.active_recorder = None # only one recorder can running each time - self.recorders = dict() # recorder id -> object + self.recorders = dict() # recorder id -> object def __repr__(self): return str(self.info) - + def __str__(self): - return str(self.info) + return str(self.info) @property def info(self): output = dict() - output['class'] = "Experiment" - output['id'] = self.id - output['name'] = self.name - output['active_recorder'] = self.active_recorder.id - output['recorders'] = list(self.recorders.keys()) + output["class"] = "Experiment" + output["id"] = self.id + output["name"] = self.name + output["active_recorder"] = self.active_recorder.id + output["recorders"] = list(self.recorders.keys()) def start(self): """ @@ -137,7 +138,6 @@ class MLflowExperiment(Experiment): run = self.active_recorder.start_run() # store the recorder self.recorders[self.active_recorder.id] = recorder - return self.active_recorder def end(self, status): @@ -147,7 +147,7 @@ class MLflowExperiment(Experiment): def create_recorder(self): num = len(self.recorders) - name = "Recorder_{}".format(num+1) + name = "Recorder_{}".format(num + 1) recorder = MLflowRecorder(name, self.id) return recorder @@ -170,9 +170,7 @@ class MLflowExperiment(Experiment): if self.recorders[rid].name == recorder_name: return self.recorders[rid] elif self.active_recorder is None: - raise Exception('No valid active recorder exists. Please make sure the experiment is running.') + raise Exception("No valid active recorder exists. Please make sure the experiment is running.") else: - logger.info( - "No experiment id or name is given. Return the current active experiment." - ) - return self.active_recorder \ No newline at end of file + logger.info("No experiment id or name is given. Return the current active experiment.") + return self.active_recorder diff --git a/qlib/workflow/expm.py b/qlib/workflow/expm.py index 2afdee279..f597d4a96 100644 --- a/qlib/workflow/expm.py +++ b/qlib/workflow/expm.py @@ -184,9 +184,7 @@ class MLflowExpManager(ExpManager): else: if experiment_name not in self.experiments: if mlflow.get_experiment_by_name(experiment_name) is not None: - logger.info( - "The experiment has already been created before. Try to resume the experiment..." - ) + logger.info("The experiment has already been created before. Try to resume the experiment...") experiment_id = mlflow.get_experiment_by_name(experiment_name).experiment_id else: experiment_id = mlflow.create_experiment(experiment_name) @@ -216,11 +214,9 @@ class MLflowExpManager(ExpManager): if self.experiments[name].id == experiment_id: return self.experiments[name] elif self.active_experiment is None: - raise Exception('No valid active experiment exists. Please make sure experiment manager is running.') + raise Exception("No valid active experiment exists. Please make sure experiment manager is running.") else: - logger.info( - "No experiment id or name is given. Return the current active experiment." - ) + logger.info("No experiment id or name is given. Return the current active experiment.") return self.active_experiment def delete_exp(self, experiment_id): diff --git a/qlib/workflow/record_temp.py b/qlib/workflow/record_temp.py index d92f836a8..3cef8b5d3 100644 --- a/qlib/workflow/record_temp.py +++ b/qlib/workflow/record_temp.py @@ -12,7 +12,7 @@ from ..utils import init_instance_by_config, get_module_by_module_path class RecordTemp: """ - This is the Records Template class that enables user to generate experiment results such as IC and + This is the Records Template class that enables user to generate experiment results such as IC and backtest in a certain format. """ @@ -116,8 +116,8 @@ class PortAnaRecord(SignalRecord): def __init__(self, recorder, config, **kwargs): self.recorder = recorder - self.strategy_config = config['strategy'] - self.backtest_config = config['backtest'] + self.strategy_config = config["strategy"] + self.backtest_config = config["backtest"] self.strategy = init_instance_by_config(self.strategy_config) self.artifact_path = "portfolio_analysis" diff --git a/qlib/workflow/recorder.py b/qlib/workflow/recorder.py index 89b16e9f1..68ce5432b 100644 --- a/qlib/workflow/recorder.py +++ b/qlib/workflow/recorder.py @@ -20,21 +20,21 @@ class Recorder: self.name = name self.experiment_id = experiment_id self.status = "SCHEDULED" - + def __repr__(self): return str(self.info) - + def __str__(self): - return str(self.info) + return str(self.info) @property def info(self): output = dict() - output['class'] = "Recorder" - output['id'] = self.id - output['name'] = self.name - output['experiment_id'] = self.experiment_id - output['status'] = self.status + output["class"] = "Recorder" + output["id"] = self.id + output["name"] = self.name + output["experiment_id"] = self.experiment_id + output["status"] = self.status def set_recorder_name(self, rname): self.recorder_name = rname @@ -188,16 +188,16 @@ class MLflowRecorder(Recorder): client = mlflow.tracking.MlflowClient(tracking_uri=self._uri) if local_path is not None: client.log_artifacts(self.id, local_path, artifact_path) - elif kwargs.get('data') is not None and kwargs.get('name') is not None: - data, name = kwargs.get('data'), kwargs.get('name') + elif kwargs.get("data") is not None and kwargs.get("name") is not None: + data, name = kwargs.get("data"), kwargs.get("name") self.fm.save_obj(data, name) client.log_artifact(self.id, self.fm.path / name, artifact_path) - elif kwargs.get('data_name_list') is not None: - data_name_list = kwargs.get('data_name_list') + elif kwargs.get("data_name_list") is not None: + data_name_list = kwargs.get("data_name_list") self.fm.save_objs(data_name_list) client.log_artifacts(self.id, self.fm.path, artifact_path) else: - raise Exception('Please provide valid arguments in order to save object properly.') + raise Exception("Please provide valid arguments in order to save object properly.") def load_object(self, name): client = mlflow.tracking.MlflowClient(tracking_uri=self._uri) diff --git a/setup.py b/setup.py index 7e1bc1583..22e806d8d 100644 --- a/setup.py +++ b/setup.py @@ -55,7 +55,7 @@ REQUIRED = [ "loguru", "lightgbm", "tornado", - "joblib>=0.17.0" + "joblib>=0.17.0", ] # Numpy include From b839733ec786d302a1df4e961a3b78e2f391c227 Mon Sep 17 00:00:00 2001 From: Jactus Date: Wed, 11 Nov 2020 09:50:38 +0800 Subject: [PATCH 033/241] Add black formatter output --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d5db7940a..d07ba1f88 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -43,7 +43,7 @@ jobs: - name: Lint with Black run: | cd .. - python -m black qlib -l 120 --check + python -m black qlib -l 120 --check --diff - name: Unit tests with Pytest run: | From 9c2dbaa94e699cbcae3856c781ceb1e4d156ecba Mon Sep 17 00:00:00 2001 From: lwwang1995 Date: Wed, 11 Nov 2020 10:26:28 +0800 Subject: [PATCH 034/241] add time series model GRU --- examples/workflow_by_code_gru.py | 146 ++++++++++++ qlib/contrib/data/handler.py | 83 +++++-- qlib/contrib/model/pytorch_gru.py | 362 ++++++++++++++++++++++++++++++ qlib/data/dataset/processor.py | 16 ++ 4 files changed, 586 insertions(+), 21 deletions(-) create mode 100755 examples/workflow_by_code_gru.py create mode 100755 qlib/contrib/model/pytorch_gru.py diff --git a/examples/workflow_by_code_gru.py b/examples/workflow_by_code_gru.py new file mode 100755 index 000000000..2bcbe2aa6 --- /dev/null +++ b/examples/workflow_by_code_gru.py @@ -0,0 +1,146 @@ +# 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.pytorch_gru import GRU +from qlib.contrib.data.handler import ALPHA360 +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 + +# from qlib.model.learner import train_model +from qlib.utils import init_instance_by_config + + +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_cn(target_dir=provider_uri) + + qlib.init(provider_uri=provider_uri, region=REG_CN) + + MARKET = "csi300" + BENCHMARK = "SH000300" + + + ################################### + # train model + ################################### + DATA_HANDLER_CONFIG = { + "start_time": "2008-01-01", + "end_time": "2020-08-01", + "fit_start_time":"2008-01-01", + "fit_end_time":"2014-12-31", + "instruments": MARKET, + } + + TRAINER_CONFIG = { + "train_start_time": "2008-01-01", + "train_end_time": "2014-12-31", + "validate_start_time": "2015-01-01", + "validate_end_time": "2016-12-31", + "test_start_time": "2017-01-01", + "test_end_time": "2020-08-01", + } + + task = { + "model": { + "class": "GRU", + "module_path": "qlib.contrib.model.pytorch_gru", + "kwargs": { + "d_feat": 6, + "hidden_size": 64, + "num_layers": 3, + "dropout": 0.0, + "n_epochs": 2000, + "lr": 1e-1, + "early_stop": 200, + "batch_size":800, + "smooth_steps": 5, + "metric": "mse", + "loss": "mse", + "seed": 0, + "GPU": 0, + } + }, + "dataset": { + "class": "DatasetH", + "module_path": "qlib.data.dataset", + "kwargs": { + 'handler': { + "class": "ALPHA360", + "module_path": "qlib.contrib.data.handler", + "kwargs": DATA_HANDLER_CONFIG + }, + 'segments': { + 'train': ("2008-01-01", "2014-12-31"), + 'valid': ("2015-01-01", "2016-12-31",), + 'test': ("2017-01-01", "2020-08-01",), + } + } + } + # You shoud record the data in specific sequence + # "record": ['SignalRecord', 'SigAnaRecord', 'PortAnaRecord'], + } + + # model = train_model(task) + model = init_instance_by_config(task['model']) + dataset = init_instance_by_config(task['dataset']) + + model.fit(dataset) + + pred_score = model.predict(dataset) + + # save pred_score to file + pred_score_path = Path("~/tmp/qlib/pred_score.pkl").expanduser() + pred_score_path.parent.mkdir(exist_ok=True, parents=True) + pred_score.to_pickle(pred_score_path) + + ################################### + # backtest + ################################### + STRATEGY_CONFIG = { + "topk": 50, + "n_drop": 5, + } + BACKTEST_CONFIG = { + "verbose": False, + "limit_threshold": 0.095, + "account": 100000000, + "benchmark": BENCHMARK, + "deal_price": "close", + "open_cost": 0.0005, + "close_cost": 0.0015, + "min_cost": 5, + } + + # use default strategy + # custom Strategy, refer to: TODO: Strategy API url + strategy = TopkDropoutStrategy(**STRATEGY_CONFIG) + report_normal, positions_normal = normal_backtest(pred_score, strategy=strategy, **BACKTEST_CONFIG) + + ################################### + # analyze + # If need a more detailed analysis, refer to: examples/train_and_bakctest.ipynb + ################################### + 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 + print(analysis_df) diff --git a/qlib/contrib/data/handler.py b/qlib/contrib/data/handler.py index b2fd0515d..61f8652be 100644 --- a/qlib/contrib/data/handler.py +++ b/qlib/contrib/data/handler.py @@ -8,29 +8,81 @@ from ...data.dataset import processor as processor_module from ...log import TimeInspector import copy - class ALPHA360(DataHandlerLP): - def __init__(self, instruments="csi500", start_time=None, end_time=None): + def __init__( + self, + instruments="csi500", + start_time=None, + end_time=None, + fit_start_time=None, + fit_end_time=None + ): data_loader = { "class": "QlibDataLoader", "kwargs": { "config": { - "feature": { - "price": {"windows": range(60)}, - "volume": {"windows": range(60)}, - }, + "feature": self.get_feature_config(), "label": self.get_label_config(), }, }, } + + learn_processors = [ + {"class": "DropnaLabel", "kwargs": {'group': 'label'}}, + {"class": "CSZScoreNorm", "kwargs": {"fields_group": "label"}}, + ] infer_processors = [ - {"class": "ConfigSectionProcessor", "module_path": "qlib.contrib.data.processor"} - ] # ConfigSectionProcessor will normalize LABEL0 - super().__init__(instruments, start_time, end_time, data_loader=data_loader, infer_processors=infer_processors) + {"class": "ProcessInf", "kwargs": {}}, + {"class": "ZscoreNorm", "kwargs": {"fit_start_time": fit_start_time, "fit_end_time": fit_end_time}}, + {"class": "Fillna", "kwargs": {}}, + ] + + super().__init__( + instruments, + start_time, + end_time, + data_loader=data_loader, + learn_processors=learn_processors, + infer_processors=infer_processors + ) def get_label_config(self): return (["Ref($close, -2)/Ref($close, -1) - 1"], ["LABEL0"]) + def get_feature_config(self): + + fields = [] + names = [] + + for i in range(59,0,-1): + fields += ["Ref($close, %d)/$close"%(i)] + names += ["CLOSE%d"%(i)] + fields += ["Ref($open, %d)/$close"%(i)] + names += ["OPEN%d"%(i)] + fields += ["Ref($high, %d)/$close"%(i)] + names += ["HIGH%d"%(i)] + fields += ["Ref($low, %d)/$close"%(i)] + names += ["LOW%d"%(i)] + fields += ["Ref($vwap, %d)/$close"%(i)] + names += ["VWAP%d"%(i)] + fields += ["Ref($volume, %d)/$volume"%(i)] + names += ["VOLUME%d"%(i)] + + fields += ["$close/$close"] + fields += ["$open/$close"] + fields += ["$high/$close"] + fields += ["$low/$close"] + fields += ["$vwap/$close"] + fields += ["$volume/$volume"] + names += ["CLOSE0"] + names += ["OPEN0"] + names += ["HIGH0"] + names += ["LOW0"] + names += ["VWAP0"] + names += ["VOLUME0"] + + return fields, names + class ALPHA360vwap(ALPHA360): def get_label_config(self): @@ -90,7 +142,7 @@ class Alpha158(DataHandlerLP): "kbar": {}, "price": { "windows": [0], - "feature": ["OPEN", "HIGH", "LOW"], + "feature": ["OPEN", "HIGH", "LOW", "VWAP"], }, "rolling": {}, } @@ -281,16 +333,5 @@ class Alpha158(DataHandlerLP): class Alpha158vwap(Alpha158): - def get_feature_config(self): - conf = { - "kbar": {}, - "price": { - "windows": [0], - "feature": ["OPEN", "HIGH", "LOW", "VWAP"], - }, - "rolling": {}, - } - return self.parse_config_to_fields(conf) - def get_label_config(self): return (["Ref($vwap, -2)/Ref($vwap, -1) - 1"], ["LABEL0"]) diff --git a/qlib/contrib/model/pytorch_gru.py b/qlib/contrib/model/pytorch_gru.py new file mode 100755 index 000000000..2a97d038e --- /dev/null +++ b/qlib/contrib/model/pytorch_gru.py @@ -0,0 +1,362 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + + +from __future__ import division +from __future__ import print_function + +import os +import numpy as np +import pandas as pd +import copy +from sklearn.metrics import roc_auc_score, mean_squared_error +import logging +from ...utils import unpack_archive_with_buffer, save_multiple_parts_file, create_save_path, drop_nan_by_y_index +from ...log import get_module_logger, TimeInspector + +import torch +import torch.nn as nn +import torch.optim as optim + +from ...model.base import Model +from ...data.dataset import DatasetH +from ...data.dataset.handler import DataHandlerLP + +class GRU(Model): + """GRU Model + + Parameters + ---------- + input_dim : int + input dimension + output_dim : int + output dimension + layers : tuple + layer sizes + lr : float + learning rate + lr_decay : float + learning rate decay + lr_decay_steps : int + learning rate decay steps + optimizer : str + optimizer name + GPU : str + the GPU ID(s) used for training + """ + + def __init__( + self, + d_feat=6, + hidden_size=64, + num_layers=2, + dropout=0.0, + n_epochs=200, + lr=0.001, + batch_size=2000, + early_stop=20, + eval_steps=5, + loss="mse", + lr_decay=0.96, + lr_decay_steps=100, + optimizer="gd", + GPU="0", + seed=0, + **kwargs + ): + # Set logger. + self.logger = get_module_logger("GRU") + self.logger.info("GRU pytorch version...") + + # set hyper-parameters. + self.d_feat = d_feat + self.hidden_size = hidden_size + self.num_layers = num_layers + self.dropout = dropout + self.n_epochs = n_epochs + self.lr = lr + self.batch_size = batch_size + self.early_stop = early_stop + self.eval_steps = eval_steps + self.lr_decay = lr_decay + self.lr_decay_steps = lr_decay_steps + self.optimizer = optimizer.lower() + self.loss_type = loss + self.visible_GPU = GPU + self.use_gpu = torch.cuda.is_available() + self.seed = seed + + self.logger.info( + "GRU parameters setting:" + "\nd_feat : {}" + "\nhidden_size : {}" + "\nnum_layers : {}" + "\ndropout : {}" + "\nn_epochs : {}" + "\nlr : {}" + "\nbatch_size : {}" + "\nearly_stop : {}" + "\neval_steps : {}" + "\nlr_decay : {}" + "\nlr_decay_steps : {}" + "\noptimizer : {}" + "\nloss_type : {}" + "\nvisible_GPU : {}" + "\nuse_GPU : {}" + "\nseed : {}".format( + d_feat, + hidden_size, + num_layers, + dropout, + n_epochs, + lr, + batch_size, + early_stop, + eval_steps, + lr_decay, + lr_decay_steps, + optimizer.lower(), + loss, + GPU, + self.use_gpu, + seed, + ) + ) + + if loss not in {"mse", "binary"}: + raise NotImplementedError("loss {} is not supported!".format(loss)) + self._scorer = mean_squared_error if loss == "mse" else roc_auc_score + + self.gru_model = GRUModel(d_feat=self.d_feat, hidden_size=self.hidden_size, num_layers=self.num_layers, dropout=self.dropout) + if optimizer.lower() == "adam": + self.train_optimizer = optim.Adam(self.gru_model.parameters(), lr=self.lr) + elif optimizer.lower() == "gd": + self.train_optimizer = optim.SGD(self.gru_model.parameters(), lr=self.lr) + else: + raise NotImplementedError("optimizer {} is not supported!".format(optimizer)) + + # Reduce learning rate when loss has stopped decrease + self.scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau( + self.train_optimizer, + mode="min", + factor=0.5, + patience=10, + verbose=True, + threshold=0.0001, + threshold_mode="rel", + cooldown=0, + min_lr=0.00001, + eps=1e-08, + ) + + self._fitted = False + if self.use_gpu: + self.gru_model.cuda() + # set the visible GPU + if self.visible_GPU: + os.environ["CUDA_VISIBLE_DEVICES"] = self.visible_GPU + + def fit( + self, + dataset: DatasetH, + evals_result=dict(), + verbose=True, + save_path=None, + ): + + df_train, df_valid = dataset.prepare( + ["train", "valid"], col_set=["feature", "label"], data_key=DataHandlerLP.DK_L + ) + x_train, y_train = df_train["feature"], df_train["label"] + x_valid, y_valid = df_valid["feature"], df_valid["label"] + + x_train.to_pickle('~/x_train_init.pkl') + y_train.to_pickle('~/y_train_init.pkl') + + x_train = x_train.fillna(0) + y_train = y_train.fillna(0) + x_valid = x_valid.fillna(0) + y_valid = y_valid.fillna(0) + x_train.to_pickle('~/x_train.pkl') + y_train.to_pickle('~/y_train.pkl') + + # Lightgbm need 1D array as its label + save_path = create_save_path(save_path) + stop_steps = 0 + train_loss = 0 + best_loss = np.inf + evals_result["train"] = [] + evals_result["valid"] = [] + + # train + self.logger.info("training...") + self._fitted = True + # return + # prepare training data + x_train_values = torch.from_numpy(x_train.values).float() + y_train_values = torch.from_numpy(np.squeeze(y_train.values)).float() + train_num = y_train_values.shape[0] + + # prepare validation data + x_val_auto = torch.from_numpy(x_valid.values).float() + y_val_auto = torch.from_numpy(np.squeeze(y_valid.values)).float() + + if self.use_gpu: + x_val_auto = x_val_auto.cuda() + y_val_auto = y_val_auto.cuda() + + for step in range(self.n_epochs): + if stop_steps >= self.early_stop: + if verbose: + self.logger.info("\tearly stop") + break + loss = AverageMeter() + self.gru_model.train() + self.train_optimizer.zero_grad() + + choice = np.random.choice(train_num, self.batch_size) + x_batch_auto = x_train_values[choice] + y_batch_auto = y_train_values[choice] + + if self.use_gpu: + x_batch_auto = x_batch_auto.float().cuda() + y_batch_auto = y_batch_auto.float().cuda() + + # forward + preds = self.gru_model(x_batch_auto) + cur_loss = self.get_loss(preds, y_batch_auto, self.loss_type) + cur_loss.backward() + self.train_optimizer.step() + loss.update(cur_loss.item()) + + # validation + train_loss += loss.val + # print(loss.val) + if step and step % self.eval_steps == 0: + stop_steps += 1 + train_loss /= self.eval_steps + + with torch.no_grad(): + self.gru_model.eval() + loss_val = AverageMeter() + + # forward + preds = self.gru_model(x_val_auto) + cur_loss_val = self.get_loss(preds, y_val_auto, self.loss_type) + loss_val.update(cur_loss_val.item()) + + if verbose: + self.logger.info( + "[Epoch {}]: train_loss {:.6f}, valid_loss {:.6f}".format(step, train_loss, loss_val.val) + ) + evals_result["train"].append(train_loss) + evals_result["valid"].append(loss_val.val) + if loss_val.val < best_loss: + if verbose: + self.logger.info( + "\tvalid loss update from {:.6f} to {:.6f}, save checkpoint.".format( + best_loss, loss_val.val + ) + ) + best_loss = loss_val.val + stop_steps = 0 + torch.save(self.gru_model.state_dict(), save_path) + train_loss = 0 + # update learning rate + self.scheduler.step(cur_loss_val) + + # restore the optimal parameters after training ?? + # self.gru_model.load_state_dict(torch.load(save_path)) + if self.use_gpu: + torch.cuda.empty_cache() + + def get_loss(self, pred, target, loss_type): + if loss_type == "mse": + sqr_loss = (pred - target)**2 + loss = sqr_loss.mean() + return loss + elif loss_type == "binary": + loss = nn.BCELoss() + return loss(pred, target) + else: + raise NotImplementedError("loss {} is not supported!".format(loss_type)) + + def predict(self, dataset): + if not self._fitted: + raise ValueError("model is not fitted yet!") + + x_test = dataset.prepare("test", col_set="feature") + x_test = x_test.fillna(0) + index = x_test.index + x_test = torch.from_numpy(x_test.values).float() + + if self.use_gpu: + x_test = x_test.cuda() + self.gru_model.eval() + + with torch.no_grad(): + if self.use_gpu: + preds = self.gru_model(x_test).detach().cpu().numpy() + else: + preds = self.gru_model(x_test).detach().numpy() + return pd.Series(preds, index=index) + + def save(self, filename, **kwargs): + with save_multiple_parts_file(filename) as model_dir: + model_path = os.path.join(model_dir, os.path.split(model_dir)[-1]) + # Save model + torch.save(self.gru_model.state_dict(), model_path) + + def load(self, buffer, **kwargs): + with unpack_archive_with_buffer(buffer) as model_dir: + # Get model name + _model_name = os.path.splitext(list(filter(lambda x: x.startswith("model.bin"), os.listdir(model_dir)))[0])[ + 0 + ] + _model_path = os.path.join(model_dir, _model_name) + # Load model + self.gru_model.load_state_dict(torch.load(_model_path)) + self._fitted = True + +class AverageMeter(object): + """Computes and stores the average and current value""" + + def __init__(self): + self.reset() + + def reset(self): + self.val = 0 + self.avg = 0 + self.sum = 0 + self.count = 0 + + def update(self, val, n=1): + self.val = val + self.sum += val * n + self.count += n + self.avg = self.sum / self.count + + +class GRUModel(nn.Module): + + def __init__(self, d_feat=6, hidden_size=64, num_layers=2, dropout=0.0): + super().__init__() + + self.rnn = nn.GRU( + input_size=d_feat, + hidden_size=hidden_size, + num_layers=num_layers, + batch_first=True, + dropout=dropout, + ) + self.fc_out = nn.Linear(hidden_size, 1) + + self.d_feat = d_feat + + def forward(self, x): + # x: [N, F*T] + x = x.reshape(len(x), self.d_feat, -1) # [N, F, T] + x = x.permute(0, 2, 1) # [N, T, F] + out, _ = self.rnn(x) + return self.fc_out(out[:, -1, :]).squeeze() + diff --git a/qlib/data/dataset/processor.py b/qlib/data/dataset/processor.py index ead8707db..1f6754312 100644 --- a/qlib/data/dataset/processor.py +++ b/qlib/data/dataset/processor.py @@ -106,6 +106,22 @@ class ProcessInf(Processor): return replace_inf(df) +class Fillna(Processor): + """Process infinity """ + + def __call__(self, df): + def fill_na(data): + def process_na(df): + for col in df.columns: + # FIXME: Such behavior is very weird + df[col] = df[col].fillna(0) + return df + + data = datetime_groupby_apply(data, process_na) + data.sort_index(inplace=True) + return data + + return fill_na(df) class MinMaxNorm(Processor): def __init__(self, fit_start_time, fit_end_time, fields_group=None): From ad6c2e759f5e75fc02f228cc663875afd704a50a Mon Sep 17 00:00:00 2001 From: lwwang1995 Date: Wed, 11 Nov 2020 11:11:36 +0800 Subject: [PATCH 035/241] Add Xgboost Model --- examples/workflow_by_code_xgboost.py | 144 +++++++++++++++++++++++++++ qlib/contrib/model/xgboost.py | 64 ++++++++++++ 2 files changed, 208 insertions(+) create mode 100755 examples/workflow_by_code_xgboost.py create mode 100755 qlib/contrib/model/xgboost.py diff --git a/examples/workflow_by_code_xgboost.py b/examples/workflow_by_code_xgboost.py new file mode 100755 index 000000000..0eb5f4e93 --- /dev/null +++ b/examples/workflow_by_code_xgboost.py @@ -0,0 +1,144 @@ +# 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.xgboost import XGBModel +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 + +# from qlib.model.learner import train_model +from qlib.utils import init_instance_by_config + +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_cn(target_dir=provider_uri) + + qlib.init(provider_uri=provider_uri, region=REG_CN) + + MARKET = "csi300" + BENCHMARK = "SH000300" + + + ################################### + # train model + ################################### + DATA_HANDLER_CONFIG = { + "start_time": "2008-01-01", + "end_time": "2020-08-01", + "fit_start_time":"2008-01-01", + "fit_end_time":"2014-12-31", + "instruments": MARKET, + } + + TRAINER_CONFIG = { + "train_start_time": "2008-01-01", + "train_end_time": "2014-12-31", + "validate_start_time": "2015-01-01", + "validate_end_time": "2016-12-31", + "test_start_time": "2017-01-01", + "test_end_time": "2020-08-01", + } + + task = { + "model": { + "class": "XGBModel", + "module_path": "qlib.contrib.model.xgboost", + "kwargs": { + "objective": 'reg:linear', + "n_estimators":5000, + "colsample_bytree": 0.85, + "learning_rate": 0.0421, + "subsample": 0.8789, + "max_depth": 8, + "num_leaves": 210, + "num_threads": 20, + "missing":-1, + "min_child_weight":1, + "nthread":4, + "tree_method":'hist', + } + }, + "dataset": { + "class": "DatasetH", + "module_path": "qlib.data.dataset", + "kwargs": { + 'handler': { + "class": "Alpha158", + "module_path": "qlib.contrib.data.handler", + "kwargs": DATA_HANDLER_CONFIG + }, + 'segments': { + 'train': ("2008-01-01", "2014-12-31"), + 'valid': ("2015-01-01", "2016-12-31",), + 'test': ("2017-01-01", "2020-08-01",), + } + } + } + # You shoud record the data in specific sequence + # "record": ['SignalRecord', 'SigAnaRecord', 'PortAnaRecord'], + } + + # model = train_model(task) + model = init_instance_by_config(task['model']) + dataset = init_instance_by_config(task['dataset']) + + model.fit(dataset) + pred_score = model.predict(dataset) + + # save pred_score to file + pred_score_path = Path("~/tmp/qlib/pred_score.pkl").expanduser() + pred_score_path.parent.mkdir(exist_ok=True, parents=True) + pred_score.to_pickle(pred_score_path) + + ################################### + # backtest + ################################### + STRATEGY_CONFIG = { + "topk": 50, + "n_drop": 5, + } + BACKTEST_CONFIG = { + "verbose": False, + "limit_threshold": 0.095, + "account": 100000000, + "benchmark": BENCHMARK, + "deal_price": "close", + "open_cost": 0.0005, + "close_cost": 0.0015, + "min_cost": 5, + } + + # use default strategy + # custom Strategy, refer to: TODO: Strategy API url + strategy = TopkDropoutStrategy(**STRATEGY_CONFIG) + report_normal, positions_normal = normal_backtest(pred_score, strategy=strategy, **BACKTEST_CONFIG) + + ################################### + # analyze + # If need a more detailed analysis, refer to: examples/train_and_bakctest.ipynb + ################################### + 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 + print(analysis_df) diff --git a/qlib/contrib/model/xgboost.py b/qlib/contrib/model/xgboost.py new file mode 100755 index 000000000..95954198e --- /dev/null +++ b/qlib/contrib/model/xgboost.py @@ -0,0 +1,64 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +import numpy as np +import pandas as pd +import xgboost as xgb + +from ...model.base import Model +from ...data.dataset import DatasetH +from ...data.dataset.handler import DataHandlerLP + + +class XGBModel(Model): + """XGBModel Model""" + + def __init__(self, obj="mse", **kwargs): + if obj not in {"mse", "binary"}: + raise NotImplementedError + self._params = {"obj": obj} + self._params.update(kwargs) + self.model = None + + def fit( + self, + dataset: DatasetH, + num_boost_round=1000, + early_stopping_rounds=50, + verbose_eval=20, + evals_result=dict(), + **kwargs + ): + + df_train, df_valid = dataset.prepare( + ["train", "valid"], col_set=["feature", "label"], data_key=DataHandlerLP.DK_L + ) + x_train, y_train = df_train["feature"], df_train["label"] + x_valid, y_valid = df_valid["feature"], df_valid["label"] + + # Lightgbm need 1D array as its label + if y_train.values.ndim == 2 and y_train.values.shape[1] == 1: + y_train_1d, y_valid_1d = np.squeeze(y_train.values), np.squeeze(y_valid.values) + else: + raise ValueError("XGBoost doesn't support multi-label training") + + dtrain = xgb.DMatrix(x_train.values, label=y_train_1d) + dvalid = xgb.DMatrix(x_valid.values, label=y_valid_1d) + self.model = xgb.train( + self._params, + dtrain=dtrain, + num_boost_round=num_boost_round, + evals=[(dtrain, 'train'), (dvalid, 'valid')], + early_stopping_rounds=early_stopping_rounds, + verbose_eval=verbose_eval, + evals_result=evals_result, + **kwargs + ) + evals_result["train"] = list(evals_result["train"].values())[0] + evals_result["valid"] = list(evals_result["valid"].values())[0] + + def predict(self, dataset): + if self.model is None: + raise ValueError("model is not fitted yet!") + x_test = dataset.prepare("test", col_set="feature") + return pd.Series(self.model.predict(xgb.DMatrix(np.squeeze(x_test.values))), index=x_test.index) From e2d89f44fb696ac49883260f313c57b0757ce105 Mon Sep 17 00:00:00 2001 From: lwwang1995 Date: Wed, 11 Nov 2020 11:35:17 +0800 Subject: [PATCH 036/241] Delete log --- qlib/contrib/model/pytorch_gru.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/qlib/contrib/model/pytorch_gru.py b/qlib/contrib/model/pytorch_gru.py index 2a97d038e..9e18b09c1 100755 --- a/qlib/contrib/model/pytorch_gru.py +++ b/qlib/contrib/model/pytorch_gru.py @@ -170,16 +170,6 @@ class GRU(Model): x_train, y_train = df_train["feature"], df_train["label"] x_valid, y_valid = df_valid["feature"], df_valid["label"] - x_train.to_pickle('~/x_train_init.pkl') - y_train.to_pickle('~/y_train_init.pkl') - - x_train = x_train.fillna(0) - y_train = y_train.fillna(0) - x_valid = x_valid.fillna(0) - y_valid = y_valid.fillna(0) - x_train.to_pickle('~/x_train.pkl') - y_train.to_pickle('~/y_train.pkl') - # Lightgbm need 1D array as its label save_path = create_save_path(save_path) stop_steps = 0 @@ -286,7 +276,6 @@ class GRU(Model): raise ValueError("model is not fitted yet!") x_test = dataset.prepare("test", col_set="feature") - x_test = x_test.fillna(0) index = x_test.index x_test = torch.from_numpy(x_test.values).float() From 52c0c4b7a808d2ed49846ab33a9c42a93c45fbd9 Mon Sep 17 00:00:00 2001 From: Jactus Date: Wed, 11 Nov 2020 14:24:04 +0800 Subject: [PATCH 037/241] Fix processor bug and format --- examples/workflow_by_code_gru.py | 35 ++++++++++------- examples/workflow_by_code_xgboost.py | 46 ++++++++++++---------- qlib/contrib/data/handler.py | 58 +++++++++++++--------------- qlib/contrib/model/pytorch_gru.py | 14 ++++--- qlib/contrib/model/xgboost.py | 4 +- qlib/data/dataset/handler.py | 25 ++---------- qlib/data/dataset/processor.py | 9 +++-- qlib/data/dataset/utils.py | 24 ++++++++++++ 8 files changed, 114 insertions(+), 101 deletions(-) diff --git a/examples/workflow_by_code_gru.py b/examples/workflow_by_code_gru.py index 2bcbe2aa6..52d3c451a 100755 --- a/examples/workflow_by_code_gru.py +++ b/examples/workflow_by_code_gru.py @@ -36,15 +36,14 @@ if __name__ == "__main__": MARKET = "csi300" BENCHMARK = "SH000300" - ################################### # train model ################################### DATA_HANDLER_CONFIG = { "start_time": "2008-01-01", "end_time": "2020-08-01", - "fit_start_time":"2008-01-01", - "fit_end_time":"2014-12-31", + "fit_start_time": "2008-01-01", + "fit_end_time": "2014-12-31", "instruments": MARKET, } @@ -69,37 +68,43 @@ if __name__ == "__main__": "n_epochs": 2000, "lr": 1e-1, "early_stop": 200, - "batch_size":800, + "batch_size": 800, "smooth_steps": 5, "metric": "mse", "loss": "mse", "seed": 0, "GPU": 0, - } + }, }, "dataset": { "class": "DatasetH", "module_path": "qlib.data.dataset", "kwargs": { - 'handler': { + "handler": { "class": "ALPHA360", "module_path": "qlib.contrib.data.handler", - "kwargs": DATA_HANDLER_CONFIG + "kwargs": DATA_HANDLER_CONFIG, }, - 'segments': { - 'train': ("2008-01-01", "2014-12-31"), - 'valid': ("2015-01-01", "2016-12-31",), - 'test': ("2017-01-01", "2020-08-01",), - } - } + "segments": { + "train": ("2008-01-01", "2014-12-31"), + "valid": ( + "2015-01-01", + "2016-12-31", + ), + "test": ( + "2017-01-01", + "2020-08-01", + ), + }, + }, } # You shoud record the data in specific sequence # "record": ['SignalRecord', 'SigAnaRecord', 'PortAnaRecord'], } # model = train_model(task) - model = init_instance_by_config(task['model']) - dataset = init_instance_by_config(task['dataset']) + model = init_instance_by_config(task["model"]) + dataset = init_instance_by_config(task["dataset"]) model.fit(dataset) diff --git a/examples/workflow_by_code_xgboost.py b/examples/workflow_by_code_xgboost.py index 0eb5f4e93..8883bacee 100755 --- a/examples/workflow_by_code_xgboost.py +++ b/examples/workflow_by_code_xgboost.py @@ -21,7 +21,6 @@ from qlib.utils import init_instance_by_config if __name__ == "__main__": - # use default data provider_uri = "~/.qlib/qlib_data/cn_data" # target_dir if not exists_qlib_data(provider_uri): @@ -36,15 +35,14 @@ if __name__ == "__main__": MARKET = "csi300" BENCHMARK = "SH000300" - ################################### # train model ################################### DATA_HANDLER_CONFIG = { "start_time": "2008-01-01", "end_time": "2020-08-01", - "fit_start_time":"2008-01-01", - "fit_end_time":"2014-12-31", + "fit_start_time": "2008-01-01", + "fit_end_time": "2014-12-31", "instruments": MARKET, } @@ -62,43 +60,49 @@ if __name__ == "__main__": "class": "XGBModel", "module_path": "qlib.contrib.model.xgboost", "kwargs": { - "objective": 'reg:linear', - "n_estimators":5000, + "objective": "reg:linear", + "n_estimators": 5000, "colsample_bytree": 0.85, "learning_rate": 0.0421, "subsample": 0.8789, "max_depth": 8, "num_leaves": 210, "num_threads": 20, - "missing":-1, - "min_child_weight":1, - "nthread":4, - "tree_method":'hist', - } + "missing": -1, + "min_child_weight": 1, + "nthread": 4, + "tree_method": "hist", + }, }, "dataset": { "class": "DatasetH", "module_path": "qlib.data.dataset", "kwargs": { - 'handler': { + "handler": { "class": "Alpha158", "module_path": "qlib.contrib.data.handler", - "kwargs": DATA_HANDLER_CONFIG + "kwargs": DATA_HANDLER_CONFIG, }, - 'segments': { - 'train': ("2008-01-01", "2014-12-31"), - 'valid': ("2015-01-01", "2016-12-31",), - 'test': ("2017-01-01", "2020-08-01",), - } - } + "segments": { + "train": ("2008-01-01", "2014-12-31"), + "valid": ( + "2015-01-01", + "2016-12-31", + ), + "test": ( + "2017-01-01", + "2020-08-01", + ), + }, + }, } # You shoud record the data in specific sequence # "record": ['SignalRecord', 'SigAnaRecord', 'PortAnaRecord'], } # model = train_model(task) - model = init_instance_by_config(task['model']) - dataset = init_instance_by_config(task['dataset']) + model = init_instance_by_config(task["model"]) + dataset = init_instance_by_config(task["dataset"]) model.fit(dataset) pred_score = model.predict(dataset) diff --git a/qlib/contrib/data/handler.py b/qlib/contrib/data/handler.py index 61f8652be..e8545c367 100644 --- a/qlib/contrib/data/handler.py +++ b/qlib/contrib/data/handler.py @@ -8,15 +8,9 @@ from ...data.dataset import processor as processor_module from ...log import TimeInspector import copy + class ALPHA360(DataHandlerLP): - def __init__( - self, - instruments="csi500", - start_time=None, - end_time=None, - fit_start_time=None, - fit_end_time=None - ): + def __init__(self, instruments="csi500", start_time=None, end_time=None, fit_start_time=None, fit_end_time=None): data_loader = { "class": "QlibDataLoader", "kwargs": { @@ -28,22 +22,22 @@ class ALPHA360(DataHandlerLP): } learn_processors = [ - {"class": "DropnaLabel", "kwargs": {'group': 'label'}}, - {"class": "CSZScoreNorm", "kwargs": {"fields_group": "label"}}, + {"class": "DropnaLabel", "kwargs": {"group": "label"}}, + {"class": "CSZScoreNorm", "kwargs": {"fields_group": "label"}}, ] infer_processors = [ - {"class": "ProcessInf", "kwargs": {}}, - {"class": "ZscoreNorm", "kwargs": {"fit_start_time": fit_start_time, "fit_end_time": fit_end_time}}, - {"class": "Fillna", "kwargs": {}}, + {"class": "ProcessInf", "kwargs": {}}, + {"class": "ZscoreNorm", "kwargs": {"fit_start_time": fit_start_time, "fit_end_time": fit_end_time}}, + {"class": "Fillna", "kwargs": {}}, ] super().__init__( - instruments, - start_time, - end_time, - data_loader=data_loader, - learn_processors=learn_processors, - infer_processors=infer_processors + instruments, + start_time, + end_time, + data_loader=data_loader, + learn_processors=learn_processors, + infer_processors=infer_processors, ) def get_label_config(self): @@ -54,19 +48,19 @@ class ALPHA360(DataHandlerLP): fields = [] names = [] - for i in range(59,0,-1): - fields += ["Ref($close, %d)/$close"%(i)] - names += ["CLOSE%d"%(i)] - fields += ["Ref($open, %d)/$close"%(i)] - names += ["OPEN%d"%(i)] - fields += ["Ref($high, %d)/$close"%(i)] - names += ["HIGH%d"%(i)] - fields += ["Ref($low, %d)/$close"%(i)] - names += ["LOW%d"%(i)] - fields += ["Ref($vwap, %d)/$close"%(i)] - names += ["VWAP%d"%(i)] - fields += ["Ref($volume, %d)/$volume"%(i)] - names += ["VOLUME%d"%(i)] + for i in range(59, 0, -1): + fields += ["Ref($close, %d)/$close" % (i)] + names += ["CLOSE%d" % (i)] + fields += ["Ref($open, %d)/$close" % (i)] + names += ["OPEN%d" % (i)] + fields += ["Ref($high, %d)/$close" % (i)] + names += ["HIGH%d" % (i)] + fields += ["Ref($low, %d)/$close" % (i)] + names += ["LOW%d" % (i)] + fields += ["Ref($vwap, %d)/$close" % (i)] + names += ["VWAP%d" % (i)] + fields += ["Ref($volume, %d)/$volume" % (i)] + names += ["VOLUME%d" % (i)] fields += ["$close/$close"] fields += ["$open/$close"] diff --git a/qlib/contrib/model/pytorch_gru.py b/qlib/contrib/model/pytorch_gru.py index 9e18b09c1..7b999d0a1 100755 --- a/qlib/contrib/model/pytorch_gru.py +++ b/qlib/contrib/model/pytorch_gru.py @@ -22,6 +22,7 @@ from ...model.base import Model from ...data.dataset import DatasetH from ...data.dataset.handler import DataHandlerLP + class GRU(Model): """GRU Model @@ -127,7 +128,9 @@ class GRU(Model): raise NotImplementedError("loss {} is not supported!".format(loss)) self._scorer = mean_squared_error if loss == "mse" else roc_auc_score - self.gru_model = GRUModel(d_feat=self.d_feat, hidden_size=self.hidden_size, num_layers=self.num_layers, dropout=self.dropout) + self.gru_model = GRUModel( + d_feat=self.d_feat, hidden_size=self.hidden_size, num_layers=self.num_layers, dropout=self.dropout + ) if optimizer.lower() == "adam": self.train_optimizer = optim.Adam(self.gru_model.parameters(), lr=self.lr) elif optimizer.lower() == "gd": @@ -262,7 +265,7 @@ class GRU(Model): def get_loss(self, pred, target, loss_type): if loss_type == "mse": - sqr_loss = (pred - target)**2 + sqr_loss = (pred - target) ** 2 loss = sqr_loss.mean() return loss elif loss_type == "binary": @@ -307,6 +310,7 @@ class GRU(Model): self.gru_model.load_state_dict(torch.load(_model_path)) self._fitted = True + class AverageMeter(object): """Computes and stores the average and current value""" @@ -327,7 +331,6 @@ class AverageMeter(object): class GRUModel(nn.Module): - def __init__(self, d_feat=6, hidden_size=64, num_layers=2, dropout=0.0): super().__init__() @@ -344,8 +347,7 @@ class GRUModel(nn.Module): def forward(self, x): # x: [N, F*T] - x = x.reshape(len(x), self.d_feat, -1) # [N, F, T] - x = x.permute(0, 2, 1) # [N, T, F] + x = x.reshape(len(x), self.d_feat, -1) # [N, F, T] + x = x.permute(0, 2, 1) # [N, T, F] out, _ = self.rnn(x) return self.fc_out(out[:, -1, :]).squeeze() - diff --git a/qlib/contrib/model/xgboost.py b/qlib/contrib/model/xgboost.py index 95954198e..f1208eb93 100755 --- a/qlib/contrib/model/xgboost.py +++ b/qlib/contrib/model/xgboost.py @@ -41,14 +41,14 @@ class XGBModel(Model): y_train_1d, y_valid_1d = np.squeeze(y_train.values), np.squeeze(y_valid.values) else: raise ValueError("XGBoost doesn't support multi-label training") - + dtrain = xgb.DMatrix(x_train.values, label=y_train_1d) dvalid = xgb.DMatrix(x_valid.values, label=y_valid_1d) self.model = xgb.train( self._params, dtrain=dtrain, num_boost_round=num_boost_round, - evals=[(dtrain, 'train'), (dvalid, 'valid')], + evals=[(dtrain, "train"), (dvalid, "valid")], early_stopping_rounds=early_stopping_rounds, verbose_eval=verbose_eval, evals_result=evals_result, diff --git a/qlib/data/dataset/handler.py b/qlib/data/dataset/handler.py index 13d3465c7..f6c097d22 100644 --- a/qlib/data/dataset/handler.py +++ b/qlib/data/dataset/handler.py @@ -16,7 +16,7 @@ from ...data import D from ...config import C from ...utils import parse_config, transform_end_date, init_instance_by_config from ...utils.serial import Serializable -from .utils import get_level_index +from .utils import get_level_index, fetch_df_by_index from pathlib import Path from .loader import DataLoader @@ -99,25 +99,6 @@ class DataHandler(Serializable): self._data = self.data_loader.load(self.instruments, self.start_time, self.end_time) # TODO: cache - def _fetch_df_by_index( - self, df: pd.DataFrame, selector: Union[pd.Timestamp, slice, str, list], level: Union[str, int] - ) -> pd.DataFrame: - """ - fetch data from `data` with `selector` and `level` - - Parameters - ---------- - selector : Union[pd.Timestamp, slice, str, list] - selector - level : Union[int, str] - the level to use the selector - """ - # Try to get the right index - idx_slc = (selector, slice(None, None)) - if get_level_index(df, level) == 1: - idx_slc = idx_slc[1], idx_slc[0] - return df.loc(axis=0)[idx_slc] - CS_ALL = "__all" def _fetch_df_by_col(self, df: pd.DataFrame, col_set: str) -> pd.DataFrame: @@ -156,7 +137,7 @@ class DataHandler(Serializable): ------- pd.DataFrame: """ - df = self._fetch_df_by_index(self._data, selector, level) + df = fetch_df_by_index(self._data, selector, level) df = self._fetch_df_by_col(df, col_set) if squeeze: # squeeze columns @@ -414,7 +395,7 @@ class DataHandlerLP(DataHandler): pd.DataFrame: """ df = self._get_df_by_key(data_key) - df = self._fetch_df_by_index(df, selector, level) + df = fetch_df_by_index(df, selector, level) return self._fetch_df_by_col(df, col_set) def get_cols(self, col_set=DataHandler.CS_ALL, data_key: str = DK_I) -> list: diff --git a/qlib/data/dataset/processor.py b/qlib/data/dataset/processor.py index 1f6754312..a9e404b7a 100644 --- a/qlib/data/dataset/processor.py +++ b/qlib/data/dataset/processor.py @@ -7,6 +7,7 @@ import pandas as pd import copy from ...log import TimeInspector +from .utils import fetch_df_by_index from ...utils.serial import Serializable from ...utils.paral import datetime_groupby_apply @@ -106,6 +107,7 @@ class ProcessInf(Processor): return replace_inf(df) + class Fillna(Processor): """Process infinity """ @@ -123,14 +125,15 @@ class Fillna(Processor): return fill_na(df) + class MinMaxNorm(Processor): def __init__(self, fit_start_time, fit_end_time, fields_group=None): - # FIXME: time is not used self.fit_start_time = fit_start_time self.fit_end_time = fit_end_time self.fields_group = fields_group def fit(self, df): + df = fetch_df_by_index(df, slice(self.fit_start_time, self.fit_end_time), level="datetime") cols = get_group_columns(df, self.fields_group) self.min_val = np.nanmin(df[cols].values, axis=0) self.max_val = np.nanmax(df[cols].values, axis=0) @@ -152,15 +155,15 @@ class MinMaxNorm(Processor): class ZscoreNorm(Processor): def __init__(self, fit_start_time, fit_end_time, fields_group=None): - # FIXME: time is not used self.fit_start_time = fit_start_time self.fit_end_time = fit_end_time self.fields_group = fields_group def fit(self, df): + df = fetch_df_by_index(df, slice(self.fit_start_time, self.fit_end_time), level="datetime") cols = get_group_columns(df, self.fields_group) self.mean_train = np.nanmean(df[cols].values, axis=0) - self.std_train = np.nanstd(df[cols].values, axis=0) + self.std_train = np.nanstd(_df[cols].values, axis=0) self.ignore = self.std_train == 0 self.cols = cols diff --git a/qlib/data/dataset/utils.py b/qlib/data/dataset/utils.py index af0900867..6eb00ffee 100644 --- a/qlib/data/dataset/utils.py +++ b/qlib/data/dataset/utils.py @@ -29,3 +29,27 @@ def get_level_index(df: pd.DataFrame, level=Union[str, int]) -> int: return level else: raise NotImplementedError(f"This type of input is not supported") + + +def fetch_df_by_index( + df: pd.DataFrame, selector: Union[pd.Timestamp, slice, str, list], level: Union[str, int] +) -> pd.DataFrame: + """ + fetch data from `data` with `selector` and `level` + + Parameters + ---------- + selector : Union[pd.Timestamp, slice, str, list] + selector + level : Union[int, str] + the level to use the selector + + Returns + ------- + Data of the given index. + """ + # Try to get the right index + idx_slc = (selector, slice(None, None)) + if get_level_index(df, level) == 1: + idx_slc = idx_slc[1], idx_slc[0] + return df.loc(axis=0)[idx_slc] From d45aa86fb5b2c4d81399b37074827d9765fcf0a0 Mon Sep 17 00:00:00 2001 From: lwwang1995 Date: Thu, 12 Nov 2020 13:11:14 +0800 Subject: [PATCH 038/241] Update GRU model. --- qlib/contrib/model/pytorch_gru.py | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/qlib/contrib/model/pytorch_gru.py b/qlib/contrib/model/pytorch_gru.py index 7b999d0a1..fb422f491 100755 --- a/qlib/contrib/model/pytorch_gru.py +++ b/qlib/contrib/model/pytorch_gru.py @@ -293,22 +293,6 @@ class GRU(Model): preds = self.gru_model(x_test).detach().numpy() return pd.Series(preds, index=index) - def save(self, filename, **kwargs): - with save_multiple_parts_file(filename) as model_dir: - model_path = os.path.join(model_dir, os.path.split(model_dir)[-1]) - # Save model - torch.save(self.gru_model.state_dict(), model_path) - - def load(self, buffer, **kwargs): - with unpack_archive_with_buffer(buffer) as model_dir: - # Get model name - _model_name = os.path.splitext(list(filter(lambda x: x.startswith("model.bin"), os.listdir(model_dir)))[0])[ - 0 - ] - _model_path = os.path.join(model_dir, _model_name) - # Load model - self.gru_model.load_state_dict(torch.load(_model_path)) - self._fitted = True class AverageMeter(object): From 063bfd4621639dd7918be200dcdb6cd9cfb88a43 Mon Sep 17 00:00:00 2001 From: lwwang1995 Date: Thu, 12 Nov 2020 13:17:10 +0800 Subject: [PATCH 039/241] Update processor. --- qlib/data/dataset/processor.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) mode change 100644 => 100755 qlib/data/dataset/processor.py diff --git a/qlib/data/dataset/processor.py b/qlib/data/dataset/processor.py old mode 100644 new mode 100755 index a9e404b7a..2be35b731 --- a/qlib/data/dataset/processor.py +++ b/qlib/data/dataset/processor.py @@ -112,16 +112,13 @@ class Fillna(Processor): """Process infinity """ def __call__(self, df): - def fill_na(data): - def process_na(df): - for col in df.columns: - # FIXME: Such behavior is very weird - df[col] = df[col].fillna(0) - return df + def fill_na(df): + for col in df.columns: + # FIXME: Such behavior is very weird + df[col] = df[col].fillna(0) - data = datetime_groupby_apply(data, process_na) - data.sort_index(inplace=True) - return data + df.sort_index(inplace=True) + return df return fill_na(df) From 54a256d3bc11099820fc044c4104d65e714ab71f Mon Sep 17 00:00:00 2001 From: lwwang1995 Date: Thu, 12 Nov 2020 13:20:49 +0800 Subject: [PATCH 040/241] Update processor. --- qlib/data/dataset/processor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qlib/data/dataset/processor.py b/qlib/data/dataset/processor.py index 2be35b731..66db55323 100755 --- a/qlib/data/dataset/processor.py +++ b/qlib/data/dataset/processor.py @@ -160,7 +160,7 @@ class ZscoreNorm(Processor): df = fetch_df_by_index(df, slice(self.fit_start_time, self.fit_end_time), level="datetime") cols = get_group_columns(df, self.fields_group) self.mean_train = np.nanmean(df[cols].values, axis=0) - self.std_train = np.nanstd(_df[cols].values, axis=0) + self.std_train = np.nanstd(df[cols].values, axis=0) self.ignore = self.std_train == 0 self.cols = cols From b32f6a8bb12025fbe68e8bc97d127d5cc49a98ac Mon Sep 17 00:00:00 2001 From: Jactus Date: Thu, 12 Nov 2020 17:05:24 +0800 Subject: [PATCH 041/241] Fix handler bug --- qlib/contrib/data/handler.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/qlib/contrib/data/handler.py b/qlib/contrib/data/handler.py index e8545c367..40f211c1c 100644 --- a/qlib/contrib/data/handler.py +++ b/qlib/contrib/data/handler.py @@ -3,9 +3,10 @@ from ...data.dataset.handler import DataHandlerLP from ...data.dataset.processor import Processor, MinMaxNorm, ZscoreNorm -from ...utils import get_cls_kwargs +from ...utils import get_cls_kwargs, get_module_by_module_path from ...data.dataset import processor as processor_module from ...log import TimeInspector +from inspect import isclass import copy @@ -99,9 +100,11 @@ class Alpha158(DataHandlerLP): for p in proc_l: if not isinstance(p, Processor): klass, pkwargs = get_cls_kwargs(p, processor_module) - # FIXME: It's hard code here!!!!! - if isinstance(klass, (MinMaxNorm, ZscoreNorm)): - assert fit_start_time is not None and fit_end_time is not None + processors = get_module_by_module_path(processor_module.__name__) + if klass.__name__ in [c for c in dir(processor_module) if isclass(getattr(processor_module, c))]: + assert ( + fit_start_time is not None and fit_end_time is not None + ), "Make sure fit_start_time and fit_end_time are not None." pkwargs.update( { "fit_start_time": fit_start_time, From 59745486f588ecbdd7f305a2f33937b9bc0c8666 Mon Sep 17 00:00:00 2001 From: Jactus Date: Thu, 12 Nov 2020 17:34:43 +0800 Subject: [PATCH 042/241] Update handler --- qlib/contrib/data/handler.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/qlib/contrib/data/handler.py b/qlib/contrib/data/handler.py index 40f211c1c..54e9de27f 100644 --- a/qlib/contrib/data/handler.py +++ b/qlib/contrib/data/handler.py @@ -2,11 +2,11 @@ # Licensed under the MIT License. from ...data.dataset.handler import DataHandlerLP -from ...data.dataset.processor import Processor, MinMaxNorm, ZscoreNorm -from ...utils import get_cls_kwargs, get_module_by_module_path +from ...data.dataset.processor import Processor +from ...utils import get_cls_kwargs from ...data.dataset import processor as processor_module from ...log import TimeInspector -from inspect import isclass +from inspect import getfullargspec import copy @@ -100,11 +100,11 @@ class Alpha158(DataHandlerLP): for p in proc_l: if not isinstance(p, Processor): klass, pkwargs = get_cls_kwargs(p, processor_module) - processors = get_module_by_module_path(processor_module.__name__) - if klass.__name__ in [c for c in dir(processor_module) if isclass(getattr(processor_module, c))]: + args = getfullargspec(klass).args + if "fit_start_time" in args and "fit_end_time" in args: assert ( fit_start_time is not None and fit_end_time is not None - ), "Make sure fit_start_time and fit_end_time are not None." + ), "Make sure `fit_start_time` and `fit_end_time` are not None." pkwargs.update( { "fit_start_time": fit_start_time, From 138ab10c1ab306bf78f6f4372ba411faea97a4e1 Mon Sep 17 00:00:00 2001 From: lwwang1995 Date: Fri, 13 Nov 2020 10:55:10 +0800 Subject: [PATCH 043/241] Update Dropna function in processor and check model loading and saving with pickle. --- qlib/data/dataset/processor.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/qlib/data/dataset/processor.py b/qlib/data/dataset/processor.py index 66db55323..308c531b9 100755 --- a/qlib/data/dataset/processor.py +++ b/qlib/data/dataset/processor.py @@ -112,12 +112,12 @@ class Fillna(Processor): """Process infinity """ def __call__(self, df): - def fill_na(df): - for col in df.columns: - # FIXME: Such behavior is very weird - df[col] = df[col].fillna(0) + def fill_na(df, columns=None, fill=0): + + if columns == None: + columns = df.columns + df[columns] = df[columns].fillna(fill) - df.sort_index(inplace=True) return df return fill_na(df) From ea5f14ce124fb7f20b76279de2dbb07302a2546f Mon Sep 17 00:00:00 2001 From: Jactus Date: Fri, 13 Nov 2020 21:34:13 +0800 Subject: [PATCH 044/241] Update R related codes --- examples/workflow_by_code_gru.py | 10 +- examples/workflow_by_code_xgboost.py | 10 +- qlib/__init__.py | 28 +- qlib/config.py | 10 +- qlib/contrib/model/pytorch_gru.py | 1 - qlib/workflow/__init__.py | 448 +++++++++++++++++++++++++-- qlib/workflow/exp.py | 143 +++++++-- qlib/workflow/expm.py | 143 ++++++--- qlib/workflow/recorder.py | 42 ++- 9 files changed, 704 insertions(+), 131 deletions(-) diff --git a/examples/workflow_by_code_gru.py b/examples/workflow_by_code_gru.py index 52d3c451a..06fea1511 100755 --- a/examples/workflow_by_code_gru.py +++ b/examples/workflow_by_code_gru.py @@ -87,14 +87,8 @@ if __name__ == "__main__": }, "segments": { "train": ("2008-01-01", "2014-12-31"), - "valid": ( - "2015-01-01", - "2016-12-31", - ), - "test": ( - "2017-01-01", - "2020-08-01", - ), + "valid": ("2015-01-01", "2016-12-31"), + "test": ("2017-01-01", "2020-08-01"), }, }, } diff --git a/examples/workflow_by_code_xgboost.py b/examples/workflow_by_code_xgboost.py index 8883bacee..94b43f449 100755 --- a/examples/workflow_by_code_xgboost.py +++ b/examples/workflow_by_code_xgboost.py @@ -85,14 +85,8 @@ if __name__ == "__main__": }, "segments": { "train": ("2008-01-01", "2014-12-31"), - "valid": ( - "2015-01-01", - "2016-12-31", - ), - "test": ( - "2017-01-01", - "2020-08-01", - ), + "valid": ("2015-01-01", "2016-12-31"), + "test": ("2017-01-01", "2020-08-01"), }, }, } diff --git a/qlib/__init__.py b/qlib/__init__.py index b26ac986d..7fe7e79dc 100644 --- a/qlib/__init__.py +++ b/qlib/__init__.py @@ -11,6 +11,7 @@ import re import subprocess import platform import yaml +import atexit from pathlib import Path from .utils import can_use_cache, init_instance_by_config, get_module_by_module_path @@ -63,12 +64,10 @@ def init(default_conf="client", **kwargs): if not os.path.exists(C["provider_uri"]): if C["auto_mount"]: LOG.error( - "Invalid provider uri: {}, please check if a valid provider uri has been set. This path does not exist.".format( - C["provider_uri"] - ) + f"Invalid provider uri: {C['provider_uri']}, please check if a valid provider uri has been set. This path does not exist." ) else: - LOG.warning("auto_path is False, please make sure {} is mounted".format(C["mount_path"])) + LOG.warning(f"auto_path is False, please make sure {C['mount_path']} is mounted") elif C.get_uri_type() == QlibConfig.NFS_URI: _mount_nfs_uri(C) else: @@ -83,10 +82,11 @@ def init(default_conf="client", **kwargs): LOG.info(f"flask_server={C['flask_server']}, flask_port={C['flask_port']}") # set up QlibRecorder - module = get_module_by_module_path("qlib.workflow.expm") - exp_manager = init_instance_by_config(C["exp_manager"], module) + exp_manager = init_instance_by_config(C["exp_manager"]) qr = QlibRecorder(exp_manager) R.register(qr) + # clean up experiment when python program ends + atexit.register(R.end_exp, status="FAILED") # will not take effect if experiment ends def _mount_nfs_uri(C): @@ -102,9 +102,7 @@ def _mount_nfs_uri(C): if not C["auto_mount"]: if not os.path.exists(C["mount_path"]): raise FileNotFoundError( - "Invalid mount path: {}! Please mount manually: {} or Set init parameter `auto_mount=True`".format( - C["mount_path"], mount_command - ) + f"Invalid mount path: {C['mount_path']}! Please mount manually: {mount_command} or Set init parameter `auto_mount=True`" ) else: # Judging system type @@ -161,9 +159,7 @@ def _mount_nfs_uri(C): os.makedirs(C["mount_path"], exist_ok=True) except Exception: raise OSError( - "Failed to create directory {}, please create {} manually!".format( - C["mount_path"], C["mount_path"] - ) + f"Failed to create directory {C['mount_path']}, please create {C['mount_path']} manually!" ) # check nfs-common @@ -175,17 +171,15 @@ def _mount_nfs_uri(C): command_status = os.system(mount_command) if command_status == 256: raise OSError( - "mount {} on {} error! Needs SUDO! Please mount manually: {}".format( - C["provider_uri"], C["mount_path"], mount_command - ) + f"mount {C['provider_uri']} on {C['mount_path']} error! Needs SUDO! Please mount manually: {mount_command}" ) elif command_status == 32512: # LOG.error("Command error") - raise OSError("mount {} on {} error! Command error".format(C["provider_uri"], C["mount_path"])) + raise OSError(f"mount {C['provider_uri']} on {C['mount_path']} error! Command error") elif command_status == 0: LOG.info("Mount finished") else: - LOG.warning("{} on {} is already mounted".format(_remote_uri, _mount_path)) + LOG.warning(f"{_remote_uri} on {_mount_path} is already mounted") def init_from_yaml_conf(conf_path): diff --git a/qlib/config.py b/qlib/config.py index 31acfc535..6c744a9f0 100644 --- a/qlib/config.py +++ b/qlib/config.py @@ -126,8 +126,14 @@ _default_config = { "loggers": {"qlib": {"level": "DEBUG", "handlers": ["console"]}}, }, # Defatult config for experiment manager - "exp_manager": {"class": "MLflowExpManager", "kwargs": {}}, - "exp_uri": str(Path(os.getcwd()).resolve() / "mlruns"), + "exp_manager": { + "class": "MLflowExpManager", + "module_path": "qlib.workflow.expm", + "kwargs": { + "uri": str(Path(os.getcwd()).resolve() / "mlruns"), + "default_exp_name": "Experiment", + }, + }, } MODE_CONF = { diff --git a/qlib/contrib/model/pytorch_gru.py b/qlib/contrib/model/pytorch_gru.py index fb422f491..464cd9ba0 100755 --- a/qlib/contrib/model/pytorch_gru.py +++ b/qlib/contrib/model/pytorch_gru.py @@ -294,7 +294,6 @@ class GRU(Model): return pd.Series(preds, index=index) - class AverageMeter(object): """Computes and stores the average and current value""" diff --git a/qlib/workflow/__init__.py b/qlib/workflow/__init__.py index b801da880..978e45c27 100644 --- a/qlib/workflow/__init__.py +++ b/qlib/workflow/__init__.py @@ -4,20 +4,51 @@ from contextlib import contextmanager from .expm import MLflowExpManager from ..utils import Wrapper -from ..config import C class QlibRecorder: """ A global system that helps to manage the experiments. + + The components of the system: + 1) ExperimentManager: a class managing experiments. + 2) Experiment: a class of experiment, and each instance of it is responsible for a single experiment. + 3) Recorder: a class of recorder, and each instance of it is responsible for a single run. + + The general structure of the system: + ExperimentManager + - Experiment 1 + - Recorder 1 + - Recorder 2 + - ... + - Experiment 2 + - ... + - ... + """ def __init__(self, exp_manager): self.exp_manager = exp_manager - self.uri = C["exp_uri"] @contextmanager def start(self, experiment_name): + """ + Method to start an experiment. This method can only be called within a Python's `with` statement. + + Use case: + --------- + ``` + with R.start('test'): + model.fit(dataset) + R.log... + ... # further operations + ``` + + Parameters + ---------- + experiment_name : str + name of the experiment one wants to start. + """ run = self.start_exp(experiment_name) try: yield run @@ -26,44 +57,425 @@ class QlibRecorder: raise e self.end_exp("FINISHED") - def start_exp(self, experiment_name=None): - return self.exp_manager.start_exp(experiment_name, self.uri) + def start_exp(self, experiment_name=None, uri=None): + """ + Lower leverl method for starting an experiment. When use this method, one should end the experiment manually + and the status of the recorder may not be handled properly. + + Use case: + --------- + ``` + R.start_exp(experiment_name='test') + ... # further operations + R.end_exp('FINISHED') + ``` + + Parameters + ---------- + experiment_name : str + the name of the experiment to be started + uri : str + the tracking uri of the experiment, where all the artifacts/metrics etc. will be stored. + + Returns + ------- + An experiment instance being started. + """ + return self.exp_manager.start_exp(experiment_name, uri) def end_exp(self, status): + """ + Method for ending an experiment manually. It will end the current active experiment, as well as its + active recorder with the specified `status` type. + + Use case: + --------- + ``` + R.start_exp(experiment_name='test') + ... # further operations + R.end_exp('FINISHED') + ``` + + Parameters + ---------- + status : str + The status of a recorder, which can be SCHEDULED, RUNNING, FINISHED, FAILED. + """ self.exp_manager.end_exp(status) def search_records(self, experiment_ids, **kwargs): + """ + Get a pandas DataFrame of records that fit the search criteria. + + Use case: + --------- + ``` + R.log_metrics(m=2.50, step=0) + records = R.search_runs([experiment_id], order_by=["metrics.m DESC"]) + ``` + + Parameters + ---------- + experiment_ids : list + list of experiment IDs. + filter_string : str + filter query string, defaults to searching all runs. + run_view_type : int + one of enum values ACTIVE_ONLY, DELETED_ONLY, or ALL (e.g. in mlflow.entities.ViewType). + max_results : int + the maximum number of runs to put in the dataframe. + order_by : list + list of columns to order by (e.g., “metrics.rmse”). + + Returns + ------- + A pandas.DataFrame of records, where each metric, parameter, and tag + are expanded into their own columns named metrics.*, params.*, and tags.* + respectively. For records that don't have a particular metric, parameter, or tag, their + value will be (NumPy) Nan, None, or None respectively. + """ return self.exp_manager.search_records(experiment_ids, **kwargs) - def get_exp(self, experiment_id=None, experiment_name=None): - return self.exp_manager.get_exp(experiment_id, experiment_name) + def list_experiments(self): + """ + Method for listing all the existing experiments (except for those being deleted.) - def delete_exp(self, experiment_id): - self.exp_manager.delete_exp(experiment_id) + Use case: + --------- + ``` + exps = R.list_experiments() + ``` + + Returns + ------- + A dictionary (name -> experiment) of experiments information that being stored. + """ + return self.exp_manager.list_experiments() + + def list_recorders(self, experiment_id=None, experiment_name=None): + """ + Method for listing all the recorders of experiment with given id or name. + + Use case: + --------- + ``` + recorders = R.list_recorders(experiment_name='test') + ``` + + Parameters + ---------- + experiment_id : str + id of the experiment. + experiment_name : str + name of the experiment. + + Returns + ------- + A dictionary (id -> recorder) of recorder information that being stored. + """ + return self.get_exp(experiment_id, experiment_name).list_recorders() + + def get_exp(self, experiment_id=None, experiment_name=None, create=True): + """ + Method for retrieving an experiment with given id or name. Once the `create` argument is set to + True, if no valid experiment is found, this method will create one for you. Otherwise, it will + only retrieve a specific experiment or raise an Error. + + If `create` is True: + If R's running: + 1) no id or name specified, return the active experiment. + 2) if id or name is specified, return the specified experiment. If no such exp found, + create a new experiment with given id or name. + If R's not running: + 1) no id or name specified, create a default experiment. + 2) if id or name is specified, return the specified experiment. If no such exp found, + create a new experiment with given id or name. + Else If `create` is False: + If R's running: + 1) no id or name specified, return the active experiment. + 2) if id or name is specified, return the specified experiment. If no such exp found, + raise Error. + If R's not running: + 1) no id or name specified, raise Error. + 2) if id or name is specified, return the specified experiment. If no such exp found, + raise Error. + + Use case: + --------- + ``` + # Case 1 + with R.start('test'): + exp = R.get_exp() + recorders = exp.list_recorders() + + # Case 2 + with R.start('test'): + exp = R.get_exp('test1') + + # Case 3 + exp = R.get_exp() -> a default experiment. + + # Case 4 + exp = R.get_exp(experiment_name='test') + + # Case 5 + exp = R.get_exp(create=False) -> Error + ``` + + Parameters + ---------- + experiment_id : str + id of the experiment. + experiment_name : str + name of the experiment. + create : boolean + decide whether to create an default experiment. + + Returns + ------- + An experiment instance with given id or name. + """ + return self.exp_manager.get_exp(experiment_id, experiment_name, create) + + def delete_exp(self, experiment_id=None, experiment_name=None): + """ + Method for deleting the experiment with given id or name. At least one of id or name must be given, + otherwise, error will occur. + + Use case: + --------- + ``` + R.delete_exp(experiment_name='test') + ``` + + Parameters + ---------- + experiment_id : str + id of the experiment. + experiment_name : str + name of the experiment. + """ + self.exp_manager.delete_exp(experiment_id, experiment_name) def get_uri(self): + """ + Method for retrieving the uri of current experiment manager. + + Use case: + --------- + ``` + uri = R.get_uri() + ``` + + Returns + ------- + The uri of current experiment manager. + """ return self.exp_manager.get_uri() - def get_recorder(self, recorder_id=None, recorder_name=None): - return self.exp_manager.active_experiment.get_recorder(recorder_id, recorder_name) + def get_recorder(self, recorder_id=None, recorder_name=None, experiment_name=None): + """ + Method for retrieving a recorder. + + If R's running: 1) no id or name specified, return the active recorder. 2) if id or name is + specified, return the specified recorder. + If R's not running: 1) no id or name specified, raise Error. 2) if id or name is specified, + and the corresponding experiment_name must be given, return the specified recorder. Otherwise, + raise Error. + + The recorder can be used for further process such as `save_object`, `load_object`, `log_params`, + `log_metrics`, etc. + + Use case: + --------- + ``` + # Case 1 + with R.start('test'): + recorder = R.get_recorder() + + # Case 2 + with R.start('test'): + recorder = R.get_recorder(recorder_id='2e7a4efd66574fa49039e00ffaefa99d') + + # Case 3 + recorder = R.get_recorder() -> Error + + # Case 4 + recorder = R.get_recorder(recorder_id='2e7a4efd66574fa49039e00ffaefa99d') -> Error + + # Case 5 + recorder = R.get_recorder(recorder_id='2e7a4efd66574fa49039e00ffaefa99d', experiment_name='test') + ``` + + Parameters + ---------- + recorder_id : str + id of the recorder. + recorder_name : str + name of the recorder. + experiment_name : str + name of the experiment. + + + Returns + ------- + A recorder instance. + """ + return self.get_exp(experiment_name=experiment_name, create=False).get_recorder( + recorder_id, recorder_name, create=False + ) + + def delete_recorder(self, recorder_id=None, recorder_name=None): + """ + Method for deleting the recorders with given id or name. At least one of id or name must be given, + otherwise, error will occur. + + Use case: + --------- + ``` + R.delete_recorder(recorder_id='2e7a4efd66574fa49039e00ffaefa99d') + ``` + + Parameters + ---------- + recorder_id : str + id of the experiment. + recorder_name : str + name of the experiment. + """ + self.get_exp().delete_recorder(recorder_id, recorder_name) def save_objects(self, local_path=None, artifact_path=None, **kwargs): - self.exp_manager.active_experiment.active_recorder.save_objects(local_path, artifact_path, **kwargs) + """ + Method for saving objects as artifacts in the experiment to the uri. It supports either saving + from a local file/directory, or directly saving objects. - def load_object(self, name): - return self.exp_manager.active_experiment.active_recorder.load_object(name) + If R's running: it will save the objects through the running recorder. + If R's not running: the system will create a default experiment, and a new recorder and + save objects under it. + + If one wants to save objects with a specific recorder. It is recommended to first + get the specific recorder through `get_recorder` API and use the recorder the save objects. + The supported arguments are the same as this method. + + Use case: + --------- + ``` + # Case 1 + with R.start('test'): + pred = model.predict(dataset) + R.save_objects(data=pred, name='pred.pkl', artifact_path='prediction') + + # Case 2 + with R.start('test'): + pred1 = model1.predict(dataset) + pred2 = model2.predict(dataset) + dn_list = [(pred1, 'pred1.pkl'), (pred2, 'pred2.pkl')] + R.save_objects(data_name_list=dn_list) + + # Case 3 + with R.start('test'): + R.save_objects(local_path='results/pred.pkl') + ``` + + Parameters + ---------- + data : any type + the data to be saved. + name : str + name of the file to be saved. + data_name_list : list + list of (data, name) pairs + local_path : str + if provided, them save the file or directory to the artifact URI. + artifact_path=None : str + the relative path for the artifact to be stored in the URI. + """ + self.get_exp().get_recorder().save_objects(local_path, artifact_path, **kwargs) def log_params(self, **kwargs): - self.exp_manager.active_experiment.active_recorder.log_params(**kwargs) + """ + Method for logging parameters during an experiment. + + If R's running: it will log parameters through the running recorder. + If R's not running: the system will create a default experiment as well as a new recorder, and + log parameters under it. + + One can also log to a specific recorder after getting it with `get_recorder` API. + + Use case: + --------- + ``` + # Case 1 + with R.start('test'): + R.log_params(learning_rate=0.01) + + # Case 2 + R.log_params(learning_rate=0.01) + ``` + + Parameters + ---------- + keyword argument: + name1=value1, name2=value2, ... + """ + self.get_exp().get_recorder().log_params(**kwargs) def log_metrics(self, step=None, **kwargs): - self.exp_manager.active_experiment.active_recorder.log_metrics(step, **kwargs) + """ + Method for logging metrics during an experiment. + + If R's running: it will log metrics through the running recorder. + If R's not running: the system will create a default experiment as well as a new recorder, and + log metrics under it. + + One can also log to a specific recorder after getting it with `get_recorder` API. + + Use case: + --------- + ``` + # Case 1 + with R.start('test'): + R.log_metrics(train_loss=0.33, step=1) + + # Case 2 + R.log_metrics(train_loss=0.33, step=1) + ``` + + Parameters + ---------- + keyword argument: + name1=value1, name2=value2, ... + """ + self.get_exp().get_recorder().log_metrics(step, **kwargs) def set_tags(self, **kwargs): - self.exp_manager.active_experiment.active_recorder.set_tags(**kwargs) + """ + Method for setting tags for a recorder. - def delete_tag(self, *key): - self.exp_manager.active_experiment.active_recorder.delete_tag(*key) + If R's running: it will set tags through the running recorder. + If R's not running: the system will create a default experiment as well as a new recorder, and + set the tags under it. + + One can also set the tag to a specific recorder after getting it with `get_recorder` API. + + Use case: + --------- + ``` + # Case 1 + with R.start('test'): + R.set_tags(release_version=2.2.0) + + # Case 2 + R.set_tags(release_version=2.2.0) + ``` + + Parameters + ---------- + keyword argument: + name1=value1, name2=value2, ... + """ + self.get_exp().get_recorder().set_tags(**kwargs) # global record diff --git a/qlib/workflow/exp.py b/qlib/workflow/exp.py index 432497fda..005b113df 100644 --- a/qlib/workflow/exp.py +++ b/qlib/workflow/exp.py @@ -2,6 +2,7 @@ # Licensed under the MIT License. import mlflow +from datetime import datetime from pathlib import Path from .recorder import MLflowRecorder from ..log import get_module_logger @@ -11,12 +12,13 @@ logger = get_module_logger("workflow", "INFO") class Experiment: """ - Thie is the `Experiment` class for each experiment being run. The API is designed + Thie is the `Experiment` class for each experiment being run. The API is designed similar to mlflow. + (The link: https://mlflow.org/docs/latest/python_api/mlflow.html) """ - def __init__(self): - self.name = None - self.id = None + def __init__(self, id, name): + self.id = id + self.name = name self.active_recorder = None # only one recorder can running each time self.recorders = dict() # recorder id -> object @@ -32,16 +34,14 @@ class Experiment: output["class"] = "Experiment" output["id"] = self.id output["name"] = self.name - output["active_recorder"] = self.active_recorder.id + output["active_recorder"] = self.active_recorder.id if self.active_recorder is not None else None output["recorders"] = list(self.recorders.keys()) + return output def start(self): """ Start the experiment. - Parameters - ---------- - Returns ------- A running recorder instance. @@ -63,9 +63,6 @@ class Experiment: """ Create a recorder for each experiment. - Parameters - ---------- - Returns ------- A recorder object. @@ -124,13 +121,31 @@ class Experiment: """ raise NotImplementedError(f"Please implement the `get_recorder` method.") + def list_recorders(self): + """ + List all the existing recorders of this experiment. + + Returns + ------- + A dictionary (id -> recorder) of recorder information that being stored. + """ + raise NotImplementedError(f"Please implement the `list_recorders` method.") + class MLflowExperiment(Experiment): """ Use mlflow to implement Experiment. """ + def __init__(self, id, name, uri): + super(MLflowExperiment, self).__init__(id, name) + self._uri = uri + self._total_recorders = 0 + self._default_name = None + def start(self): + # get all the recorders of the experiment + self.recorders = self.list_recorders() # set up recorder recorder = self.create_recorder() self.active_recorder = recorder @@ -138,17 +153,22 @@ class MLflowExperiment(Experiment): run = self.active_recorder.start_run() # store the recorder self.recorders[self.active_recorder.id] = recorder + self._total_recorders += 1 # update recorder num + logger.info(f"Experiment {self.id} starts running ...") + return self.active_recorder def end(self, status): if self.active_recorder is not None: self.active_recorder.end_run(status) self.active_recorder = None + self._total_recorders -= 1 def create_recorder(self): num = len(self.recorders) name = "Recorder_{}".format(num + 1) - recorder = MLflowRecorder(name, self.id) + recorder = MLflowRecorder(name, self.id, self._uri) + return recorder def search_records(self, **kwargs): @@ -156,21 +176,92 @@ class MLflowExperiment(Experiment): run_view_type = 1 if kwargs.get("run_view_type") is None else kwargs.get("run_view_type") max_results = 100000 if kwargs.get("max_results") is None else kwargs.get("max_results") order_by = kwargs.get("order_by") + return mlflow.search_runs([self.id], filter_string, run_view_type, max_results, order_by) - def delete_recorder(self, recorder_id): - mlflow.delete_run(recorder_id) - self.recorders = [r for r in self.recorders if r.id == recorder_id] + def delete_recorder(self, recorder_id=None, recorder_name=None): + assert ( + recorder_id is not None or recorder_name is not None + ), "Please input a valid recorder id or name before deleting." + try: + if recorder_id is not None: + mlflow.delete_run(recorder_id) + self.recorders = [r for r in self.recorders if r == recorder_id] + else: + for r in self.recorders: + if self.recorders[r].name == recorder_name: + recorder_id = r + break + mlflow.delete_run(recorder_id) + except: + raise Exception( + "Something went wrong when deleting recorder. Please check if the name/id of the recorder is correct." + ) - def get_recorder(self, recorder_id=None, recorder_name=None): - if recorder_id is not None: - return self.recorders[recorder_id] - elif recorder_name is not None: - for rid in self.recorders: - if self.recorders[rid].name == recorder_name: - return self.recorders[rid] - elif self.active_recorder is None: - raise Exception("No valid active recorder exists. Please make sure the experiment is running.") + def get_recorder(self, recorder_id=None, recorder_name=None, create=True): + if recorder_id is None and recorder_name is None: + if self.active_recorder: + return self.active_recorder + else: + if create: + self.start() + logger.warning( + f"Recorder {self.active_recorder.id} is running under the experiment with name {self.name}..." + ) + return self.active_recorder + else: + raise Exception( + "Something went wrong when retrieving recorders. Please check if QlibRecorder is running or the name/id of the recorder is correct." + ) else: - logger.info("No experiment id or name is given. Return the current active experiment.") - return self.active_recorder + if recorder_id is not None: + if recorder_id in self.recorders: + return self.recorders[recorder_id] + else: + # mlflow does not support create a run with given id + raise Exception( + "Something went wrong when retrieving recorders. Please check if QlibRecorder is running or the name/id of the recorder is correct." + ) + else: + for rid in self.recorders: + if self.recorders[rid].name == recorder_name: + return self.recorders[rid] + if create: + self.recorders = self.list_recorders() + logger.warning(f"No valid recorder found. Create a new recorder with name {recorder_name}.") + recorder = self.create_recorder() + recorder.name = recorder_name + recorder.start_run() + return recorder + else: + raise Exception( + "Something went wrong when retrieving experiments. Please check if QlibRecorder is running or the name/id of the experiment is correct." + ) + + def list_recorders(self): + client = mlflow.tracking.MlflowClient(tracking_uri=self._uri) + runs = client.list_run_infos(self.id)[::-1] + recorders = dict() + self._total_recorders = len(runs) + for i in range(len(runs)): + rid = runs[i].run_id + status = runs[i].status + start_time = runs[i].start_time + end_time = runs[i].end_time + recorder = MLflowRecorder(f"Recorder_{i+1}", self.id, self._uri) + recorder.id = rid + recorder.status = status + recorder.start_time = ( + datetime.fromtimestamp(float(start_time) / 1000.0).strftime("%Y-%m-%d %H:%M:%S") + if start_time is not None + else None + ) + recorder.end_time = ( + datetime.fromtimestamp(float(end_time) / 1000.0).strftime("%Y-%m-%d %H:%M:%S") + if end_time is not None + else None + ) + recorder._uri = self._uri + recorders[rid] = recorder + + return recorders diff --git a/qlib/workflow/expm.py b/qlib/workflow/expm.py index f597d4a96..ebf6aeb7f 100644 --- a/qlib/workflow/expm.py +++ b/qlib/workflow/expm.py @@ -18,8 +18,9 @@ class ExpManager: (The link: https://mlflow.org/docs/latest/python_api/mlflow.html) """ - def __init__(self): - self.uri = None + def __init__(self, uri, default_exp_name): + self.uri = uri + self.default_exp_name = default_exp_name self.active_experiment = None # only one experiment can running each time self.experiments = dict() # store the experiment name --> Experiment object @@ -39,6 +40,7 @@ class ExpManager: controls whether run is nested in parent run. Returns + ------- An active recorder. """ raise NotImplementedError(f"Please implement the `start_exp` method.") @@ -112,7 +114,7 @@ class ExpManager: """ raise NotImplementedError(f"Please implement the `get_exp` method.") - def delete_exp(self, experiment_id): + def delete_exp(self, experiment_id=None, experiment_name=None): """ Delete an experiment. @@ -120,41 +122,51 @@ class ExpManager: ---------- experiment_id : str the experiment id. + experiment_name : str + the experiment name. """ - raise NotImplementedError(f"Please implement the `create_exp` method.") + raise NotImplementedError(f"Please implement the `delete_exp` method.") def get_uri(self): """ Get the default tracking URI or current URI. - Parameters - ---------- - Returns ------- The tracking URI string. """ return self.uri + def list_experiments(self): + """ + List all the existing experiments. + + Returns + ------- + A dictionary (name -> experiment) of experiments information that being stored. + """ + raise NotImplementedError(f"Please implement the `list_experiments` method.") + class MLflowExpManager(ExpManager): """ Use mlflow to implement ExpManager. """ - def __init__(self): - super(MLflowExpManager, self).__init__() - self.uri = None + def __init__(self, uri, default_exp_name): + super(MLflowExpManager, self).__init__(uri, default_exp_name) + self._total_exps = 0 + # get all the exps + self.experiments = self.list_experiments() def start_exp(self, experiment_name=None, uri=None): # create experiment experiment = self.create_exp(experiment_name, uri) # set up active experiment self.active_experiment = experiment - # store the experiment - self.experiments[experiment_name] = experiment # start the experiment self.active_experiment.start() + self._total_exps += 1 # update exp num return self.active_experiment @@ -162,10 +174,9 @@ class MLflowExpManager(ExpManager): if self.active_experiment is not None: self.active_experiment.end(status) self.active_experiment = None + self._total_exps -= 1 def create_exp(self, experiment_name=None, uri=None): - # init experiment - experiment = MLflowExperiment() # set the tracking uri if uri is None: logger.info( @@ -176,15 +187,19 @@ class MLflowExpManager(ExpManager): mlflow.set_tracking_uri(self.uri) # start the experiment if experiment_name is None: - logger.info("No experiment name provided. The default experiment name is set as `experiment`.") - experiment_id = mlflow.create_experiment("experiment") + logger.info( + f"No experiment name provided. The default experiment name is set as `{self.default_exp_name}`." + ) + experiment_id = mlflow.create_experiment(self.default_exp_name) # set the active experiment - mlflow.set_experiment("experiment") - experiment_name = "experiment" + mlflow.set_experiment(self.default_exp_name) + experiment_name = self.default_exp_name else: if experiment_name not in self.experiments: if mlflow.get_experiment_by_name(experiment_name) is not None: - logger.info("The experiment has already been created before. Try to resume the experiment...") + logger.info( + "The experiment has already been created before. Try to resume the experiment with a new recorder..." + ) experiment_id = mlflow.get_experiment_by_name(experiment_name).experiment_id else: experiment_id = mlflow.create_experiment(experiment_name) @@ -193,9 +208,11 @@ class MLflowExpManager(ExpManager): experiment = self.experiments[experiment_name] # set the active experiment mlflow.set_experiment(experiment_name) - # set up experiment - experiment.id = experiment_id - experiment.name = experiment_name + # init experiment + experiment = MLflowExperiment(experiment_id, experiment_name, self.uri) + experiment._default_name = self.default_exp_name + # store the experiment + self.experiments[experiment_name] = experiment return experiment @@ -206,19 +223,73 @@ class MLflowExpManager(ExpManager): order_by = kwargs.get("order_by") return mlflow.search_runs(experiment_ids, filter_string, run_view_type, max_results, order_by) - def get_exp(self, experiment_id=None, experiment_name=None): - if experiment_name is not None: - return self.experiments[experiment_name] - elif experiment_id is not None: - for name in self.experiments: - if self.experiments[name].id == experiment_id: - return self.experiments[name] - elif self.active_experiment is None: - raise Exception("No valid active experiment exists. Please make sure experiment manager is running.") + def get_exp(self, experiment_id=None, experiment_name=None, create=True): + if experiment_id is None and experiment_name is None: + if self.active_experiment: + return self.active_experiment + else: + if create: + logger.warning("QlibRecorder is not running. Use the Default experiment for further process.") + return self.start_exp() + else: + raise Exception( + "Something went wrong when retrieving experiments. Please check if QlibRecorder is running or the name/id of the experiment is correct." + ) else: - logger.info("No experiment id or name is given. Return the current active experiment.") - return self.active_experiment + if experiment_name is not None: + if experiment_name in self.experiments: + return self.experiments[experiment_name] + else: + if create: + logger.warning( + f"No valid experiment found. Create experiment with name {experiment_name} for further process." + ) + return self.start_exp(experiment_name) + else: + raise Exception( + "Something went wrong when retrieving experiments. Please check if QlibRecorder is running or the name/id of the experiment is correct." + ) + else: + for name in self.experiments: + if self.experiments[name].id == experiment_id: + return self.experiments[name] + if create: + logger.warning(f"No valid experiment found. Use the Default experiment for further process.") + return self.start_exp() + else: + raise Exception( + "Something went wrong when retrieving experiments. Please check if QlibRecorder is running or the name/id of the experiment is correct." + ) - def delete_exp(self, experiment_id): - mlflow.delete_experiment(experiment_id) - self.experiments = {key: val for key, val in self.experiments.items() if val.id != experiment_id} + def delete_exp(self, experiment_id=None, experiment_name=None): + assert ( + experiment_id is not None or experiment_name is not None + ), "Please input a valid experiment id or name before deleting." + try: + if experiment_id is not None: + mlflow.delete_experiment(experiment_id) + self.experiments = {key: val for key, val in self.experiments.items() if val.id != experiment_id} + else: + experiment_id = self.experiments[experiment_name].id + mlflow.delete_experiment(experiment_id) + except: + raise Exception( + "Something went wrong when deleting experiment. Please check if the name/id of the experiment is correct." + ) + + def list_experiments(self): + # retrieve all the existing experiments + client = mlflow.tracking.MlflowClient(tracking_uri=self.uri) + exps = client.list_experiments() + experiments = dict() + self._total_exps = len(exps) + for i in range(len(exps)): + eid = exps[i].experiment_id + ename = exps[i].name + experiment = MLflowExperiment(eid, ename, self.uri) + experiment.id = eid + experiment.name = ename + experiment._uri = self.uri + experiments[ename] = experiment + + return experiments diff --git a/qlib/workflow/recorder.py b/qlib/workflow/recorder.py index 68ce5432b..1adaa3f8a 100644 --- a/qlib/workflow/recorder.py +++ b/qlib/workflow/recorder.py @@ -2,7 +2,7 @@ # Licensed under the MIT License. import mlflow -import shutil, os, pickle, tempfile, codecs +import shutil, os, pickle, tempfile, codecs, datetime from pathlib import Path from ..utils.objm import FileManager @@ -19,6 +19,8 @@ class Recorder: self.id = None self.name = name self.experiment_id = experiment_id + self.start_time = None + self.end_time = None self.status = "SCHEDULED" def __repr__(self): @@ -34,7 +36,10 @@ class Recorder: output["id"] = self.id output["name"] = self.name output["experiment_id"] = self.experiment_id + output["start_time"] = self.start_time + output["end_time"] = self.end_time output["status"] = self.status + return output def set_recorder_name(self, rname): self.recorder_name = rname @@ -78,9 +83,6 @@ class Recorder: Start running or resuming the Recorder. The return value can be used as a context manager within a `with` block; otherwise, you must call end_run() to terminate the current run. (See `ActiveRun` class in mlflow) - Parameters - ---------- - Returns ------- An active running object (e.g. mlflow.ActiveRun object). @@ -139,7 +141,7 @@ class Recorder: def list_artifacts(self, artifact_path=None): """ - Delete some tags from a run. + List all the artifacts of a recorder. Parameters ---------- @@ -161,10 +163,13 @@ class MLflowRecorder(Recorder): use file manager to help maintain the objects in the project. """ - def __init__(self, name, experiment_id): + def __init__(self, name, experiment_id, uri): super(MLflowRecorder, self).__init__(name, experiment_id) - self.fm = None - self.temp_dir = None + self._uri = uri + self.artifact_uri = None + # set up file manager for saving objects + self.temp_dir = tempfile.mkdtemp() + self.fm = FileManager(Path(self.temp_dir).absolute()) def start_run(self): # start the run @@ -172,19 +177,21 @@ class MLflowRecorder(Recorder): # save the run id and artifact_uri self.id = run.info.run_id self.artifact_uri = run.info.artifact_uri - self._uri = mlflow.get_tracking_uri() # Fix!!! : this is not proper to have uri in recorder - # set up file manager for saving objects - self.temp_dir = tempfile.mkdtemp() - self.fm = FileManager(Path(self.temp_dir).absolute()) + self.start_time = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") self.status = "RUNNING" + return run def end_run(self, status): + assert status in ["SCHEDULED", "RUNNING", "FINISHED", "FAILED"], f"The status type {status} is not supported." mlflow.end_run(status) - self.status = status + self.end_time = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") + if self.status is not "FINISHED": + self.status = status shutil.rmtree(self.temp_dir) def save_objects(self, data_name_list=None, local_path=None, artifact_path=None, **kwargs): + assert self._uri is not None, "Please start the experiment and recorder first before using recorder directly." client = mlflow.tracking.MlflowClient(tracking_uri=self._uri) if local_path is not None: client.log_artifacts(self.id, local_path, artifact_path) @@ -200,6 +207,7 @@ class MLflowRecorder(Recorder): raise Exception("Please provide valid arguments in order to save object properly.") def load_object(self, name): + assert self._uri is not None, "Please start the experiment and recorder first before using recorder directly." client = mlflow.tracking.MlflowClient(tracking_uri=self._uri) path = client.download_artifacts(self.id, name) try: @@ -235,12 +243,16 @@ class MLflowRecorder(Recorder): for count, key in enumerate(keys): mlflow.delete_tag(key) - def get_artifact_uri(self, artifact_path=None): + def get_artifact_uri(self): if self.artifact_uri is not None: return self.artifact_uri - return mlflow.get_artifact_uri(artifact_path) + else: + raise Exception( + "Please make sure the recorder has been created and started properly before getting artifact uri." + ) def list_artifacts(self, artifact_path=None): + assert self._uri is not None, "Please start the experiment and recorder first before using recorder directly." client = mlflow.tracking.MlflowClient(tracking_uri=self._uri) artifacts = client.list_artifacts(self.id, artifact_path) return artifacts From 7a79028a720bdccc4f9971212a24c1cd44659523 Mon Sep 17 00:00:00 2001 From: Young Date: Sat, 14 Nov 2020 08:23:19 +0000 Subject: [PATCH 045/241] fix some small bug --- qlib/data/data.py | 6 ++++-- qlib/data/dataset/handler.py | 9 +++++---- qlib/data/dataset/utils.py | 2 +- qlib/utils/__init__.py | 24 +++++++++++++++++++++++- qlib/workflow/expm.py | 10 ++++------ qlib/workflow/recorder.py | 6 +++--- 6 files changed, 40 insertions(+), 17 deletions(-) diff --git a/qlib/data/data.py b/qlib/data/data.py index 6298cfa85..8331b1802 100644 --- a/qlib/data/data.py +++ b/qlib/data/data.py @@ -664,9 +664,11 @@ class LocalExpressionProvider(ExpressionProvider): lft_etd, rght_etd = expression.get_extended_window_size() series = expression.load(instrument, max(0, start_index - lft_etd), end_index + rght_etd, freq) # Ensure that each column type is consistent - # FIXME: The stock data is currently float. If there is other types of data, this part needs to be re-implemented. + # FIXME: + # 1) The stock data is currently float. If there is other types of data, this part needs to be re-implemented. + # 2) The the precision should be configurable try: - series = series.astype(float) + series = series.astype(np.float32) except ValueError: pass if not series.empty: diff --git a/qlib/data/dataset/handler.py b/qlib/data/dataset/handler.py index f6c097d22..9812864af 100644 --- a/qlib/data/dataset/handler.py +++ b/qlib/data/dataset/handler.py @@ -99,10 +99,11 @@ class DataHandler(Serializable): self._data = self.data_loader.load(self.instruments, self.start_time, self.end_time) # TODO: cache - CS_ALL = "__all" + CS_ALL = "__all" # return all columns with single-level index column + CS_RAW = "__raw" # return raw data with multi-level index column def _fetch_df_by_col(self, df: pd.DataFrame, col_set: str) -> pd.DataFrame: - if not isinstance(df.columns, pd.MultiIndex): + if not isinstance(df.columns, pd.MultiIndex) or col_set == self.CS_RAW: return df elif col_set == self.CS_ALL: return df.droplevel(axis=1, level=0) @@ -111,7 +112,7 @@ class DataHandler(Serializable): def fetch( self, - selector: Union[pd.Timestamp, slice, str], + selector: Union[pd.Timestamp, slice, str] = slice(None, None), level: Union[str, int] = "datetime", col_set: Union[str, List[str]] = CS_ALL, squeeze: bool = False, @@ -371,7 +372,7 @@ class DataHandlerLP(DataHandler): def fetch( self, - selector: Union[pd.Timestamp, slice, str], + selector: Union[pd.Timestamp, slice, str] = slice(None, None), level: Union[str, int] = "datetime", col_set=DataHandler.CS_ALL, data_key: str = DK_I, diff --git a/qlib/data/dataset/utils.py b/qlib/data/dataset/utils.py index 6eb00ffee..d82a2d5b5 100644 --- a/qlib/data/dataset/utils.py +++ b/qlib/data/dataset/utils.py @@ -52,4 +52,4 @@ def fetch_df_by_index( idx_slc = (selector, slice(None, None)) if get_level_index(df, level) == 1: idx_slc = idx_slc[1], idx_slc[0] - return df.loc(axis=0)[idx_slc] + return df.loc[pd.IndexSlice[idx_slc], ] # This could be faster than df.loc(axis=0)[idx_slc] diff --git a/qlib/utils/__init__.py b/qlib/utils/__init__.py index d9ae98bd5..c469829d2 100644 --- a/qlib/utils/__init__.py +++ b/qlib/utils/__init__.py @@ -185,7 +185,7 @@ def get_cls_kwargs(config: Union[dict, str], module) -> (type, dict): if isinstance(config, dict): # raise AttributeError klass = getattr(module, config["class"]) - kwargs = config["kwargs"] + kwargs = config.get("kwargs", {}) elif isinstance(config, str): klass = getattr(module, config) kwargs = {} @@ -619,6 +619,28 @@ def exists_qlib_data(qlib_dir): return True +def lexsort_index(df: pd.DataFrame) -> pd.DataFrame: + """ + make the df index lexsorted + + df.sort_index() will take a lot of time even when `df.is_lexsorted() == True` + This function could avoid such case + + Parameters + ---------- + df : pd.DataFrame + + Returns + ------- + pd.DataFrame: + sorted dataframe + """ + if df.index.is_lexsorted(): + return df + else: + return df.sort_index() + + #################### Wrapper ##################### class Wrapper(object): """Data Provider Wrapper""" diff --git a/qlib/workflow/expm.py b/qlib/workflow/expm.py index ebf6aeb7f..04f9c080f 100644 --- a/qlib/workflow/expm.py +++ b/qlib/workflow/expm.py @@ -14,7 +14,7 @@ logger = get_module_logger("workflow", "INFO") class ExpManager: """ - This is the `ExpManager` class for managing the experiments. The API is designed similar to mlflow. + This is the `ExpManager` class for managing experiments. The API is designed similar to mlflow. (The link: https://mlflow.org/docs/latest/python_api/mlflow.html) """ @@ -34,10 +34,6 @@ class ExpManager: name of the active experiment. uri : str the current tracking URI. - artifact_location : str - the location to store all the artifacts. - nested : boolean - controls whether run is nested in parent run. Returns ------- @@ -99,7 +95,7 @@ class ExpManager: """ raise NotImplementedError(f"Please implement the `create_exp` method.") - def get_exp(self, experiment_id=None, experiment_name=None): + def get_exp(self, experiment_id=None, experiment_name=None, create: bool = True): """ Retrieve an experiment by experiment_id from the backend store. @@ -107,6 +103,8 @@ class ExpManager: ---------- experiment_id : str the experiment id to return. + create : boolean + create the experiment if it does not exists Returns ------- diff --git a/qlib/workflow/recorder.py b/qlib/workflow/recorder.py index 1adaa3f8a..e5ea8d07a 100644 --- a/qlib/workflow/recorder.py +++ b/qlib/workflow/recorder.py @@ -139,13 +139,13 @@ class Recorder: """ raise NotImplementedError(f"Please implement the `delete_tags` method.") - def list_artifacts(self, artifact_path=None): + def list_artifacts(self, artifact_path: str = None): """ List all the artifacts of a recorder. Parameters ---------- - artifact_path=None : str + artifact_path : str the relative path for the artifact to be stored in the URI. Returns @@ -186,7 +186,7 @@ class MLflowRecorder(Recorder): assert status in ["SCHEDULED", "RUNNING", "FINISHED", "FAILED"], f"The status type {status} is not supported." mlflow.end_run(status) self.end_time = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") - if self.status is not "FINISHED": + if self.status != "FINISHED": self.status = status shutil.rmtree(self.temp_dir) From 42867264f3e09801ff628f86023d3db639c072f9 Mon Sep 17 00:00:00 2001 From: Jactus Date: Mon, 16 Nov 2020 15:49:50 +0800 Subject: [PATCH 046/241] Update R and workflow --- docs/start/initialization.rst | 5 ++ examples/workflow_by_code.py | 81 +++++++---------- qlib/__init__.py | 27 ++++-- qlib/config.py | 4 +- qlib/contrib/model/pytorch_nn.py | 24 +----- qlib/data/dataset/utils.py | 4 +- qlib/workflow/__init__.py | 53 +++++------- qlib/workflow/exp.py | 105 +++++++++++++---------- qlib/workflow/expm.py | 143 ++++++++++++++++--------------- qlib/workflow/record_temp.py | 17 +++- qlib/workflow/recorder.py | 78 ++++++++--------- qlib/workflow/utils.py | 33 +++++++ scripts/data_collector/utils.py | 2 +- 13 files changed, 301 insertions(+), 275 deletions(-) create mode 100644 qlib/workflow/utils.py diff --git a/docs/start/initialization.rst b/docs/start/initialization.rst index e34ab82fe..cffe11f52 100644 --- a/docs/start/initialization.rst +++ b/docs/start/initialization.rst @@ -58,3 +58,8 @@ Besides `provider_uri` and `region`, `qlib.init` has other parameters. The follo .. note:: If Qlib fails to connect redis via `redis_host` and `redis_port`, cache mechanism will not be used! Please refer to `Cache <../component/data.html#cache>`_ for details. +- `exp_manager` + Type: str, optional parameter(default: "MLflowExpManager"), the experiment manager to be used in qlib. +- `exp_uri` + Type: str, optional parameter(default: "mlruns" in local execution path), the tracking uri of the experiment manager. + It can either be a local path or a remote uri. \ No newline at end of file diff --git a/examples/workflow_by_code.py b/examples/workflow_by_code.py index a959d6ea1..98cd1f928 100644 --- a/examples/workflow_by_code.py +++ b/examples/workflow_by_code.py @@ -14,10 +14,9 @@ from qlib.contrib.evaluate import ( backtest as normal_backtest, risk_analysis, ) -from qlib.utils import exists_qlib_data - -# from qlib.model.learner import train_model -from qlib.utils import init_instance_by_config +from qlib.utils import exists_qlib_data, init_instance_by_config +from qlib.workflow import R +from qlib.workflow.record_temp import SignalRecord, PortAnaRecord if __name__ == "__main__": @@ -93,55 +92,41 @@ if __name__ == "__main__": ), }, }, - } + }, # You shoud record the data in specific sequence - # "record": ['SignalRecord', 'SigAnaRecord', 'PortAnaRecord'], + "record": ["SignalRecord", "PortAnaRecord"], } - # model = train_model(task) + port_analysis_config = { + "strategy": { + "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, + }, + } + + # model initiaiton model = init_instance_by_config(task["model"]) dataset = init_instance_by_config(task["dataset"]) - model.fit(dataset) + # start exp + with R.start("workflow"): + model.fit(dataset) - pred_score = model.predict(dataset) + # prediction + recorder = R.get_recorder() + sr = SignalRecord(model, dataset, recorder) + sr.generate() - # save pred_score to file - pred_score_path = Path("~/tmp/qlib/pred_score.pkl").expanduser() - pred_score_path.parent.mkdir(exist_ok=True, parents=True) - pred_score.to_pickle(pred_score_path) - - ################################### - # backtest - ################################### - STRATEGY_CONFIG = { - "topk": 50, - "n_drop": 5, - } - BACKTEST_CONFIG = { - "verbose": False, - "limit_threshold": 0.095, - "account": 100000000, - "benchmark": BENCHMARK, - "deal_price": "close", - "open_cost": 0.0005, - "close_cost": 0.0015, - "min_cost": 5, - } - - # use default strategy - # custom Strategy, refer to: TODO: Strategy API url - strategy = TopkDropoutStrategy(**STRATEGY_CONFIG) - report_normal, positions_normal = normal_backtest(pred_score, strategy=strategy, **BACKTEST_CONFIG) - - ################################### - # analyze - # If need a more detailed analysis, refer to: examples/train_and_bakctest.ipynb - ################################### - 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 - print(analysis_df) + # backtest + par = PortAnaRecord(recorder, port_analysis_config) + par.generate() diff --git a/qlib/__init__.py b/qlib/__init__.py index 7fe7e79dc..c9159dadf 100644 --- a/qlib/__init__.py +++ b/qlib/__init__.py @@ -5,17 +5,19 @@ __version__ = "0.5.1.dev0" import os -import copy -import logging import re -import subprocess -import platform +import sys +import copy import yaml import atexit +import signal +import logging +import platform +import subprocess from pathlib import Path from .utils import can_use_cache, init_instance_by_config, get_module_by_module_path - +from .workflow.utils import experiment_exception_hook, experiment_kill_signal_handler # init qlib def init(default_conf="client", **kwargs): @@ -44,9 +46,14 @@ def init(default_conf="client", **kwargs): C.set_region(kwargs.get("region", C["region"] if "region" in C else REG_CN)) for k, v in kwargs.items(): - C[k] = v - if k not in C: - LOG.warning("Unrecognized config %s" % k) + if k == "exp_manager": + C["exp_manager"].update({"class": v}) + elif k == "exp_uri": + C["exp_manager"]["kwargs"].update({"uri": v}) + else: + C[k] = v + if k not in C: + LOG.warning("Unrecognized config %s" % k) C.resolve_path() @@ -86,7 +93,9 @@ def init(default_conf="client", **kwargs): qr = QlibRecorder(exp_manager) R.register(qr) # clean up experiment when python program ends - atexit.register(R.end_exp, status="FAILED") # will not take effect if experiment ends + atexit.register(R.end_exp, recorder_status="FINISHED") # will not take effect if experiment ends + signal.signal(signal.SIGINT, experiment_kill_signal_handler) + sys.excepthook = experiment_exception_hook def _mount_nfs_uri(C): diff --git a/qlib/config.py b/qlib/config.py index 0557908ee..002134f9d 100644 --- a/qlib/config.py +++ b/qlib/config.py @@ -222,7 +222,9 @@ class QlibConfig(Config): def get_uri_type(self): is_win = re.match("^[a-zA-Z]:.*", self["provider_uri"]) is not None # such as 'C:\\data', 'D:' - is_nfs_or_win = re.match("^[^/]+:.+", self["provider_uri"]) is not None # such as 'host:/data/' (User may define short hostname by themselves or use localhost) + is_nfs_or_win = ( + re.match("^[^/]+:.+", self["provider_uri"]) is not None + ) # such as 'host:/data/' (User may define short hostname by themselves or use localhost) if is_nfs_or_win and not is_win: return QlibConfig.NFS_URI diff --git a/qlib/contrib/model/pytorch_nn.py b/qlib/contrib/model/pytorch_nn.py index 1835fb617..82b7d0950 100644 --- a/qlib/contrib/model/pytorch_nn.py +++ b/qlib/contrib/model/pytorch_nn.py @@ -161,7 +161,7 @@ class DNNModelPytorch(Model): try: wdf_train, wdf_valid = dataset.prepare(["train", "valid"], col_set=["weight"], data_key=DataHandlerLP.DK_L) w_train, w_valid = wdf_train["weight"], wdf_valid["weight"] - except: + except KeyError as e: w_train = pd.DataFrame(np.ones_like(y_train.values), index=y_train.index) w_valid = pd.DataFrame(np.ones_like(y_valid.values), index=y_valid.index) @@ -287,20 +287,6 @@ class DNNModelPytorch(Model): preds = self.dnn_model(x_test).detach().numpy() return pd.Series(np.squeeze(preds), index=x_test_pd.index) - def score(self, x_test, y_test, w_test=None): - # Remove rows from x, y and w, which contain Nan in any columns in y_test. - df_test = dataset.prepare("test", col_set=["feature", "label"]) - x_test, y_test = df_test["feature"], df_test["label"] - x_test, y_test, w_test = drop_nan_by_y_index(x_test, y_test, w_test) - preds = self.predict(x_test) - try: - df_test = dataset.prepare("test", col_set=["weight"]) - w_test = df_test["weight"] - w_test_weight = w_test.values - except: - w_test_weight = None - return self._scorer(y_test.values, preds, sample_weight=w_test_weight) - def save(self, filename, **kwargs): with save_multiple_parts_file(filename) as model_dir: model_path = os.path.join(model_dir, os.path.split(model_dir)[-1]) @@ -318,14 +304,6 @@ class DNNModelPytorch(Model): self.dnn_model.load_state_dict(torch.load(_model_path)) self._fitted = True - def finetune(self, dataset, w_train=None, w_valid=None, **kwargs): - df_train, df_valid = dataset.prepare( - ["train", "valid"], col_set=["feature", "label"], data_key=DataHandlerLP.DK_L - ) - x_train, y_train = df_train["feature"], df_train["label"] - x_valid, y_valid = df_valid["feature"], df_valid["label"] - self.fit(x_train, y_train, x_valid, y_valid, w_train=w_train, w_valid=w_valid, **kwargs) - class AverageMeter(object): """Computes and stores the average and current value""" diff --git a/qlib/data/dataset/utils.py b/qlib/data/dataset/utils.py index d82a2d5b5..8ee199bc0 100644 --- a/qlib/data/dataset/utils.py +++ b/qlib/data/dataset/utils.py @@ -52,4 +52,6 @@ def fetch_df_by_index( idx_slc = (selector, slice(None, None)) if get_level_index(df, level) == 1: idx_slc = idx_slc[1], idx_slc[0] - return df.loc[pd.IndexSlice[idx_slc], ] # This could be faster than df.loc(axis=0)[idx_slc] + return df.loc[ + pd.IndexSlice[idx_slc], + ] # This could be faster than df.loc(axis=0)[idx_slc] diff --git a/qlib/workflow/__init__.py b/qlib/workflow/__init__.py index 978e45c27..457dc4acd 100644 --- a/qlib/workflow/__init__.py +++ b/qlib/workflow/__init__.py @@ -3,6 +3,7 @@ from contextlib import contextmanager from .expm import MLflowExpManager +from .recorder import Recorder from ..utils import Wrapper @@ -31,7 +32,7 @@ class QlibRecorder: self.exp_manager = exp_manager @contextmanager - def start(self, experiment_name): + def start(self, experiment_name=None): """ Method to start an experiment. This method can only be called within a Python's `with` statement. @@ -53,13 +54,13 @@ class QlibRecorder: try: yield run except Exception as e: - self.end_exp("FAILED") # end the experiment if something went wrong + self.end_exp(Recorder.STATUS_FA) # end the experiment if something went wrong raise e - self.end_exp("FINISHED") + self.end_exp(Recorder.STATUS_FI) def start_exp(self, experiment_name=None, uri=None): """ - Lower leverl method for starting an experiment. When use this method, one should end the experiment manually + Lower level method for starting an experiment. When use this method, one should end the experiment manually and the status of the recorder may not be handled properly. Use case: @@ -67,7 +68,7 @@ class QlibRecorder: ``` R.start_exp(experiment_name='test') ... # further operations - R.end_exp('FINISHED') + R.end_exp('FINISHED') or R.end_exp(Recorder.STATUS_S) ``` Parameters @@ -83,7 +84,7 @@ class QlibRecorder: """ return self.exp_manager.start_exp(experiment_name, uri) - def end_exp(self, status): + def end_exp(self, recorder_status=Recorder.STATUS_FI): """ Method for ending an experiment manually. It will end the current active experiment, as well as its active recorder with the specified `status` type. @@ -93,7 +94,7 @@ class QlibRecorder: ``` R.start_exp(experiment_name='test') ... # further operations - R.end_exp('FINISHED') + R.end_exp('FINISHED') or R.end_exp(Recorder.STATUS_S) ``` Parameters @@ -101,7 +102,7 @@ class QlibRecorder: status : str The status of a recorder, which can be SCHEDULED, RUNNING, FINISHED, FAILED. """ - self.exp_manager.end_exp(status) + self.exp_manager.end_exp(recorder_status) def search_records(self, experiment_ids, **kwargs): """ @@ -175,7 +176,7 @@ class QlibRecorder: """ return self.get_exp(experiment_id, experiment_name).list_recorders() - def get_exp(self, experiment_id=None, experiment_name=None, create=True): + def get_exp(self, experiment_id=None, experiment_name=None, create: bool = True): """ Method for retrieving an experiment with given id or name. Once the `create` argument is set to True, if no valid experiment is found, this method will create one for you. Otherwise, it will @@ -185,18 +186,18 @@ class QlibRecorder: If R's running: 1) no id or name specified, return the active experiment. 2) if id or name is specified, return the specified experiment. If no such exp found, - create a new experiment with given id or name. + create a new experiment with given id or name, and the experiment is set to be running. If R's not running: 1) no id or name specified, create a default experiment. 2) if id or name is specified, return the specified experiment. If no such exp found, - create a new experiment with given id or name. + create a new experiment with given id or name, and the experiment is set to be running. Else If `create` is False: If R's running: 1) no id or name specified, return the active experiment. 2) if id or name is specified, return the specified experiment. If no such exp found, raise Error. If R's not running: - 1) no id or name specified, raise Error. + 1) no id or name specified. If the default experiment exists, return it, otherwise, raise Error. 2) if id or name is specified, return the specified experiment. If no such exp found, raise Error. @@ -219,7 +220,7 @@ class QlibRecorder: exp = R.get_exp(experiment_name='test') # Case 5 - exp = R.get_exp(create=False) -> Error + exp = R.get_exp(create=False) -> the default experiment if exists. ``` Parameters @@ -229,7 +230,8 @@ class QlibRecorder: experiment_name : str name of the experiment. create : boolean - decide whether to create an default experiment. + an argument determines whether the method will automatically create a new experiment + according to user's specification if the experiment hasn't been created before. Returns ------- @@ -348,7 +350,8 @@ class QlibRecorder: def save_objects(self, local_path=None, artifact_path=None, **kwargs): """ Method for saving objects as artifacts in the experiment to the uri. It supports either saving - from a local file/directory, or directly saving objects. + from a local file/directory, or directly saving objects. User can use valid python's keywords arguments + to specify the object to be saved as well as its name (name: value). If R's running: it will save the objects through the running recorder. If R's not running: the system will create a default experiment, and a new recorder and @@ -364,28 +367,16 @@ class QlibRecorder: # Case 1 with R.start('test'): pred = model.predict(dataset) - R.save_objects(data=pred, name='pred.pkl', artifact_path='prediction') + kwargs = {"pred.pkl": pred} + R.save_objects(**kwargs, artifact_path='prediction') # Case 2 - with R.start('test'): - pred1 = model1.predict(dataset) - pred2 = model2.predict(dataset) - dn_list = [(pred1, 'pred1.pkl'), (pred2, 'pred2.pkl')] - R.save_objects(data_name_list=dn_list) - - # Case 3 with R.start('test'): R.save_objects(local_path='results/pred.pkl') ``` Parameters ---------- - data : any type - the data to be saved. - name : str - name of the file to be saved. - data_name_list : list - list of (data, name) pairs local_path : str if provided, them save the file or directory to the artifact URI. artifact_path=None : str @@ -464,10 +455,10 @@ class QlibRecorder: ``` # Case 1 with R.start('test'): - R.set_tags(release_version=2.2.0) + R.set_tags(release_version="2.2.0") # Case 2 - R.set_tags(release_version=2.2.0) + R.set_tags(release_version="2.2.0") ``` Parameters diff --git a/qlib/workflow/exp.py b/qlib/workflow/exp.py index 005b113df..a32d33d57 100644 --- a/qlib/workflow/exp.py +++ b/qlib/workflow/exp.py @@ -4,7 +4,7 @@ import mlflow from datetime import datetime from pathlib import Path -from .recorder import MLflowRecorder +from .recorder import Recorder, MLflowRecorder from ..log import get_module_logger logger = get_module_logger("workflow", "INFO") @@ -20,7 +20,6 @@ class Experiment: self.id = id self.name = name self.active_recorder = None # only one recorder can running each time - self.recorders = dict() # recorder id -> object def __repr__(self): return str(self.info) @@ -30,31 +29,32 @@ class Experiment: @property def info(self): + recorders = self.list_recorders() output = dict() output["class"] = "Experiment" output["id"] = self.id output["name"] = self.name output["active_recorder"] = self.active_recorder.id if self.active_recorder is not None else None - output["recorders"] = list(self.recorders.keys()) + output["recorders"] = list(recorders.keys()) return output def start(self): """ - Start the experiment. + Start the experiment and set it to be active. This method will also start a new recorder. Returns ------- - A running recorder instance. + An active recorder. """ raise NotImplementedError(f"Please implement the `start` method.") - def end(self, status): + def end(self, recorder_status=Recorder.STATUS_S): """ End the experiment. Parameters ---------- - status : str + recorder_status : str the status the recorder to be set with when ending (SCHEDULED, RUNNING, FINISHED, FAILED). """ raise NotImplementedError(f"Please implement the `end` method.") @@ -72,17 +72,7 @@ class Experiment: def search_records(self, **kwargs): """ Get a pandas DataFrame of records that fit the search criteria of the experiment. - - Parameters - ---------- - filter_string : str - filter query string, defaults to searching all runs. - run_view_type : int - one of enum values ACTIVE_ONLY, DELETED_ONLY, or ALL (e.g. in mlflow.entities.ViewType). - max_results : int - the maximum number of runs to put in the dataframe. - order_by : list - list of columns to order by (e.g., “metrics.rmse”). + Inputs are the search critera user want to apply. Returns ------- @@ -104,9 +94,31 @@ class Experiment: """ raise NotImplementedError(f"Please implement the `delete_recorder` method.") - def get_recorder(self, recorder_id=None, recorder_name=None): + def get_recorder(self, recorder_id=None, recorder_name=None, create: bool = True): """ - Get the current active Recorder. + Retrieve a Recorder for user. When user specify recorder id and name, the method will try to return the + specific recorder. When user does not provide recorder id or name, the method will try to return the current + active recorder. The `create` argument determines whether the method will automatically create a new recorder + according to user's specification if the recorder hasn't been created before + + If `create` is True: + If R's running: + 1) no id or name specified, return the active recorder. + 2) if id or name is specified, return the specified recorder. If no such exp found, + create a new recorder with given id or name, and the recorder shoud be running. + If R's not running: + 1) no id or name specified, create a new recorder. + 2) if id or name is specified, return the specified experiment. If no such exp found, + create a new recorder with given id or name, and the recorder shoud be running. + Else If `create` is False: + If R's running: + 1) no id or name specified, return the active recorder. + 2) if id or name is specified, return the specified recorder. If no such exp found, + raise Error. + If R's not running: + 1) no id or name specified, raise Error. + 2) if id or name is specified, return the specified recorder. If no such exp found, + raise Error. Parameters ---------- @@ -140,32 +152,29 @@ class MLflowExperiment(Experiment): def __init__(self, id, name, uri): super(MLflowExperiment, self).__init__(id, name) self._uri = uri - self._total_recorders = 0 self._default_name = None + self.client = mlflow.tracking.MlflowClient(tracking_uri=self._uri) def start(self): - # get all the recorders of the experiment - self.recorders = self.list_recorders() + # set the active experiment + mlflow.set_experiment(self.name) + logger.info(f"Experiment {self.id} starts running ...") # set up recorder recorder = self.create_recorder() self.active_recorder = recorder # start the recorder run = self.active_recorder.start_run() - # store the recorder - self.recorders[self.active_recorder.id] = recorder - self._total_recorders += 1 # update recorder num - logger.info(f"Experiment {self.id} starts running ...") return self.active_recorder - def end(self, status): + def end(self, recorder_status): if self.active_recorder is not None: - self.active_recorder.end_run(status) + self.active_recorder.end_run(recorder_status) self.active_recorder = None - self._total_recorders -= 1 def create_recorder(self): - num = len(self.recorders) + recorders = self.list_recorders() + num = len(recorders) name = "Recorder_{}".format(num + 1) recorder = MLflowRecorder(name, self.id, self._uri) @@ -177,7 +186,7 @@ class MLflowExperiment(Experiment): max_results = 100000 if kwargs.get("max_results") is None else kwargs.get("max_results") order_by = kwargs.get("order_by") - return mlflow.search_runs([self.id], filter_string, run_view_type, max_results, order_by) + return self.client.search_runs([self.id], filter_string, run_view_type, max_results, order_by) def delete_recorder(self, recorder_id=None, recorder_name=None): assert ( @@ -185,20 +194,26 @@ class MLflowExperiment(Experiment): ), "Please input a valid recorder id or name before deleting." try: if recorder_id is not None: - mlflow.delete_run(recorder_id) - self.recorders = [r for r in self.recorders if r == recorder_id] + self.client.delete_run(recorder_id) else: - for r in self.recorders: - if self.recorders[r].name == recorder_name: + recorders = self.list_recorders() + for r in recorders: + if recorders[r].name == recorder_name: recorder_id = r break - mlflow.delete_run(recorder_id) + self.client.delete_run(recorder_id) except: raise Exception( "Something went wrong when deleting recorder. Please check if the name/id of the recorder is correct." ) def get_recorder(self, recorder_id=None, recorder_name=None, create=True): + """ + MLflow doesn't support create recorder with a specific id. Thus, when user only provides recorder id and `create` + is set to True, this method will not automatically create an active recorder. + """ + # retrive all the recorders under this experiment + recorders = self.list_recorders() if recorder_id is None and recorder_name is None: if self.active_recorder: return self.active_recorder @@ -215,19 +230,19 @@ class MLflowExperiment(Experiment): ) else: if recorder_id is not None: - if recorder_id in self.recorders: - return self.recorders[recorder_id] + if recorder_id in recorders: + return recorders[recorder_id] else: # mlflow does not support create a run with given id raise Exception( "Something went wrong when retrieving recorders. Please check if QlibRecorder is running or the name/id of the recorder is correct." ) else: - for rid in self.recorders: - if self.recorders[rid].name == recorder_name: - return self.recorders[rid] + for rid in recorders: + if recorders[rid].name == recorder_name: + return recorders[rid] if create: - self.recorders = self.list_recorders() + recorders = self.list_recorders() logger.warning(f"No valid recorder found. Create a new recorder with name {recorder_name}.") recorder = self.create_recorder() recorder.name = recorder_name @@ -239,10 +254,8 @@ class MLflowExperiment(Experiment): ) def list_recorders(self): - client = mlflow.tracking.MlflowClient(tracking_uri=self._uri) - runs = client.list_run_infos(self.id)[::-1] + runs = self.client.list_run_infos(self.id, run_view_type=1)[::-1] recorders = dict() - self._total_recorders = len(runs) for i in range(len(runs)): rid = runs[i].run_id status = runs[i].status diff --git a/qlib/workflow/expm.py b/qlib/workflow/expm.py index 04f9c080f..1fe345577 100644 --- a/qlib/workflow/expm.py +++ b/qlib/workflow/expm.py @@ -6,7 +6,7 @@ import os from pathlib import Path from contextlib import contextmanager from .exp import MLflowExperiment -from .recorder import MLflowRecorder +from .recorder import Recorder, MLflowRecorder from ..log import get_module_logger logger = get_module_logger("workflow", "INFO") @@ -22,11 +22,10 @@ class ExpManager: self.uri = uri self.default_exp_name = default_exp_name self.active_experiment = None # only one experiment can running each time - self.experiments = dict() # store the experiment name --> Experiment object def start_exp(self, experiment_name=None, uri=None, **kwargs): """ - Start running an experiment. + Start an experiment. Parameters ---------- @@ -37,11 +36,18 @@ class ExpManager: Returns ------- - An active recorder. + An active experiment. """ - raise NotImplementedError(f"Please implement the `start_exp` method.") + # create experiment + experiment = self.create_exp(experiment_name, uri) + # set up active experiment + self.active_experiment = experiment + # start the experiment + self.active_experiment.start() - def end_exp(self, **kwargs): + return self.active_experiment + + def end_exp(self, recorder_status: str = Recorder.STATUS_S, **kwargs): """ End an running experiment. @@ -49,25 +55,17 @@ class ExpManager: ---------- experiment_name : str name of the active experiment. + recorder_status : str + the status of the active recorder of the experiment. """ - raise NotImplementedError(f"Please implement the `end_exp` method.") + if self.active_experiment is not None: + self.active_experiment.end(recorder_status) + self.active_experiment = None def search_records(self, experiment_ids=None, **kwargs): """ - Get a pandas DataFrame of records that fit the search criteria. - - Parameters - ---------- - experiment_ids : list - list of experiment IDs. - filter_string : str - filter query string, defaults to searching all runs. - run_view_type : int - one of enum values ACTIVE_ONLY, DELETED_ONLY, or ALL (e.g. in mlflow.entities.ViewType). - max_results : int - the maximum number of runs to put in the dataframe. - order_by : list - list of columns to order by (e.g., “metrics.rmse”). + Get a pandas DataFrame of records that fit the search criteria of the experiment. + Inputs are the search critera user want to apply. Returns ------- @@ -78,7 +76,7 @@ class ExpManager: """ raise NotImplementedError(f"Please implement the `search_records` method.") - def create_exp(self, experiment_name, artifact_location=None): + def create_exp(self, experiment_name=None, uri=None): """ Create an experiment. @@ -86,8 +84,8 @@ class ExpManager: ---------- experiment_name : str the experiment name, which must be unique. - artifact_location : str - the location to store run artifacts. + uri : str + the tracking uri of the experiment. Returns ------- @@ -97,14 +95,36 @@ class ExpManager: def get_exp(self, experiment_id=None, experiment_name=None, create: bool = True): """ - Retrieve an experiment by experiment_id from the backend store. + Retrieve an experiment. When user specify experiment id and name, the method will try to return the + specific experiment. When user does not provide recorder id or name, the method will try to return the current + active experiment. The `create` argument determines whether the method will automatically create a new experiment + according to user's specification if the experiment hasn't been created before + + If `create` is True: + If R's running: + 1) no id or name specified, return the active experiment. + 2) if id or name is specified, return the specified experiment. If no such exp found, + create a new experiment with given id or name, and the experiment is set to be running. + If R's not running: + 1) no id or name specified, create a default experiment. + 2) if id or name is specified, return the specified experiment. If no such exp found, + create a new experiment with given id or name, and the experiment is set to be running. + Else If `create` is False: + If R's running: + 1) no id or name specified, return the active experiment. + 2) if id or name is specified, return the specified experiment. If no such exp found, + raise Error. + If R's not running: + 1) no id or name specified. If the default experiment exists, return it, otherwise, raise Error. + 2) if id or name is specified, return the specified experiment. If no such exp found, + raise Error. Parameters ---------- experiment_id : str the experiment id to return. create : boolean - create the experiment if it does not exists + create the experiment if hasn't been created before. Returns ------- @@ -153,28 +173,11 @@ class MLflowExpManager(ExpManager): def __init__(self, uri, default_exp_name): super(MLflowExpManager, self).__init__(uri, default_exp_name) - self._total_exps = 0 - # get all the exps - self.experiments = self.list_experiments() - - def start_exp(self, experiment_name=None, uri=None): - # create experiment - experiment = self.create_exp(experiment_name, uri) - # set up active experiment - self.active_experiment = experiment - # start the experiment - self.active_experiment.start() - self._total_exps += 1 # update exp num - - return self.active_experiment - - def end_exp(self, status): - if self.active_experiment is not None: - self.active_experiment.end(status) - self.active_experiment = None - self._total_exps -= 1 + self.client = mlflow.tracking.MlflowClient(tracking_uri=self.uri) def create_exp(self, experiment_name=None, uri=None): + # retrieve all created experiments + experiments = self.list_experiments() # set the tracking uri if uri is None: logger.info( @@ -188,29 +191,28 @@ class MLflowExpManager(ExpManager): logger.info( f"No experiment name provided. The default experiment name is set as `{self.default_exp_name}`." ) - experiment_id = mlflow.create_experiment(self.default_exp_name) + if self.default_exp_name not in experiments: + experiment_id = self.client.create_experiment(self.default_exp_name) + else: + experiment_id = self.client.get_experiment_by_name(self.default_exp_name).experiment_id # set the active experiment mlflow.set_experiment(self.default_exp_name) experiment_name = self.default_exp_name else: - if experiment_name not in self.experiments: - if mlflow.get_experiment_by_name(experiment_name) is not None: + if experiment_name not in experiments: + if self.client.get_experiment_by_name(experiment_name) is not None: logger.info( "The experiment has already been created before. Try to resume the experiment with a new recorder..." ) - experiment_id = mlflow.get_experiment_by_name(experiment_name).experiment_id + experiment_id = self.client.get_experiment_by_name(experiment_name).experiment_id else: - experiment_id = mlflow.create_experiment(experiment_name) + experiment_id = self.client.create_experiment(experiment_name) else: - experiment_id = self.experiments[experiment_name].id - experiment = self.experiments[experiment_name] - # set the active experiment - mlflow.set_experiment(experiment_name) + experiment_id = experiments[experiment_name].id + experiment = experiments[experiment_name] # init experiment experiment = MLflowExperiment(experiment_id, experiment_name, self.uri) experiment._default_name = self.default_exp_name - # store the experiment - self.experiments[experiment_name] = experiment return experiment @@ -219,9 +221,11 @@ class MLflowExpManager(ExpManager): run_view_type = 1 if kwargs.get("run_view_type") is None else kwargs.get("run_view_type") max_results = 100000 if kwargs.get("max_results") is None else kwargs.get("max_results") order_by = kwargs.get("order_by") - return mlflow.search_runs(experiment_ids, filter_string, run_view_type, max_results, order_by) + return self.client.search_runs(experiment_ids, filter_string, run_view_type, max_results, order_by) def get_exp(self, experiment_id=None, experiment_name=None, create=True): + # retrive all created experiments + experiments = self.list_experiments() if experiment_id is None and experiment_name is None: if self.active_experiment: return self.active_experiment @@ -230,13 +234,15 @@ class MLflowExpManager(ExpManager): logger.warning("QlibRecorder is not running. Use the Default experiment for further process.") return self.start_exp() else: + if self.default_exp_name in experiments: + return experiments[self.default_exp_name] raise Exception( "Something went wrong when retrieving experiments. Please check if QlibRecorder is running or the name/id of the experiment is correct." ) else: if experiment_name is not None: - if experiment_name in self.experiments: - return self.experiments[experiment_name] + if experiment_name in experiments: + return experiments[experiment_name] else: if create: logger.warning( @@ -248,9 +254,9 @@ class MLflowExpManager(ExpManager): "Something went wrong when retrieving experiments. Please check if QlibRecorder is running or the name/id of the experiment is correct." ) else: - for name in self.experiments: - if self.experiments[name].id == experiment_id: - return self.experiments[name] + for name in experiments: + if experiments[name].id == experiment_id: + return experiments[name] if create: logger.warning(f"No valid experiment found. Use the Default experiment for further process.") return self.start_exp() @@ -265,11 +271,10 @@ class MLflowExpManager(ExpManager): ), "Please input a valid experiment id or name before deleting." try: if experiment_id is not None: - mlflow.delete_experiment(experiment_id) - self.experiments = {key: val for key, val in self.experiments.items() if val.id != experiment_id} + self.client.delete_experiment(experiment_id) else: - experiment_id = self.experiments[experiment_name].id - mlflow.delete_experiment(experiment_id) + experiment = self.client.get_experiment_by_name(experiment_name) + self.client.delete_experiment(experiment.experiment_id) except: raise Exception( "Something went wrong when deleting experiment. Please check if the name/id of the experiment is correct." @@ -277,10 +282,8 @@ class MLflowExpManager(ExpManager): def list_experiments(self): # retrieve all the existing experiments - client = mlflow.tracking.MlflowClient(tracking_uri=self.uri) - exps = client.list_experiments() + exps = self.client.list_experiments(view_type=1) experiments = dict() - self._total_exps = len(exps) for i in range(len(exps)): eid = exps[i].experiment_id ename = exps[i].name diff --git a/qlib/workflow/record_temp.py b/qlib/workflow/record_temp.py index 3cef8b5d3..0c20f6efc 100644 --- a/qlib/workflow/record_temp.py +++ b/qlib/workflow/record_temp.py @@ -8,6 +8,9 @@ from ..contrib.evaluate import ( risk_analysis, ) from ..utils import init_instance_by_config, get_module_by_module_path +from ..log import get_module_logger + +logger = get_module_logger("workflow", "INFO") class RecordTemp: @@ -76,7 +79,10 @@ class SignalRecord(RecordTemp): def generate(self, **kwargs): # generate prediciton pred = self.model.predict(self.dataset) - self.recorder.save_objects(data=pred, name="pred.pkl") + self.recorder.save_objects(**{"pred.pkl": pred}) + logger.info( + f"Signal record 'pred.pkl' has been saved as the artifact of the Experiment {self.recorder.experiment_id}" + ) def load(self): # try to load the saved object @@ -133,8 +139,8 @@ 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) - self.recorder.save_objects(data=report_normal, name="report_normal.pkl", artifact_path=self.artifact_path) - self.recorder.save_objects(data=positions_normal, name="positions_normal.pkl", artifact_path=self.artifact_path) + self.recorder.save_objects(**{"report_normal.pkl": report_normal}, artifact_path=self.artifact_path) + self.recorder.save_objects(**{"positions_normal.pkl": positions_normal}, artifact_path=self.artifact_path) # analysis analysis = dict() @@ -143,7 +149,10 @@ class PortAnaRecord(SignalRecord): report_normal["return"] - report_normal["bench"] - report_normal["cost"] ) analysis_df = pd.concat(analysis) # type: pd.DataFrame - self.recorder.save_objects(data=analysis_df, name="port_analysis.pkl", artifact_path=self.artifact_path) + self.recorder.save_objects(**{"port_analysis.pkl": analysis_df}, artifact_path=self.artifact_path) + logger.info( + f"Portfolio analysis record 'port_analysis.pkl' has been saved as the artifact of the Experiment {self.recorder.experiment_id}" + ) def load(self): # try to load the saved object diff --git a/qlib/workflow/recorder.py b/qlib/workflow/recorder.py index e5ea8d07a..6c83641a9 100644 --- a/qlib/workflow/recorder.py +++ b/qlib/workflow/recorder.py @@ -5,6 +5,9 @@ import mlflow import shutil, os, pickle, tempfile, codecs, datetime from pathlib import Path from ..utils.objm import FileManager +from ..log import get_module_logger + +logger = get_module_logger("workflow", "INFO") class Recorder: @@ -15,13 +18,19 @@ class Recorder: The status of the recorder can be SCHEDULED, RUNNING, FINISHED, FAILED. """ + # status type + STATUS_S = "SCHEDULED" + STATUS_R = "RUNNING" + STATUS_FI = "FINISHED" + STATUS_FA = "FAILED" + def __init__(self, name, experiment_id): self.id = None self.name = name self.experiment_id = experiment_id self.start_time = None self.end_time = None - self.status = "SCHEDULED" + self.status = Recorder.STATUS_S def __repr__(self): return str(self.info) @@ -46,16 +55,11 @@ class Recorder: def save_objects(self, local_path=None, artifact_path=None, **kwargs): """ - Save objects such as prediction file or model checkpoints to the artifact URI. + Save objects such as prediction file or model checkpoints to the artifact URI. User + can save object through keywords arguments (name:value). Parameters ---------- - data : any type - the data to be saved. - name : str - name of the file to be saved. - data_name_list : list - list of (data, name) pairs local_path : str if provided, them save the file or directory to the artifact URI. artifact_path=None : str @@ -170,6 +174,7 @@ class MLflowRecorder(Recorder): # set up file manager for saving objects self.temp_dir = tempfile.mkdtemp() self.fm = FileManager(Path(self.temp_dir).absolute()) + self.client = mlflow.tracking.MlflowClient(tracking_uri=self._uri) def start_run(self): # start the run @@ -178,38 +183,36 @@ class MLflowRecorder(Recorder): self.id = run.info.run_id self.artifact_uri = run.info.artifact_uri self.start_time = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") - self.status = "RUNNING" + self.status = Recorder.STATUS_R + logger.info(f"Recorder {self.id} starts running under Experiment {self.experiment_id} ...") return run - def end_run(self, status): - assert status in ["SCHEDULED", "RUNNING", "FINISHED", "FAILED"], f"The status type {status} is not supported." + def end_run(self, status: str = Recorder.STATUS_S): + assert status in [ + Recorder.STATUS_S, + Recorder.STATUS_R, + Recorder.STATUS_FI, + Recorder.STATUS_FA, + ], f"The status type {status} is not supported." mlflow.end_run(status) self.end_time = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") - if self.status != "FINISHED": + if self.status != Recorder.STATUS_S: self.status = status shutil.rmtree(self.temp_dir) - def save_objects(self, data_name_list=None, local_path=None, artifact_path=None, **kwargs): + def save_objects(self, local_path=None, artifact_path=None, **kwargs): assert self._uri is not None, "Please start the experiment and recorder first before using recorder directly." - client = mlflow.tracking.MlflowClient(tracking_uri=self._uri) if local_path is not None: - client.log_artifacts(self.id, local_path, artifact_path) - elif kwargs.get("data") is not None and kwargs.get("name") is not None: - data, name = kwargs.get("data"), kwargs.get("name") - self.fm.save_obj(data, name) - client.log_artifact(self.id, self.fm.path / name, artifact_path) - elif kwargs.get("data_name_list") is not None: - data_name_list = kwargs.get("data_name_list") - self.fm.save_objs(data_name_list) - client.log_artifacts(self.id, self.fm.path, artifact_path) + self.client.log_artifacts(self.id, local_path, artifact_path) else: - raise Exception("Please provide valid arguments in order to save object properly.") + for name, data in kwargs.items(): + self.fm.save_obj(data, name) + self.client.log_artifact(self.id, self.fm.path / name, artifact_path) def load_object(self, name): assert self._uri is not None, "Please start the experiment and recorder first before using recorder directly." - client = mlflow.tracking.MlflowClient(tracking_uri=self._uri) - path = client.download_artifacts(self.id, name) + path = self.client.download_artifacts(self.id, name) try: with Path(path).open("rb") as f: f.seek(0) @@ -220,28 +223,22 @@ class MLflowRecorder(Recorder): def log_params(self, **kwargs): keys = list(kwargs.keys()) - if len(keys) == 0: - mlflow.log_param(keys[0], kwargs.get(keys[0])) - else: - mlflow.log_params(dict(kwargs)) + for name, data in kwargs.items(): + self.client.log_param(self.id, name, data) def log_metrics(self, step=None, **kwargs): keys = list(kwargs.keys()) - if len(keys) == 0: - mlflow.log_metric(keys[0], kwargs.get(keys[0])) - else: - mlflow.log_metrics(dict(kwargs)) + for name, data in kwargs.items(): + self.client.log_metric(self.id, name, data) def set_tags(self, **kwargs): keys = list(kwargs.keys()) - if len(keys) == 0: - mlflow.set_tag(keys[0], kwargs.get(keys[0])) - else: - mlflow.set_tags(dict(kwargs)) + for name, data in kwargs.items(): + self.client.set_tag(self.id, name, data) def delete_tags(self, *keys): for count, key in enumerate(keys): - mlflow.delete_tag(key) + self.client.delete_tag(self.id, key) def get_artifact_uri(self): if self.artifact_uri is not None: @@ -253,6 +250,5 @@ class MLflowRecorder(Recorder): def list_artifacts(self, artifact_path=None): assert self._uri is not None, "Please start the experiment and recorder first before using recorder directly." - client = mlflow.tracking.MlflowClient(tracking_uri=self._uri) - artifacts = client.list_artifacts(self.id, artifact_path) + artifacts = self.client.list_artifacts(self.id, artifact_path) return artifacts diff --git a/qlib/workflow/utils.py b/qlib/workflow/utils.py new file mode 100644 index 000000000..196669fea --- /dev/null +++ b/qlib/workflow/utils.py @@ -0,0 +1,33 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +import sys, traceback, signal +from . import R +from .recorder import Recorder +from ..log import get_module_logger + +logger = get_module_logger("workflow", "INFO") + + +def experiment_exception_hook(type, value, tb): + """ + End an experiment with status to be "FAILED". This exception tries to catch those uncaught exception + and end the experiment automatically. + + Parameters + type: Exception type + value: Exception's value + tb: Exception's traceback + """ + error_msg = "An exception has been raised.\n" f"Type: {type}\n" f"Value: {value}\n" + logger.error(error_msg) + traceback.print_tb(tb) + + R.end_exp(recorder_status=Recorder.STATUS_FA) + + +def experiment_kill_signal_handler(signum, frame): + """ + End an experiment when user kill the program (CTRL+C, etc.). + """ + R.end_exp(recorder_status=Recorder.STATUS_FA) diff --git a/scripts/data_collector/utils.py b/scripts/data_collector/utils.py index d2b3835c1..08fef7ec9 100644 --- a/scripts/data_collector/utils.py +++ b/scripts/data_collector/utils.py @@ -137,5 +137,5 @@ def symbol_prefix_to_sufix(symbol: str, capital: bool = True) -> str: return res.upper() if capital else res.lower() -if __name__ == '__main__': +if __name__ == "__main__": assert len(get_hs_stock_symbols()) >= MINIMUM_SYMBOLS_NUM From 3e04ded750e2735af05b54d100d58525083cf9b2 Mon Sep 17 00:00:00 2001 From: Jactus Date: Mon, 16 Nov 2020 17:29:26 +0800 Subject: [PATCH 047/241] Add initial workflow_by_config --- examples/workflow_by_code.py | 27 ++++++------------ examples/workflow_by_config.py | 49 ++++++++++++++++++++++++++++++++ examples/workflow_config.yaml | 52 ++++++++++++++++++++++++++++++++++ setup.py | 1 + 4 files changed, 110 insertions(+), 19 deletions(-) create mode 100644 examples/workflow_by_config.py create mode 100644 examples/workflow_config.yaml diff --git a/examples/workflow_by_code.py b/examples/workflow_by_code.py index 98cd1f928..cae890672 100644 --- a/examples/workflow_by_code.py +++ b/examples/workflow_by_code.py @@ -46,15 +46,6 @@ if __name__ == "__main__": "instruments": MARKET, } - TRAINER_CONFIG = { - "train_start_time": "2008-01-01", - "train_end_time": "2014-12-31", - "validate_start_time": "2015-01-01", - "validate_end_time": "2016-12-31", - "test_start_time": "2017-01-01", - "test_end_time": "2020-08-01", - } - task = { "model": { "class": "LGBModel", @@ -82,14 +73,8 @@ if __name__ == "__main__": }, "segments": { "train": ("2008-01-01", "2014-12-31"), - "valid": ( - "2015-01-01", - "2016-12-31", - ), - "test": ( - "2017-01-01", - "2020-08-01", - ), + "valid": ("2015-01-01", "2016-12-31"), + "test": ("2017-01-01", "2020-08-01"), }, }, }, @@ -99,8 +84,12 @@ if __name__ == "__main__": port_analysis_config = { "strategy": { - "topk": 50, - "n_drop": 5, + "class": "TopkDropoutStrategy", + "module_path": "qlib.contrib.strategy.strategy", + "kwargs": { + "topk": 50, + "n_drop": 5, + } }, "backtest": { "verbose": False, diff --git a/examples/workflow_by_config.py b/examples/workflow_by_config.py new file mode 100644 index 000000000..7955d29d0 --- /dev/null +++ b/examples/workflow_by_config.py @@ -0,0 +1,49 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +import sys +from pathlib import Path + +import qlib +import fire +import yaml +import pandas as pd +from qlib.config import REG_CN +from qlib.utils import exists_qlib_data, init_instance_by_config +from qlib.workflow import R +from qlib.workflow.record_temp import SignalRecord, PortAnaRecord + +# worflow handler function +def workflow(config_path): + with open(config_path) as fp: + config = yaml.load(fp, Loader=yaml.FullLoader) + + provider_uri = config.get("PROVIDER_URI") + 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_cn(target_dir=provider_uri) + + qlib.init(provider_uri=provider_uri, region=REG_CN) + + # model initiaiton + model = init_instance_by_config(config.get("TASK")["model"]) + dataset = init_instance_by_config(config.get("TASK")["dataset"]) + + # start exp + with R.start("workflow"): + model.fit(dataset) + + # prediction + recorder = R.get_recorder() + sr = SignalRecord(model, dataset, recorder) + sr.generate() + + # backtest + par = PortAnaRecord(recorder, config.get("PORT_ANALYSIS_CONFIG")) + par.generate() + +if __name__ == "__main__": + fire.Fire(workflow) \ No newline at end of file diff --git a/examples/workflow_config.yaml b/examples/workflow_config.yaml new file mode 100644 index 000000000..2698423df --- /dev/null +++ b/examples/workflow_config.yaml @@ -0,0 +1,52 @@ +PROVIDER_URI: "~/.qlib/qlib_data/cn_data" +MARKET: &market csi300 +BENCHMARK: &benchmark SH000300 +DATA_HANDLER_CONFIG: &data_handerler_config + start_time: 2008-01-01 + end_time: 2020-08-01 + fit_start_time: 2008-01-01 + fit_end_time: 2014-12-31 + 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_handerler_config + segments: + train: [2008-01-01, 2014-12-31] + valid: [2015-01-01, 2016-12-31] + test: [2017-01-01, 2020-08-01] + record: [SignalRecord, PortAnaRecord] +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 \ No newline at end of file diff --git a/setup.py b/setup.py index 22e806d8d..38a84ef7c 100644 --- a/setup.py +++ b/setup.py @@ -56,6 +56,7 @@ REQUIRED = [ "lightgbm", "tornado", "joblib>=0.17.0", + "fire>=0.3.1", ] # Numpy include From 90d41e4022a314f57c919d01ff52e410956054a0 Mon Sep 17 00:00:00 2001 From: Young Date: Mon, 16 Nov 2020 13:11:39 +0000 Subject: [PATCH 048/241] add finetune example & fix serial bug --- examples/workflow_by_code_finetune.py | 131 ++++++++++++++++++++++++++ qlib/contrib/model/gbdt.py | 84 ++++++++++------- qlib/model/base.py | 15 +++ qlib/utils/serial.py | 4 +- qlib/workflow/exp.py | 6 +- 5 files changed, 203 insertions(+), 37 deletions(-) create mode 100644 examples/workflow_by_code_finetune.py diff --git a/examples/workflow_by_code_finetune.py b/examples/workflow_by_code_finetune.py new file mode 100644 index 000000000..041e23b83 --- /dev/null +++ b/examples/workflow_by_code_finetune.py @@ -0,0 +1,131 @@ +# 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 +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_cn(target_dir=provider_uri) + + qlib.init(provider_uri=provider_uri, region=REG_CN) + + MARKET = "csi300" + BENCHMARK = "SH000300" + + ################################### + # train model + ################################### + DATA_HANDLER_CONFIG = { + "start_time": "2008-01-01", + "end_time": "2020-08-01", + "fit_start_time": "2008-01-01", + "fit_end_time": "2014-12-31", + "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": ("2008-01-01", "2014-12-31"), + "valid": ("2015-01-01", "2016-12-31"), + "test": ("2017-01-01", "2020-08-01"), + }, + }, + }, + # You shoud record the data in specific sequence + "record": ["SignalRecord", "PortAnaRecord"], + } + + 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, + }, + } + + # model initiaiton + model = init_instance_by_config(task["model"]) + dataset = init_instance_by_config(task["dataset"]) + + # start exp to train init model + with R.start(experiment_name="init models"): + model.fit(dataset) + R.save_objects(init_model=model) + rid = R.get_recorder().id + + + # Finetune model based on previous trained model + with R.start(experiment_name="finetune model"): + recorder = R.get_recorder(rid, experiment_name="init models") + model = recorder.load_object("init_model") + model.finetune(dataset, num_boost_round=10) + R.save_objects(model=model) + + # 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/model/gbdt.py b/qlib/contrib/model/gbdt.py index 61c617b8d..535e9b453 100644 --- a/qlib/contrib/model/gbdt.py +++ b/qlib/contrib/model/gbdt.py @@ -5,56 +5,54 @@ import numpy as np import pandas as pd import lightgbm as lgb -from ...model.base import Model +from ...model.base import ModelFT from ...data.dataset import DatasetH from ...data.dataset.handler import DataHandlerLP -class LGBModel(Model): +class LGBModel(ModelFT): """LightGBM Model""" - def __init__(self, loss="mse", **kwargs): if loss not in {"mse", "binary"}: raise NotImplementedError - self._params = {"objective": loss} - self._params.update(kwargs) + self.params = {"objective": loss} + self.params.update(kwargs) self.model = None - def fit( - self, - dataset: DatasetH, - num_boost_round=1000, - early_stopping_rounds=50, - verbose_eval=20, - evals_result=dict(), - **kwargs - ): - - df_train, df_valid = dataset.prepare( - ["train", "valid"], col_set=["feature", "label"], data_key=DataHandlerLP.DK_L - ) + def _prepare_data(self, dataset: DatasetH): + df_train, df_valid = dataset.prepare(["train", "valid"], + col_set=["feature", "label"], + data_key=DataHandlerLP.DK_L) x_train, y_train = df_train["feature"], df_train["label"] x_valid, y_valid = df_valid["feature"], df_valid["label"] # Lightgbm need 1D array as its label if y_train.values.ndim == 2 and y_train.values.shape[1] == 1: - y_train_1d, y_valid_1d = np.squeeze(y_train.values), np.squeeze(y_valid.values) + y_train, y_valid = np.squeeze(y_train.values), np.squeeze(y_valid.values) else: raise ValueError("LightGBM doesn't support multi-label training") - dtrain = lgb.Dataset(x_train.values, label=y_train_1d) - dvalid = lgb.Dataset(x_valid.values, label=y_valid_1d) - self.model = lgb.train( - self._params, - dtrain, - num_boost_round=num_boost_round, - valid_sets=[dtrain, dvalid], - valid_names=["train", "valid"], - early_stopping_rounds=early_stopping_rounds, - verbose_eval=verbose_eval, - evals_result=evals_result, - **kwargs - ) + dtrain = lgb.Dataset(x_train.values, label=y_train) + dvalid = lgb.Dataset(x_valid.values, label=y_valid) + return dtrain, dvalid + + def fit(self, + dataset: DatasetH, + num_boost_round=1000, + early_stopping_rounds=50, + verbose_eval=20, + evals_result=dict(), + **kwargs): + dtrain, dvalid = self._prepare_data(dataset) + self.model = lgb.train(self.params, + dtrain, + num_boost_round=num_boost_round, + valid_sets=[dtrain, dvalid], + valid_names=["train", "valid"], + early_stopping_rounds=early_stopping_rounds, + verbose_eval=verbose_eval, + evals_result=evals_result, + **kwargs) evals_result["train"] = list(evals_result["train"].values())[0] evals_result["valid"] = list(evals_result["valid"].values())[0] @@ -63,3 +61,25 @@ class LGBModel(Model): raise ValueError("model is not fitted yet!") x_test = dataset.prepare("test", col_set="feature") return pd.Series(self.model.predict(np.squeeze(x_test.values)), index=x_test.index) + + def finetune(self, dataset: DatasetH, num_boost_round=10, verbose_eval=20): + """ + finetune model + + Parameters + ---------- + dataset : DatasetH + dataset for finetuning + num_boost_round : int + number of round to finetune model + verbose_eval : int + verbose level + """ + dtrain, _ = self._prepare_data(dataset) + self.model = lgb.train(self.params, + dtrain, + num_boost_round=num_boost_round, + init_model=self.model, + valid_sets=[dtrain], + valid_names=["train"], + verbose_eval=verbose_eval) diff --git a/qlib/model/base.py b/qlib/model/base.py index 02333bfb6..719b69581 100644 --- a/qlib/model/base.py +++ b/qlib/model/base.py @@ -45,3 +45,18 @@ class Model(BaseModel): dataset will generate the processed dataset from model training """ raise NotImplementedError() + + +class ModelFT(Model): + '''Model (F)ine(t)unable''' + + @abc.abstractmethod + def finetune(self, dataset: Dataset): + """finetune model based given dataset + + Parameters + ---------- + dataset : Dataset + dataset will generate the processed dataset from model training + """ + raise NotImplementedError() diff --git a/qlib/utils/serial.py b/qlib/utils/serial.py index 04781d655..9bc8ce94a 100644 --- a/qlib/utils/serial.py +++ b/qlib/utils/serial.py @@ -8,11 +8,11 @@ import pickle class Serializable: """ Serializable behaves like pickle. - But it only save the state whose name starts with `_` + But it only saves the state whose name **does not** start with `_` """ def __getstate__(self) -> dict: - return {k: v for k, v in self.__dict__.items() if k.startswith("_")} + return {k: v for k, v in self.__dict__.items() if not k.startswith("_")} def __setstate__(self, state: dict): self.__dict__.update(state) diff --git a/qlib/workflow/exp.py b/qlib/workflow/exp.py index a32d33d57..b64b1544c 100644 --- a/qlib/workflow/exp.py +++ b/qlib/workflow/exp.py @@ -226,7 +226,7 @@ class MLflowExperiment(Experiment): return self.active_recorder else: raise Exception( - "Something went wrong when retrieving recorders. Please check if QlibRecorder is running or the name/id of the recorder is correct." + "Something went wrong when retrieving recorders. Please check if QlibRecorder is running." ) else: if recorder_id is not None: @@ -235,7 +235,7 @@ class MLflowExperiment(Experiment): else: # mlflow does not support create a run with given id raise Exception( - "Something went wrong when retrieving recorders. Please check if QlibRecorder is running or the name/id of the recorder is correct." + "Something went wrong when retrieving recorders. Please check if id of the recorder is correct." ) else: for rid in recorders: @@ -250,7 +250,7 @@ class MLflowExperiment(Experiment): return recorder else: raise Exception( - "Something went wrong when retrieving experiments. Please check if QlibRecorder is running or the name/id of the experiment is correct." + "Something went wrong when retrieving experiments. Please check if the name of the experiment is correct." ) def list_recorders(self): From 0afe57f2fe2cac385951ad7cce1fc0066b7720f5 Mon Sep 17 00:00:00 2001 From: lwwang1995 Date: Mon, 16 Nov 2020 23:28:11 +0800 Subject: [PATCH 049/241] Update GRU model. --- examples/workflow_by_code_gru.py | 17 +- qlib/contrib/data/handler.py | 98 ++++++++-- qlib/contrib/model/pytorch_gru.py | 290 +++++++++++++++--------------- qlib/data/dataset/processor.py | 13 ++ 4 files changed, 258 insertions(+), 160 deletions(-) diff --git a/examples/workflow_by_code_gru.py b/examples/workflow_by_code_gru.py index 06fea1511..e55f0ae45 100755 --- a/examples/workflow_by_code_gru.py +++ b/examples/workflow_by_code_gru.py @@ -8,7 +8,7 @@ import qlib import pandas as pd from qlib.config import REG_CN from qlib.contrib.model.pytorch_gru import GRU -from qlib.contrib.data.handler import ALPHA360 +from qlib.contrib.data.handler import ALPHA360_Denoise from qlib.contrib.strategy.strategy import TopkDropoutStrategy from qlib.contrib.evaluate import ( backtest as normal_backtest, @@ -19,6 +19,7 @@ from qlib.utils import exists_qlib_data # from qlib.model.learner import train_model from qlib.utils import init_instance_by_config +import pickle if __name__ == "__main__": @@ -63,14 +64,13 @@ if __name__ == "__main__": "kwargs": { "d_feat": 6, "hidden_size": 64, - "num_layers": 3, + "num_layers": 2, "dropout": 0.0, - "n_epochs": 2000, - "lr": 1e-1, - "early_stop": 200, + "n_epochs": 200, + "lr": 1e-3, + "early_stop": 20, "batch_size": 800, - "smooth_steps": 5, - "metric": "mse", + "metric": "IC", "loss": "mse", "seed": 0, "GPU": 0, @@ -81,7 +81,7 @@ if __name__ == "__main__": "module_path": "qlib.data.dataset", "kwargs": { "handler": { - "class": "ALPHA360", + "class": "ALPHA360_Denoise", "module_path": "qlib.contrib.data.handler", "kwargs": DATA_HANDLER_CONFIG, }, @@ -99,7 +99,6 @@ if __name__ == "__main__": # model = train_model(task) model = init_instance_by_config(task["model"]) dataset = init_instance_by_config(task["dataset"]) - model.fit(dataset) pred_score = model.predict(dataset) diff --git a/qlib/contrib/data/handler.py b/qlib/contrib/data/handler.py index 54e9de27f..c69345173 100644 --- a/qlib/contrib/data/handler.py +++ b/qlib/contrib/data/handler.py @@ -9,6 +9,78 @@ from ...log import TimeInspector from inspect import getfullargspec import copy +class ALPHA360_Denoise(DataHandlerLP): + def __init__(self, instruments="csi500", start_time=None, end_time=None, fit_start_time=None, fit_end_time=None): + data_loader = { + "class": "QlibDataLoader", + "kwargs": { + "config": { + "feature": self.get_feature_config(), + "label": self.get_label_config(), + }, + }, + } + + learn_processors = [ + {"class": "DropnaLabel", "kwargs": {"group": "label"}}, + {"class": "CSZScoreNorm", "kwargs": {"fields_group": "label"}}, + ] + infer_processors = [ + {"class": "ProcessInf", "kwargs": {}}, + {"class": "TanhProcess", "kwargs": {}}, + {"class": "Fillna", "kwargs": {}}, + ] + + super().__init__( + instruments, + start_time, + end_time, + data_loader=data_loader, + learn_processors=learn_processors, + infer_processors=infer_processors, + ) + + def get_label_config(self): + return (["Ref($close, -2)/Ref($close, -1) - 1"], ["LABEL0"]) + + def get_feature_config(self): + + fields = [] + names = [] + + for i in range(59, 0, -1): + fields += ["Ref($close, %d)/$close" % (i)] + names += ["CLOSE%d" % (i)] + fields += ["$close/$close"] + names += ["CLOSE0"] + for i in range(59, 0, -1): + fields += ["Ref($open, %d)/$close" % (i)] + names += ["OPEN%d" % (i)] + fields += ["$open/$close"] + names += ["OPEN0"] + for i in range(59, 0, -1): + fields += ["Ref($high, %d)/$close" % (i)] + names += ["HIGH%d" % (i)] + fields += ["$high/$close"] + names += ["HIGH0"] + for i in range(59, 0, -1): + fields += ["Ref($low, %d)/$close" % (i)] + names += ["LOW%d" % (i)] + fields += ["$low/$close"] + names += ["LOW0"] + for i in range(59, 0, -1): + fields += ["Ref($vwap, %d)/$close" % (i)] + names += ["VWAP%d" % (i)] + fields += ["$vwap/$close"] + names += ["VWAP0"] + for i in range(59, 0, -1): + fields += ["Ref($volume, %d)/$volume" % (i)] + names += ["VOLUME%d" % (i)] + fields += ["$volume/$volume"] + names += ["VOLUME0"] + + return fields, names + class ALPHA360(DataHandlerLP): def __init__(self, instruments="csi500", start_time=None, end_time=None, fit_start_time=None, fit_end_time=None): @@ -52,28 +124,32 @@ class ALPHA360(DataHandlerLP): for i in range(59, 0, -1): fields += ["Ref($close, %d)/$close" % (i)] names += ["CLOSE%d" % (i)] + fields += ["$close/$close"] + names += ["CLOSE0"] + for i in range(59, 0, -1): fields += ["Ref($open, %d)/$close" % (i)] names += ["OPEN%d" % (i)] + fields += ["$open/$close"] + names += ["OPEN0"] + for i in range(59, 0, -1): fields += ["Ref($high, %d)/$close" % (i)] names += ["HIGH%d" % (i)] + fields += ["$high/$close"] + names += ["HIGH0"] + for i in range(59, 0, -1): fields += ["Ref($low, %d)/$close" % (i)] names += ["LOW%d" % (i)] + fields += ["$low/$close"] + names += ["LOW0"] + for i in range(59, 0, -1): fields += ["Ref($vwap, %d)/$close" % (i)] names += ["VWAP%d" % (i)] + fields += ["$vwap/$close"] + names += ["VWAP0"] + for i in range(59, 0, -1): fields += ["Ref($volume, %d)/$volume" % (i)] names += ["VOLUME%d" % (i)] - - fields += ["$close/$close"] - fields += ["$open/$close"] - fields += ["$high/$close"] - fields += ["$low/$close"] - fields += ["$vwap/$close"] fields += ["$volume/$volume"] - names += ["CLOSE0"] - names += ["OPEN0"] - names += ["HIGH0"] - names += ["LOW0"] - names += ["VWAP0"] names += ["VOLUME0"] return fields, names diff --git a/qlib/contrib/model/pytorch_gru.py b/qlib/contrib/model/pytorch_gru.py index 464cd9ba0..f118542d6 100755 --- a/qlib/contrib/model/pytorch_gru.py +++ b/qlib/contrib/model/pytorch_gru.py @@ -36,10 +36,6 @@ class GRU(Model): layer sizes lr : float learning rate - lr_decay : float - learning rate decay - lr_decay_steps : int - learning rate decay steps optimizer : str optimizer name GPU : str @@ -54,13 +50,11 @@ class GRU(Model): dropout=0.0, n_epochs=200, lr=0.001, + metric='IC', batch_size=2000, early_stop=20, - eval_steps=5, loss="mse", - lr_decay=0.96, - lr_decay_steps=100, - optimizer="gd", + optimizer="adam", GPU="0", seed=0, **kwargs @@ -76,13 +70,11 @@ class GRU(Model): self.dropout = dropout self.n_epochs = n_epochs self.lr = lr + self.metric = metric self.batch_size = batch_size self.early_stop = early_stop - self.eval_steps = eval_steps - self.lr_decay = lr_decay - self.lr_decay_steps = lr_decay_steps self.optimizer = optimizer.lower() - self.loss_type = loss + self.loss = loss self.visible_GPU = GPU self.use_gpu = torch.cuda.is_available() self.seed = seed @@ -95,11 +87,9 @@ class GRU(Model): "\ndropout : {}" "\nn_epochs : {}" "\nlr : {}" + "\nmetric : {}" "\nbatch_size : {}" "\nearly_stop : {}" - "\neval_steps : {}" - "\nlr_decay : {}" - "\nlr_decay_steps : {}" "\noptimizer : {}" "\nloss_type : {}" "\nvisible_GPU : {}" @@ -111,11 +101,9 @@ class GRU(Model): dropout, n_epochs, lr, + metric, batch_size, early_stop, - eval_steps, - lr_decay, - lr_decay_steps, optimizer.lower(), loss, GPU, @@ -138,20 +126,6 @@ class GRU(Model): else: raise NotImplementedError("optimizer {} is not supported!".format(optimizer)) - # Reduce learning rate when loss has stopped decrease - self.scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau( - self.train_optimizer, - mode="min", - factor=0.5, - patience=10, - verbose=True, - threshold=0.0001, - threshold_mode="rel", - cooldown=0, - min_lr=0.00001, - eps=1e-08, - ) - self._fitted = False if self.use_gpu: self.gru_model.cuda() @@ -159,6 +133,98 @@ class GRU(Model): if self.visible_GPU: os.environ["CUDA_VISIBLE_DEVICES"] = self.visible_GPU + def mse(self, pred, label): + loss = (pred - label)**2 + return torch.mean(loss) + + def loss_fn(self, pred, label): + mask = ~torch.isnan(label) + + if self.loss == 'mse': + return self.mse(pred[mask], label[mask]) + + raise ValueError('unknown loss `%s`'%self.loss) + + def metric_fn(self, pred, label): + + mask = torch.isfinite(label) + if self.metric == 'IC': + return self.cal_ic(pred[mask], label[mask]) + + if self.metric == '' or self.metric == 'loss': # use loss + return -self.loss_fn(pred[mask], label[mask]) + + raise ValueError('unknown metric `%s`'%self.metric) + + def cal_ic(self, pred, label): + return torch.mean(pred * label) + + def train_epoch(self, x_train, y_train): + + x_train_values = x_train.values + y_train_values = np.squeeze(y_train.values)*100 + + self.gru_model.train() + + indices = np.arange(len(x_train_values)) + np.random.shuffle(indices) + + for i in range(len(indices))[::self.batch_size]: + + if len(indices) - i < self.batch_size: + break + + feature = torch.from_numpy(x_train_values[indices[i:i+self.batch_size]]).float() + label = torch.from_numpy(y_train_values[indices[i:i+self.batch_size]]).float() + + if self.use_gpu: + feature = feature.cuda() + label = label.cuda() + + pred = self.gru_model(feature) + loss = self.loss_fn(pred, label) + + self.train_optimizer.zero_grad() + loss.backward() + torch.nn.utils.clip_grad_value_(self.gru_model.parameters(), 3.) + self.train_optimizer.step() + + + def test_epoch(self, data_x, data_y): + + # prepare training data + x_values = data_x.values + y_values = np.squeeze(data_y.values) + + self.gru_model.eval() + + scores = [] + losses = [] + + indices = np.arange(len(x_values)) + np.random.shuffle(indices) + + for i in range(len(indices))[::self.batch_size]: + + if len(indices) - i < self.batch_size: + break + + feature = torch.from_numpy(x_values[indices[i:i+self.batch_size]]).float() + label = torch.from_numpy(y_values[indices[i:i+self.batch_size]]).float() + + if self.use_gpu: + feature = feature.cuda() + label = label.cuda() + + pred = self.gru_model(feature) + loss = self.loss_fn(pred, label) + losses.append(loss.item()) + + score = self.metric_fn(pred, label) + scores.append(score.item()) + + return np.mean(losses), np.mean(scores) + def fit( self, dataset: DatasetH, @@ -167,17 +233,23 @@ class GRU(Model): save_path=None, ): - df_train, df_valid = dataset.prepare( - ["train", "valid"], col_set=["feature", "label"], data_key=DataHandlerLP.DK_L + df_train, df_valid, df_test = dataset.prepare( + ["train", "valid", "test"], col_set=["feature", "label"], data_key=DataHandlerLP.DK_L ) + print(df_test) + df_train.to_pickle('~/df_train_2.pkl') + df_valid.to_pickle('~/df_valid_2.pkl') + df_test.to_pickle('~/df_test_2.pkl') + x_train, y_train = df_train["feature"], df_train["label"] x_valid, y_valid = df_valid["feature"], df_valid["label"] - # Lightgbm need 1D array as its label - save_path = create_save_path(save_path) + if save_path == None: + save_path = create_save_path(save_path) stop_steps = 0 train_loss = 0 - best_loss = np.inf + best_score = -np.inf + best_epoch = 0 evals_result["train"] = [] evals_result["valid"] = [] @@ -185,94 +257,36 @@ class GRU(Model): self.logger.info("training...") self._fitted = True # return - # prepare training data - x_train_values = torch.from_numpy(x_train.values).float() - y_train_values = torch.from_numpy(np.squeeze(y_train.values)).float() - train_num = y_train_values.shape[0] - - # prepare validation data - x_val_auto = torch.from_numpy(x_valid.values).float() - y_val_auto = torch.from_numpy(np.squeeze(y_valid.values)).float() - - if self.use_gpu: - x_val_auto = x_val_auto.cuda() - y_val_auto = y_val_auto.cuda() for step in range(self.n_epochs): - if stop_steps >= self.early_stop: - if verbose: - self.logger.info("\tearly stop") - break - loss = AverageMeter() - self.gru_model.train() - self.train_optimizer.zero_grad() + self.logger.info('Epoch%d:', step) + self.logger.info('training...') + self.train_epoch(x_train, y_train) + self.logger.info('evaluating...') + train_loss, train_score = self.test_epoch(x_train, y_train) + val_loss, val_score = self.test_epoch(x_valid, y_valid) + self.logger.info('train %.6f, valid %.6f'%(train_score, val_score)) + evals_result["train"].append(train_score) + evals_result["valid"].append(val_score) - choice = np.random.choice(train_num, self.batch_size) - x_batch_auto = x_train_values[choice] - y_batch_auto = y_train_values[choice] - - if self.use_gpu: - x_batch_auto = x_batch_auto.float().cuda() - y_batch_auto = y_batch_auto.float().cuda() - - # forward - preds = self.gru_model(x_batch_auto) - cur_loss = self.get_loss(preds, y_batch_auto, self.loss_type) - cur_loss.backward() - self.train_optimizer.step() - loss.update(cur_loss.item()) - - # validation - train_loss += loss.val - # print(loss.val) - if step and step % self.eval_steps == 0: + if val_score > best_score: + best_score = val_score + stop_steps = 0 + best_epoch = step + best_param = copy.deepcopy(self.gru_model.state_dict()) + else: stop_steps += 1 - train_loss /= self.eval_steps + if stop_steps >= self.early_stop: + self.logger.info('early stop') + break - with torch.no_grad(): - self.gru_model.eval() - loss_val = AverageMeter() - - # forward - preds = self.gru_model(x_val_auto) - cur_loss_val = self.get_loss(preds, y_val_auto, self.loss_type) - loss_val.update(cur_loss_val.item()) - - if verbose: - self.logger.info( - "[Epoch {}]: train_loss {:.6f}, valid_loss {:.6f}".format(step, train_loss, loss_val.val) - ) - evals_result["train"].append(train_loss) - evals_result["valid"].append(loss_val.val) - if loss_val.val < best_loss: - if verbose: - self.logger.info( - "\tvalid loss update from {:.6f} to {:.6f}, save checkpoint.".format( - best_loss, loss_val.val - ) - ) - best_loss = loss_val.val - stop_steps = 0 - torch.save(self.gru_model.state_dict(), save_path) - train_loss = 0 - # update learning rate - self.scheduler.step(cur_loss_val) - - # restore the optimal parameters after training ?? - # self.gru_model.load_state_dict(torch.load(save_path)) + self.logger.info('best score: %.6lf @ %d'%(best_score, best_epoch)) + self.gru_model.load_state_dict(best_param) + torch.save(best_param, save_path) + if self.use_gpu: torch.cuda.empty_cache() - def get_loss(self, pred, target, loss_type): - if loss_type == "mse": - sqr_loss = (pred - target) ** 2 - loss = sqr_loss.mean() - return loss - elif loss_type == "binary": - loss = nn.BCELoss() - return loss(pred, target) - else: - raise NotImplementedError("loss {} is not supported!".format(loss_type)) def predict(self, dataset): if not self._fitted: @@ -280,37 +294,33 @@ class GRU(Model): x_test = dataset.prepare("test", col_set="feature") index = x_test.index - x_test = torch.from_numpy(x_test.values).float() - - if self.use_gpu: - x_test = x_test.cuda() self.gru_model.eval() + x_values = x_test.values + sample_num = x_values.shape[0] + preds = [] - with torch.no_grad(): - if self.use_gpu: - preds = self.gru_model(x_test).detach().cpu().numpy() + for begin in range(sample_num)[::self.batch_size]: + + if sample_num-begin < self.batch_size: + end = sample_num else: - preds = self.gru_model(x_test).detach().numpy() - return pd.Series(preds, index=index) + end = begin+self.batch_size + + x_batch = torch.from_numpy(x_values[begin:end]).float() -class AverageMeter(object): - """Computes and stores the average and current value""" + if self.use_gpu: + x_batch = x_batch.cuda() - def __init__(self): - self.reset() + with torch.no_grad(): + if self.use_gpu: + pred = self.gru_model(x_batch).detach().cpu().numpy() + else: + pred = self.gru_model(x_batch).detach().numpy() - def reset(self): - self.val = 0 - self.avg = 0 - self.sum = 0 - self.count = 0 + preds.append(pred) - def update(self, val, n=1): - self.val = val - self.sum += val * n - self.count += n - self.avg = self.sum / self.count + return pd.Series(np.concatenate(preds), index=index) class GRUModel(nn.Module): diff --git a/qlib/data/dataset/processor.py b/qlib/data/dataset/processor.py index 308c531b9..400574320 100755 --- a/qlib/data/dataset/processor.py +++ b/qlib/data/dataset/processor.py @@ -89,6 +89,19 @@ class DropnaLabel(DropnaProcessor): """The samples are dropped according to label. So it is not usable for inference""" return False +class TanhProcess(Processor): + """ Use tanh to process noise data""" + def __call__(self, df): + def tanh_denoise(data): + mask = data.columns.get_level_values(1).str.contains('LABEL') + col = df.columns[~mask] + data[col] = data[col] - 1 + data[col] = np.tanh(data[col]) + + return data + + return tanh_denoise(df) + class ProcessInf(Processor): """Process infinity """ From a8b46dd41d509913cb866d45e8f0ab337aaaad4c Mon Sep 17 00:00:00 2001 From: lwwang1995 Date: Mon, 16 Nov 2020 23:29:51 +0800 Subject: [PATCH 050/241] Delete log. --- qlib/contrib/model/pytorch_gru.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/qlib/contrib/model/pytorch_gru.py b/qlib/contrib/model/pytorch_gru.py index f118542d6..a78ee27af 100755 --- a/qlib/contrib/model/pytorch_gru.py +++ b/qlib/contrib/model/pytorch_gru.py @@ -236,10 +236,6 @@ class GRU(Model): df_train, df_valid, df_test = dataset.prepare( ["train", "valid", "test"], col_set=["feature", "label"], data_key=DataHandlerLP.DK_L ) - print(df_test) - df_train.to_pickle('~/df_train_2.pkl') - df_valid.to_pickle('~/df_valid_2.pkl') - df_test.to_pickle('~/df_test_2.pkl') x_train, y_train = df_train["feature"], df_train["label"] x_valid, y_valid = df_valid["feature"], df_valid["label"] From 64ed43b791ca7b22877e9e480cbeb8c5eb25c728 Mon Sep 17 00:00:00 2001 From: Jactus Date: Tue, 17 Nov 2020 22:05:18 +0800 Subject: [PATCH 051/241] Update R and workflow --- docs/start/initialization.rst | 15 +- examples/workflow_by_code.py | 2 +- examples/workflow_by_code_finetune.py | 3 +- examples/workflow_by_config.py | 49 ------ examples/workflow_config.yaml | 53 +++--- qlib/__init__.py | 19 +- qlib/contrib/data/handler.py | 1 + qlib/contrib/model/gbdt.py | 59 ++++--- qlib/contrib/model/pytorch_gru.py | 55 +++--- qlib/data/dataset/processor.py | 4 +- qlib/model/base.py | 2 +- qlib/utils/__init__.py | 2 +- qlib/workflow/__init__.py | 24 +-- qlib/workflow/cli.py | 53 ++++++ qlib/workflow/exp.py | 158 +++++++++-------- qlib/workflow/expm.py | 245 ++++++++++++++------------ qlib/workflow/record_temp.py | 53 ++++-- qlib/workflow/recorder.py | 39 ++-- qlib/workflow/utils.py | 19 +- setup.py | 2 +- 20 files changed, 481 insertions(+), 376 deletions(-) delete mode 100644 examples/workflow_by_config.py create mode 100644 qlib/workflow/cli.py diff --git a/docs/start/initialization.rst b/docs/start/initialization.rst index cffe11f52..bcb09925e 100644 --- a/docs/start/initialization.rst +++ b/docs/start/initialization.rst @@ -59,7 +59,14 @@ Besides `provider_uri` and `region`, `qlib.init` has other parameters. The follo If Qlib fails to connect redis via `redis_host` and `redis_port`, cache mechanism will not be used! Please refer to `Cache <../component/data.html#cache>`_ for details. - `exp_manager` - Type: str, optional parameter(default: "MLflowExpManager"), the experiment manager to be used in qlib. -- `exp_uri` - Type: str, optional parameter(default: "mlruns" in local execution path), the tracking uri of the experiment manager. - It can either be a local path or a remote uri. \ No newline at end of file + Type: dict, optional parameter, the setting of experiment manager to be used in qlib. Users can specify an experiment manager class, as well as the tracking URI for all the experiments. However, please be aware that we only support input of a dictionary in the following style for `exp_manager`. + :: + + { + "class": "MLflowExpManager", + "module_path": "qlib.workflow.expm", + "kwargs": { + "uri": "python_execution_path/mlruns"), + "default_exp_name": "Experiment", + } + } \ No newline at end of file diff --git a/examples/workflow_by_code.py b/examples/workflow_by_code.py index cae890672..b70a9e963 100644 --- a/examples/workflow_by_code.py +++ b/examples/workflow_by_code.py @@ -89,7 +89,7 @@ if __name__ == "__main__": "kwargs": { "topk": 50, "n_drop": 5, - } + }, }, "backtest": { "verbose": False, diff --git a/examples/workflow_by_code_finetune.py b/examples/workflow_by_code_finetune.py index 041e23b83..6df8c9821 100644 --- a/examples/workflow_by_code_finetune.py +++ b/examples/workflow_by_code_finetune.py @@ -89,7 +89,7 @@ if __name__ == "__main__": "kwargs": { "topk": 50, "n_drop": 5, - } + }, }, "backtest": { "verbose": False, @@ -113,7 +113,6 @@ if __name__ == "__main__": R.save_objects(init_model=model) rid = R.get_recorder().id - # Finetune model based on previous trained model with R.start(experiment_name="finetune model"): recorder = R.get_recorder(rid, experiment_name="init models") diff --git a/examples/workflow_by_config.py b/examples/workflow_by_config.py deleted file mode 100644 index 7955d29d0..000000000 --- a/examples/workflow_by_config.py +++ /dev/null @@ -1,49 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. - -import sys -from pathlib import Path - -import qlib -import fire -import yaml -import pandas as pd -from qlib.config import REG_CN -from qlib.utils import exists_qlib_data, init_instance_by_config -from qlib.workflow import R -from qlib.workflow.record_temp import SignalRecord, PortAnaRecord - -# worflow handler function -def workflow(config_path): - with open(config_path) as fp: - config = yaml.load(fp, Loader=yaml.FullLoader) - - provider_uri = config.get("PROVIDER_URI") - 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_cn(target_dir=provider_uri) - - qlib.init(provider_uri=provider_uri, region=REG_CN) - - # model initiaiton - model = init_instance_by_config(config.get("TASK")["model"]) - dataset = init_instance_by_config(config.get("TASK")["dataset"]) - - # start exp - with R.start("workflow"): - model.fit(dataset) - - # prediction - recorder = R.get_recorder() - sr = SignalRecord(model, dataset, recorder) - sr.generate() - - # backtest - par = PortAnaRecord(recorder, config.get("PORT_ANALYSIS_CONFIG")) - par.generate() - -if __name__ == "__main__": - fire.Fire(workflow) \ No newline at end of file diff --git a/examples/workflow_config.yaml b/examples/workflow_config.yaml index 2698423df..212558044 100644 --- a/examples/workflow_config.yaml +++ b/examples/workflow_config.yaml @@ -1,13 +1,29 @@ -PROVIDER_URI: "~/.qlib/qlib_data/cn_data" -MARKET: &market csi300 -BENCHMARK: &benchmark SH000300 -DATA_HANDLER_CONFIG: &data_handerler_config +provider_uri: "~/.qlib/qlib_data/cn_data" +market: &market csi300 +benchmark: &benchmark SH000300 +data_handler_config: &data_handler_config start_time: 2008-01-01 end_time: 2020-08-01 fit_start_time: 2008-01-01 fit_end_time: 2014-12-31 instruments: *market -TASK: +port_analysis_config: &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 +task: model: class: LGBModel module_path: qlib.contrib.model.gbdt @@ -28,25 +44,16 @@ TASK: handler: class: Alpha158 module_path: qlib.contrib.data.handler - kwargs: *data_handerler_config + kwargs: *data_handler_config segments: train: [2008-01-01, 2014-12-31] valid: [2015-01-01, 2016-12-31] test: [2017-01-01, 2020-08-01] - record: [SignalRecord, PortAnaRecord] -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 \ No newline at end of file + record: + - class: SignalRecord + module_path: qlib.workflow.record_temp + kwargs: {} + - class: PortAnaRecord + module_path: qlib.workflow.record_temp + kwargs: + config: *port_analysis_config \ No newline at end of file diff --git a/qlib/__init__.py b/qlib/__init__.py index c9159dadf..ed6ae1543 100644 --- a/qlib/__init__.py +++ b/qlib/__init__.py @@ -9,15 +9,13 @@ import re import sys import copy import yaml -import atexit -import signal import logging import platform import subprocess from pathlib import Path from .utils import can_use_cache, init_instance_by_config, get_module_by_module_path -from .workflow.utils import experiment_exception_hook, experiment_kill_signal_handler +from .workflow.utils import experiment_exit_handler # init qlib def init(default_conf="client", **kwargs): @@ -46,14 +44,9 @@ def init(default_conf="client", **kwargs): C.set_region(kwargs.get("region", C["region"] if "region" in C else REG_CN)) for k, v in kwargs.items(): - if k == "exp_manager": - C["exp_manager"].update({"class": v}) - elif k == "exp_uri": - C["exp_manager"]["kwargs"].update({"uri": v}) - else: - C[k] = v - if k not in C: - LOG.warning("Unrecognized config %s" % k) + C[k] = v + if k not in C: + LOG.warning("Unrecognized config %s" % k) C.resolve_path() @@ -93,9 +86,7 @@ def init(default_conf="client", **kwargs): qr = QlibRecorder(exp_manager) R.register(qr) # clean up experiment when python program ends - atexit.register(R.end_exp, recorder_status="FINISHED") # will not take effect if experiment ends - signal.signal(signal.SIGINT, experiment_kill_signal_handler) - sys.excepthook = experiment_exception_hook + experiment_exit_handler() def _mount_nfs_uri(C): diff --git a/qlib/contrib/data/handler.py b/qlib/contrib/data/handler.py index c69345173..99a601b9e 100644 --- a/qlib/contrib/data/handler.py +++ b/qlib/contrib/data/handler.py @@ -9,6 +9,7 @@ from ...log import TimeInspector from inspect import getfullargspec import copy + class ALPHA360_Denoise(DataHandlerLP): def __init__(self, instruments="csi500", start_time=None, end_time=None, fit_start_time=None, fit_end_time=None): data_loader = { diff --git a/qlib/contrib/model/gbdt.py b/qlib/contrib/model/gbdt.py index 535e9b453..41b773756 100644 --- a/qlib/contrib/model/gbdt.py +++ b/qlib/contrib/model/gbdt.py @@ -12,6 +12,7 @@ from ...data.dataset.handler import DataHandlerLP class LGBModel(ModelFT): """LightGBM Model""" + def __init__(self, loss="mse", **kwargs): if loss not in {"mse", "binary"}: raise NotImplementedError @@ -20,9 +21,9 @@ class LGBModel(ModelFT): self.model = None def _prepare_data(self, dataset: DatasetH): - df_train, df_valid = dataset.prepare(["train", "valid"], - col_set=["feature", "label"], - data_key=DataHandlerLP.DK_L) + df_train, df_valid = dataset.prepare( + ["train", "valid"], col_set=["feature", "label"], data_key=DataHandlerLP.DK_L + ) x_train, y_train = df_train["feature"], df_train["label"] x_valid, y_valid = df_valid["feature"], df_valid["label"] @@ -36,23 +37,27 @@ class LGBModel(ModelFT): dvalid = lgb.Dataset(x_valid.values, label=y_valid) return dtrain, dvalid - def fit(self, - dataset: DatasetH, - num_boost_round=1000, - early_stopping_rounds=50, - verbose_eval=20, - evals_result=dict(), - **kwargs): + def fit( + self, + dataset: DatasetH, + num_boost_round=1000, + early_stopping_rounds=50, + verbose_eval=20, + evals_result=dict(), + **kwargs + ): dtrain, dvalid = self._prepare_data(dataset) - self.model = lgb.train(self.params, - dtrain, - num_boost_round=num_boost_round, - valid_sets=[dtrain, dvalid], - valid_names=["train", "valid"], - early_stopping_rounds=early_stopping_rounds, - verbose_eval=verbose_eval, - evals_result=evals_result, - **kwargs) + self.model = lgb.train( + self.params, + dtrain, + num_boost_round=num_boost_round, + valid_sets=[dtrain, dvalid], + valid_names=["train", "valid"], + early_stopping_rounds=early_stopping_rounds, + verbose_eval=verbose_eval, + evals_result=evals_result, + **kwargs + ) evals_result["train"] = list(evals_result["train"].values())[0] evals_result["valid"] = list(evals_result["valid"].values())[0] @@ -76,10 +81,12 @@ class LGBModel(ModelFT): verbose level """ dtrain, _ = self._prepare_data(dataset) - self.model = lgb.train(self.params, - dtrain, - num_boost_round=num_boost_round, - init_model=self.model, - valid_sets=[dtrain], - valid_names=["train"], - verbose_eval=verbose_eval) + self.model = lgb.train( + self.params, + dtrain, + num_boost_round=num_boost_round, + init_model=self.model, + valid_sets=[dtrain], + valid_names=["train"], + verbose_eval=verbose_eval, + ) diff --git a/qlib/contrib/model/pytorch_gru.py b/qlib/contrib/model/pytorch_gru.py index a78ee27af..4cc7f9852 100755 --- a/qlib/contrib/model/pytorch_gru.py +++ b/qlib/contrib/model/pytorch_gru.py @@ -50,7 +50,7 @@ class GRU(Model): dropout=0.0, n_epochs=200, lr=0.001, - metric='IC', + metric="IC", batch_size=2000, early_stop=20, loss="mse", @@ -134,48 +134,48 @@ class GRU(Model): os.environ["CUDA_VISIBLE_DEVICES"] = self.visible_GPU def mse(self, pred, label): - loss = (pred - label)**2 + loss = (pred - label) ** 2 return torch.mean(loss) def loss_fn(self, pred, label): mask = ~torch.isnan(label) - if self.loss == 'mse': + if self.loss == "mse": return self.mse(pred[mask], label[mask]) - raise ValueError('unknown loss `%s`'%self.loss) + raise ValueError("unknown loss `%s`" % self.loss) def metric_fn(self, pred, label): mask = torch.isfinite(label) - if self.metric == 'IC': + if self.metric == "IC": return self.cal_ic(pred[mask], label[mask]) - if self.metric == '' or self.metric == 'loss': # use loss + if self.metric == "" or self.metric == "loss": # use loss return -self.loss_fn(pred[mask], label[mask]) - raise ValueError('unknown metric `%s`'%self.metric) + raise ValueError("unknown metric `%s`" % self.metric) def cal_ic(self, pred, label): - return torch.mean(pred * label) + return torch.mean(pred * label) def train_epoch(self, x_train, y_train): x_train_values = x_train.values - y_train_values = np.squeeze(y_train.values)*100 + y_train_values = np.squeeze(y_train.values) * 100 self.gru_model.train() indices = np.arange(len(x_train_values)) np.random.shuffle(indices) - for i in range(len(indices))[::self.batch_size]: + for i in range(len(indices))[:: self.batch_size]: if len(indices) - i < self.batch_size: break - feature = torch.from_numpy(x_train_values[indices[i:i+self.batch_size]]).float() - label = torch.from_numpy(y_train_values[indices[i:i+self.batch_size]]).float() + feature = torch.from_numpy(x_train_values[indices[i : i + self.batch_size]]).float() + label = torch.from_numpy(y_train_values[indices[i : i + self.batch_size]]).float() if self.use_gpu: feature = feature.cuda() @@ -186,10 +186,9 @@ class GRU(Model): self.train_optimizer.zero_grad() loss.backward() - torch.nn.utils.clip_grad_value_(self.gru_model.parameters(), 3.) + torch.nn.utils.clip_grad_value_(self.gru_model.parameters(), 3.0) self.train_optimizer.step() - def test_epoch(self, data_x, data_y): # prepare training data @@ -204,13 +203,13 @@ class GRU(Model): indices = np.arange(len(x_values)) np.random.shuffle(indices) - for i in range(len(indices))[::self.batch_size]: + for i in range(len(indices))[:: self.batch_size]: if len(indices) - i < self.batch_size: break - feature = torch.from_numpy(x_values[indices[i:i+self.batch_size]]).float() - label = torch.from_numpy(y_values[indices[i:i+self.batch_size]]).float() + feature = torch.from_numpy(x_values[indices[i : i + self.batch_size]]).float() + label = torch.from_numpy(y_values[indices[i : i + self.batch_size]]).float() if self.use_gpu: feature = feature.cuda() @@ -255,13 +254,13 @@ class GRU(Model): # return for step in range(self.n_epochs): - self.logger.info('Epoch%d:', step) - self.logger.info('training...') + self.logger.info("Epoch%d:", step) + self.logger.info("training...") self.train_epoch(x_train, y_train) - self.logger.info('evaluating...') + self.logger.info("evaluating...") train_loss, train_score = self.test_epoch(x_train, y_train) val_loss, val_score = self.test_epoch(x_valid, y_valid) - self.logger.info('train %.6f, valid %.6f'%(train_score, val_score)) + self.logger.info("train %.6f, valid %.6f" % (train_score, val_score)) evals_result["train"].append(train_score) evals_result["valid"].append(val_score) @@ -273,17 +272,16 @@ class GRU(Model): else: stop_steps += 1 if stop_steps >= self.early_stop: - self.logger.info('early stop') + self.logger.info("early stop") break - self.logger.info('best score: %.6lf @ %d'%(best_score, best_epoch)) + self.logger.info("best score: %.6lf @ %d" % (best_score, best_epoch)) self.gru_model.load_state_dict(best_param) torch.save(best_param, save_path) - + if self.use_gpu: torch.cuda.empty_cache() - def predict(self, dataset): if not self._fitted: raise ValueError("model is not fitted yet!") @@ -295,16 +293,15 @@ class GRU(Model): sample_num = x_values.shape[0] preds = [] - for begin in range(sample_num)[::self.batch_size]: + for begin in range(sample_num)[:: self.batch_size]: - if sample_num-begin < self.batch_size: + if sample_num - begin < self.batch_size: end = sample_num else: - end = begin+self.batch_size + end = begin + self.batch_size x_batch = torch.from_numpy(x_values[begin:end]).float() - if self.use_gpu: x_batch = x_batch.cuda() diff --git a/qlib/data/dataset/processor.py b/qlib/data/dataset/processor.py index 400574320..3970c8a0a 100755 --- a/qlib/data/dataset/processor.py +++ b/qlib/data/dataset/processor.py @@ -89,11 +89,13 @@ class DropnaLabel(DropnaProcessor): """The samples are dropped according to label. So it is not usable for inference""" return False + class TanhProcess(Processor): """ Use tanh to process noise data""" + def __call__(self, df): def tanh_denoise(data): - mask = data.columns.get_level_values(1).str.contains('LABEL') + mask = data.columns.get_level_values(1).str.contains("LABEL") col = df.columns[~mask] data[col] = data[col] - 1 data[col] = np.tanh(data[col]) diff --git a/qlib/model/base.py b/qlib/model/base.py index 719b69581..3fe83445c 100644 --- a/qlib/model/base.py +++ b/qlib/model/base.py @@ -48,7 +48,7 @@ class Model(BaseModel): class ModelFT(Model): - '''Model (F)ine(t)unable''' + """Model (F)ine(t)unable""" @abc.abstractmethod def finetune(self, dataset: Dataset): diff --git a/qlib/utils/__init__.py b/qlib/utils/__init__.py index c469829d2..30483af2e 100644 --- a/qlib/utils/__init__.py +++ b/qlib/utils/__init__.py @@ -643,7 +643,7 @@ def lexsort_index(df: pd.DataFrame) -> pd.DataFrame: #################### Wrapper ##################### class Wrapper(object): - """Data Provider Wrapper""" + """Wrapper class for anything that needs to set up during qlib.init""" def __init__(self): self._provider = None diff --git a/qlib/workflow/__init__.py b/qlib/workflow/__init__.py index 457dc4acd..6a8b857fc 100644 --- a/qlib/workflow/__init__.py +++ b/qlib/workflow/__init__.py @@ -32,14 +32,14 @@ class QlibRecorder: self.exp_manager = exp_manager @contextmanager - def start(self, experiment_name=None): + def start(self, experiment_name=None, recorder_name=None): """ Method to start an experiment. This method can only be called within a Python's `with` statement. Use case: --------- ``` - with R.start('test'): + with R.start('test', 'recorder_1'): model.fit(dataset) R.log... ... # further operations @@ -49,8 +49,10 @@ class QlibRecorder: ---------- experiment_name : str name of the experiment one wants to start. + recorder_name : str + name of the recorder under the experiment one wants to start. """ - run = self.start_exp(experiment_name) + run = self.start_exp(experiment_name, recorder_name) try: yield run except Exception as e: @@ -58,7 +60,7 @@ class QlibRecorder: raise e self.end_exp(Recorder.STATUS_FI) - def start_exp(self, experiment_name=None, uri=None): + def start_exp(self, experiment_name=None, recorder_name=None, uri=None): """ Lower level method for starting an experiment. When use this method, one should end the experiment manually and the status of the recorder may not be handled properly. @@ -66,7 +68,7 @@ class QlibRecorder: Use case: --------- ``` - R.start_exp(experiment_name='test') + R.start_exp(experiment_name='test', recorder_name='recorder_1') ... # further operations R.end_exp('FINISHED') or R.end_exp(Recorder.STATUS_S) ``` @@ -75,14 +77,17 @@ class QlibRecorder: ---------- experiment_name : str the name of the experiment to be started + recorder_name : str + name of the recorder under the experiment one wants to start. uri : str the tracking uri of the experiment, where all the artifacts/metrics etc. will be stored. + The default uri are set in the qlib.config. Returns ------- An experiment instance being started. """ - return self.exp_manager.start_exp(experiment_name, uri) + return self.exp_manager.start_exp(experiment_name, recorder_name, uri) def end_exp(self, recorder_status=Recorder.STATUS_FI): """ @@ -188,9 +193,9 @@ class QlibRecorder: 2) if id or name is specified, return the specified experiment. If no such exp found, create a new experiment with given id or name, and the experiment is set to be running. If R's not running: - 1) no id or name specified, create a default experiment. + 1) no id or name specified, create a default experiment, and the experiment is set to be running. 2) if id or name is specified, return the specified experiment. If no such exp found, - create a new experiment with given id or name, and the experiment is set to be running. + create a new experiment with given name or the default experiment, and the experiment is set to be running. Else If `create` is False: If R's running: 1) no id or name specified, return the active experiment. @@ -367,8 +372,7 @@ class QlibRecorder: # Case 1 with R.start('test'): pred = model.predict(dataset) - kwargs = {"pred.pkl": pred} - R.save_objects(**kwargs, artifact_path='prediction') + R.save_objects(**{"pred.pkl": pred}, artifact_path='prediction') # Case 2 with R.start('test'): diff --git a/qlib/workflow/cli.py b/qlib/workflow/cli.py new file mode 100644 index 000000000..6acbee66e --- /dev/null +++ b/qlib/workflow/cli.py @@ -0,0 +1,53 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +import sys +from pathlib import Path + +import qlib +import fire +import yaml +import pandas as pd +from qlib.config import REG_CN +from qlib.utils import init_instance_by_config +from qlib.workflow import R +from qlib.workflow.record_temp import SignalRecord + +# worflow handler function +def workflow(config_path): + with open(config_path) as fp: + config = yaml.load(fp, Loader=yaml.FullLoader) + + provider_uri = config.get("provider_uri") + qlib.init(provider_uri=provider_uri, region=REG_CN) + + # model initiaiton + model = init_instance_by_config(config.get("task")["model"]) + dataset = init_instance_by_config(config.get("task")["dataset"]) + + # start exp + with R.start("workflow"): + model.fit(dataset) + recorder = R.get_recorder() + + # generate records + for record in config.get("task")["record"]: + if record["class"] == SignalRecord.__name__: + srconf = {"model": model, "dataset": dataset, "recorder": recorder} + record["kwargs"].update(srconf) + sr = init_instance_by_config(record) + sr.generate() + else: + rconf = {"recorder": recorder} + record["kwargs"].update(rconf) + ar = init_instance_by_config(record) + ar.generate() + + +# function to run worklflow by config +def run(): + fire.Fire(workflow) + + +if __name__ == "__main__": + run() diff --git a/qlib/workflow/exp.py b/qlib/workflow/exp.py index b64b1544c..b7ef160df 100644 --- a/qlib/workflow/exp.py +++ b/qlib/workflow/exp.py @@ -2,7 +2,7 @@ # Licensed under the MIT License. import mlflow -from datetime import datetime +from mlflow.exceptions import MlflowException from pathlib import Path from .recorder import Recorder, MLflowRecorder from ..log import get_module_logger @@ -38,10 +38,15 @@ class Experiment: output["recorders"] = list(recorders.keys()) return output - def start(self): + def start(self, recorder_name=None): """ Start the experiment and set it to be active. This method will also start a new recorder. + Parameters + ---------- + recorder_name : str + the name of the recorder to be created. + Returns ------- An active recorder. @@ -59,10 +64,15 @@ class Experiment: """ raise NotImplementedError(f"Please implement the `end` method.") - def create_recorder(self): + def create_recorder(self, name=None): """ Create a recorder for each experiment. + Parameters + ---------- + name : str + the name of the recorder to be created. + Returns ------- A recorder object. @@ -126,6 +136,8 @@ class Experiment: the id of the recorder to be deleted. recorder_name : str the name of the recorder to be deleted. + create : boolean + create the recorder if it hasn't been created before. Returns ------- @@ -155,12 +167,12 @@ class MLflowExperiment(Experiment): self._default_name = None self.client = mlflow.tracking.MlflowClient(tracking_uri=self._uri) - def start(self): + def start(self, recorder_name=None): # set the active experiment mlflow.set_experiment(self.name) logger.info(f"Experiment {self.id} starts running ...") # set up recorder - recorder = self.create_recorder() + recorder = self.create_recorder(recorder_name) self.active_recorder = recorder # start the recorder run = self.active_recorder.start_run() @@ -172,11 +184,12 @@ class MLflowExperiment(Experiment): self.active_recorder.end_run(recorder_status) self.active_recorder = None - def create_recorder(self): - recorders = self.list_recorders() - num = len(recorders) - name = "Recorder_{}".format(num + 1) - recorder = MLflowRecorder(name, self.id, self._uri) + def create_recorder(self, recorder_name=None): + if recorder_name is None: + recorders = self.list_recorders() + num = len(recorders) + recorder_name = "Recorder_{}".format(num + 1) + recorder = MLflowRecorder(recorder_name, self.id, self._uri) return recorder @@ -197,14 +210,67 @@ class MLflowExperiment(Experiment): self.client.delete_run(recorder_id) else: recorders = self.list_recorders() - for r in recorders: - if recorders[r].name == recorder_name: - recorder_id = r - break - self.client.delete_run(recorder_id) - except: + recorder = self._get_recorder_by_name(recorder_name) + self.client.delete_run(recorder.id) + except MlflowException as e: raise Exception( - "Something went wrong when deleting recorder. Please check if the name/id of the recorder is correct." + f"Error: {e}. Something went wrong when deleting recorder. Please check if the name/id of the recorder is correct." + ) + + def _get_recorder_by_id(self, recorder_id=None, create=False): + """ + Get a recorder by its id. If the `create` is set to True, this method will also start to run the recorder. + + Parameters + ---------- + recorder_id : str + the id of the recorder to be returned. + create : boolean + create the recorder if it hasn't been created before. + + Returns + ------- + The specific recorder with given id. + """ + recorders = self.list_recorders() + if recorder_id in recorders: + return recorders[recorder_id] + else: + if create: + logger.warning(f"No valid recorder found. Create a new recorder with name {recorder_name}.") + self.start(recorder_name) + return self.active_recorder + else: + raise Exception( + "Something went wrong when retrieving recorders. Please check if id of the recorder is correct." + ) + + def _get_recorder_by_name(self, recorder_name=None, create=False): + """ + Get a recorder by its name. If the `create` is set to True, this method will also start to run the recorder. + + Parameters + ---------- + recorder_name : str + the name of the recorder to be returned. + create : boolean + create the recorder if it hasn't been created before. + + Returns + ------- + The specific recorder with given name. + """ + recorders = self.list_recorders() + for rid in recorders: + if recorders[rid].name == recorder_name: + return recorders[rid] + if create: + logger.warning(f"No valid recorder found. Create a new recorder with name {recorder_name}.") + self.start(recorder_name) + return self.active_recorder + else: + raise Exception( + "Something went wrong when retrieving recorders. Please check if the name of the experiment is correct." ) def get_recorder(self, recorder_id=None, recorder_name=None, create=True): @@ -213,68 +279,22 @@ class MLflowExperiment(Experiment): is set to True, this method will not automatically create an active recorder. """ # retrive all the recorders under this experiment - recorders = self.list_recorders() if recorder_id is None and recorder_name is None: if self.active_recorder: return self.active_recorder else: - if create: - self.start() - logger.warning( - f"Recorder {self.active_recorder.id} is running under the experiment with name {self.name}..." - ) - return self.active_recorder - else: - raise Exception( - "Something went wrong when retrieving recorders. Please check if QlibRecorder is running." - ) + return self._get_recorder_by_name(create=create) else: if recorder_id is not None: - if recorder_id in recorders: - return recorders[recorder_id] - else: - # mlflow does not support create a run with given id - raise Exception( - "Something went wrong when retrieving recorders. Please check if id of the recorder is correct." - ) + return self._get_recorder_by_id(recorder_id, create=create) else: - for rid in recorders: - if recorders[rid].name == recorder_name: - return recorders[rid] - if create: - recorders = self.list_recorders() - logger.warning(f"No valid recorder found. Create a new recorder with name {recorder_name}.") - recorder = self.create_recorder() - recorder.name = recorder_name - recorder.start_run() - return recorder - else: - raise Exception( - "Something went wrong when retrieving experiments. Please check if the name of the experiment is correct." - ) + return self._get_recorder_by_name(recorder_name, create=create) def list_recorders(self): - runs = self.client.list_run_infos(self.id, run_view_type=1)[::-1] + runs = self.client.search_runs(self.id, run_view_type=1)[::-1] recorders = dict() for i in range(len(runs)): - rid = runs[i].run_id - status = runs[i].status - start_time = runs[i].start_time - end_time = runs[i].end_time - recorder = MLflowRecorder(f"Recorder_{i+1}", self.id, self._uri) - recorder.id = rid - recorder.status = status - recorder.start_time = ( - datetime.fromtimestamp(float(start_time) / 1000.0).strftime("%Y-%m-%d %H:%M:%S") - if start_time is not None - else None - ) - recorder.end_time = ( - datetime.fromtimestamp(float(end_time) / 1000.0).strftime("%Y-%m-%d %H:%M:%S") - if end_time is not None - else None - ) - recorder._uri = self._uri - recorders[rid] = recorder + recorder = MLflowRecorder(f"Recorder_{i+1}", self.id, self._uri, runs[i]) + recorders[runs[i].info.run_id] = recorder return recorders diff --git a/qlib/workflow/expm.py b/qlib/workflow/expm.py index 1fe345577..00637e29d 100644 --- a/qlib/workflow/expm.py +++ b/qlib/workflow/expm.py @@ -2,6 +2,7 @@ # Licensed under the MIT License. import mlflow +from mlflow.exceptions import MlflowException import os from pathlib import Path from contextlib import contextmanager @@ -23,14 +24,17 @@ class ExpManager: self.default_exp_name = default_exp_name self.active_experiment = None # only one experiment can running each time - def start_exp(self, experiment_name=None, uri=None, **kwargs): + def start_exp(self, experiment_name=None, recorder_name=None, uri=None, **kwargs): """ - Start an experiment. + Start an experiment. This method includes first get_or_create an experiment, and then + set it to be running. Parameters ---------- experiment_name : str name of the active experiment. + recorder_name : str + name of the recorder to be started. uri : str the current tracking URI. @@ -38,14 +42,7 @@ class ExpManager: ------- An active experiment. """ - # create experiment - experiment = self.create_exp(experiment_name, uri) - # set up active experiment - self.active_experiment = experiment - # start the experiment - self.active_experiment.start() - - return self.active_experiment + raise NotImplementedError(f"Please implement the `start_exp` method.") def end_exp(self, recorder_status: str = Recorder.STATUS_S, **kwargs): """ @@ -58,9 +55,7 @@ class ExpManager: recorder_status : str the status of the active recorder of the experiment. """ - if self.active_experiment is not None: - self.active_experiment.end(recorder_status) - self.active_experiment = None + raise NotImplementedError(f"Please implement the `end_exp` method.") def search_records(self, experiment_ids=None, **kwargs): """ @@ -76,29 +71,15 @@ class ExpManager: """ raise NotImplementedError(f"Please implement the `search_records` method.") - def create_exp(self, experiment_name=None, uri=None): + def get_exp(self, experiment_id=None, experiment_name=None, create: bool = True, run: bool = False): """ - Create an experiment. + Retrieve an experiment. This method includes getting an active experiment, and get_or_create a specific experiment. + The returned experiment will be running. - Parameters - ---------- - experiment_name : str - the experiment name, which must be unique. - uri : str - the tracking uri of the experiment. - - Returns - ------- - An experiment object. - """ - raise NotImplementedError(f"Please implement the `create_exp` method.") - - def get_exp(self, experiment_id=None, experiment_name=None, create: bool = True): - """ - Retrieve an experiment. When user specify experiment id and name, the method will try to return the - specific experiment. When user does not provide recorder id or name, the method will try to return the current - active experiment. The `create` argument determines whether the method will automatically create a new experiment - according to user's specification if the experiment hasn't been created before + When user specify experiment id and name, the method will try to return the specific experiment. + When user does not provide recorder id or name, the method will try to return the current active experiment. + The `create` argument determines whether the method will automatically create a new experiment according + to user's specification if the experiment hasn't been created before If `create` is True: If R's running: @@ -122,9 +103,13 @@ class ExpManager: Parameters ---------- experiment_id : str - the experiment id to return. + id of the experiment to return. + experiment_name : str + name of the experiment to return. create : boolean - create the experiment if hasn't been created before. + create the experiment it if hasn't been created before. + run : boolean + run the experiment when it is created for the first time. Returns ------- @@ -175,9 +160,13 @@ class MLflowExpManager(ExpManager): super(MLflowExpManager, self).__init__(uri, default_exp_name) self.client = mlflow.tracking.MlflowClient(tracking_uri=self.uri) - def create_exp(self, experiment_name=None, uri=None): - # retrieve all created experiments - experiments = self.list_experiments() + def start_exp(self, experiment_name=None, recorder_name=None, uri=None): + # create experiment + experiment = self.get_exp(experiment_name=experiment_name, run=False) + # set up active experiment + self.active_experiment = experiment + # start the experiment + self.active_experiment.start(recorder_name) # set the tracking uri if uri is None: logger.info( @@ -186,35 +175,102 @@ class MLflowExpManager(ExpManager): else: self.uri = uri mlflow.set_tracking_uri(self.uri) - # start the experiment - if experiment_name is None: - logger.info( - f"No experiment name provided. The default experiment name is set as `{self.default_exp_name}`." - ) - if self.default_exp_name not in experiments: - experiment_id = self.client.create_experiment(self.default_exp_name) - else: - experiment_id = self.client.get_experiment_by_name(self.default_exp_name).experiment_id - # set the active experiment - mlflow.set_experiment(self.default_exp_name) - experiment_name = self.default_exp_name - else: - if experiment_name not in experiments: - if self.client.get_experiment_by_name(experiment_name) is not None: - logger.info( - "The experiment has already been created before. Try to resume the experiment with a new recorder..." - ) - experiment_id = self.client.get_experiment_by_name(experiment_name).experiment_id - else: - experiment_id = self.client.create_experiment(experiment_name) - else: - experiment_id = experiments[experiment_name].id - experiment = experiments[experiment_name] - # init experiment - experiment = MLflowExperiment(experiment_id, experiment_name, self.uri) - experiment._default_name = self.default_exp_name - return experiment + return self.active_experiment + + def end_exp(self, recorder_status: str = Recorder.STATUS_S): + if self.active_experiment is not None: + self.active_experiment.end(recorder_status) + self.active_experiment = None + + def __get_exp_by_id(self, experiment_id=None, create=False, run=False): + """ + Method for retrieving an experiment by its id. If the `create` is set to True, this method will also start to run the experiment. + + Parameters + ---------- + experiment_id : str + the id of the experiment to be returned. + create : boolean + create the experiment if it hasn't been created before. + + Returns + ------- + The specific experiment with given id. + """ + # retrive all created experiments + experiments = self.list_experiments() + for name in experiments: + if experiments[name].id == experiment_id: + return experiments[name] + if create: + logger.warning(f"No valid experiment found. Use the Default experiment for further process.") + return self.__get_exp_by_name(create=create, run=True) + else: + raise Exception( + "Something went wrong when retrieving experiments. Please check if QlibRecorder is running or the name/id of the experiment is correct." + ) + + def __get_exp_by_name(self, experiment_name=None, create=False, run=False): + """ + Method for retrieving an experiment by its name. If the `create` is set to True, this method will also start to run the experiment. + + Parameters + ---------- + experiment_name : str + the name of the experiment to be returned. + create : boolean + create the experiment if it hasn't been created before. + + Returns + ------- + The specific experiment with given name. + """ + # retrive all created experiments + experiments = self.list_experiments() + if experiment_name in experiments: + return experiments[experiment_name] + if create: + if experiment_name is None: + logger.info( + f"No experiment name provided. Create experiment with name {self.default_exp_name} for further process." + ) + experiment_name = self.default_exp_name + if self.client.get_experiment_by_name(experiment_name) is not None: + logger.info( + "The experiment has already been created before and deleted. Try to restore the experiment with a new recorder..." + ) + experiment_id = self.client.get_experiment_by_name(experiment_name).experiment_id + self.client.restore_experiment(experiment_id) + else: + experiment_id = self.client.create_experiment(experiment_name) + + # init experiment + experiment = MLflowExperiment(experiment_id, experiment_name, self.uri) + experiment._default_name = self.default_exp_name + if run: + self.active_experiment = experiment + self.active_experiment.start() + + return experiment + else: + if experiment_name is None and self.default_exp_name in experiments: + return experiments[self.default_exp_name] + raise Exception( + "Something went wrong when retrieving experiments. Please check if QlibRecorder is running or the name/id of the experiment is correct." + ) + + def get_exp(self, experiment_id=None, experiment_name=None, create=True, run=True): + if experiment_id is None and experiment_name is None: + if self.active_experiment: + return self.active_experiment + else: + return self.__get_exp_by_name(create=create, run=run) + else: + if experiment_name is not None: + return self.__get_exp_by_name(experiment_name, create=create, run=run) + else: + return self.__get_exp_by_id(experiment_id, create=create, run=run) def search_records(self, experiment_ids, **kwargs): filter_string = "" if kwargs.get("filter_string") is None else kwargs.get("filter_string") @@ -223,48 +279,6 @@ class MLflowExpManager(ExpManager): order_by = kwargs.get("order_by") return self.client.search_runs(experiment_ids, filter_string, run_view_type, max_results, order_by) - def get_exp(self, experiment_id=None, experiment_name=None, create=True): - # retrive all created experiments - experiments = self.list_experiments() - if experiment_id is None and experiment_name is None: - if self.active_experiment: - return self.active_experiment - else: - if create: - logger.warning("QlibRecorder is not running. Use the Default experiment for further process.") - return self.start_exp() - else: - if self.default_exp_name in experiments: - return experiments[self.default_exp_name] - raise Exception( - "Something went wrong when retrieving experiments. Please check if QlibRecorder is running or the name/id of the experiment is correct." - ) - else: - if experiment_name is not None: - if experiment_name in experiments: - return experiments[experiment_name] - else: - if create: - logger.warning( - f"No valid experiment found. Create experiment with name {experiment_name} for further process." - ) - return self.start_exp(experiment_name) - else: - raise Exception( - "Something went wrong when retrieving experiments. Please check if QlibRecorder is running or the name/id of the experiment is correct." - ) - else: - for name in experiments: - if experiments[name].id == experiment_id: - return experiments[name] - if create: - logger.warning(f"No valid experiment found. Use the Default experiment for further process.") - return self.start_exp() - else: - raise Exception( - "Something went wrong when retrieving experiments. Please check if QlibRecorder is running or the name/id of the experiment is correct." - ) - def delete_exp(self, experiment_id=None, experiment_name=None): assert ( experiment_id is not None or experiment_name is not None @@ -275,22 +289,19 @@ class MLflowExpManager(ExpManager): else: experiment = self.client.get_experiment_by_name(experiment_name) self.client.delete_experiment(experiment.experiment_id) - except: + except MlflowException as e: raise Exception( - "Something went wrong when deleting experiment. Please check if the name/id of the experiment is correct." + f"Error: {e}. Something went wrong when deleting experiment. Please check if the name/id of the experiment is correct." ) def list_experiments(self): # retrieve all the existing experiments exps = self.client.list_experiments(view_type=1) experiments = dict() - for i in range(len(exps)): - eid = exps[i].experiment_id - ename = exps[i].name + for exp in exps: + eid = exp.experiment_id + ename = exp.name experiment = MLflowExperiment(eid, ename, self.uri) - experiment.id = eid - experiment.name = ename - experiment._uri = self.uri experiments[ename] = experiment return experiments diff --git a/qlib/workflow/record_temp.py b/qlib/workflow/record_temp.py index 0c20f6efc..e3e19bd10 100644 --- a/qlib/workflow/record_temp.py +++ b/qlib/workflow/record_temp.py @@ -3,6 +3,7 @@ import pandas as pd from pathlib import Path +from pprint import pprint from ..contrib.evaluate import ( backtest as normal_backtest, risk_analysis, @@ -83,14 +84,14 @@ class SignalRecord(RecordTemp): logger.info( f"Signal record 'pred.pkl' has been saved as the artifact of the Experiment {self.recorder.experiment_id}" ) + # print out results + pprint(f"The following are prediction results of the {type(self.model).__name__} model.") + pprint(pred.head(5)) def load(self): # try to load the saved object - try: - pred = self.recorder.load_object("pred.pkl") - return pred - except: - raise Exception("Something went wrong when loading the saved object.") + pred = self.recorder.load_object("pred.pkl") + return pred def check(self, **kwargs): artifacts = self.recorder.list_artifacts() @@ -148,19 +149,51 @@ class PortAnaRecord(SignalRecord): analysis["excess_return_with_cost"] = risk_analysis( report_normal["return"] - report_normal["bench"] - report_normal["cost"] ) + # log metrics + self.recorder.log_metrics( + excess_return_without_cost_mean=analysis["excess_return_without_cost"]["risk"]["mean"] + ) + self.recorder.log_metrics(excess_return_without_cost_std=analysis["excess_return_without_cost"]["risk"]["std"]) + self.recorder.log_metrics( + excess_return_without_cost_annualized_return=analysis["excess_return_without_cost"]["risk"][ + "annualized_return" + ] + ) + self.recorder.log_metrics( + excess_return_without_cost_information_ratio=analysis["excess_return_without_cost"]["risk"][ + "information_ratio" + ] + ) + self.recorder.log_metrics( + excess_return_without_cost_max_drawdown=analysis["excess_return_without_cost"]["risk"]["max_drawdown"] + ) + self.recorder.log_metrics(excess_return_with_cost_mean=analysis["excess_return_with_cost"]["risk"]["mean"]) + self.recorder.log_metrics(excess_return_with_cost_std=analysis["excess_return_with_cost"]["risk"]["std"]) + self.recorder.log_metrics( + excess_return_with_cost_annualized_return=analysis["excess_return_with_cost"]["risk"]["annualized_return"] + ) + self.recorder.log_metrics( + excess_return_with_cost_information_ratio=analysis["excess_return_with_cost"]["risk"]["information_ratio"] + ) + self.recorder.log_metrics( + excess_return_with_cost_max_drawdown=analysis["excess_return_with_cost"]["risk"]["max_drawdown"] + ) + # save portfolio analysis results analysis_df = pd.concat(analysis) # type: pd.DataFrame self.recorder.save_objects(**{"port_analysis.pkl": analysis_df}, artifact_path=self.artifact_path) logger.info( f"Portfolio analysis record 'port_analysis.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 load(self): # try to load the saved object - try: - pred = self.recorder.load_object(self.artifact_path / "port_analysis.pkl") - return pred - except: - raise Exception("Something went wrong when loading the saved object.") + pred = self.recorder.load_object(self.artifact_path / "port_analysis.pkl") + return pred def check(self): artifacts = self.recorder.list_artifacts(self.artifact_path) diff --git a/qlib/workflow/recorder.py b/qlib/workflow/recorder.py index 6c83641a9..a2cddadcb 100644 --- a/qlib/workflow/recorder.py +++ b/qlib/workflow/recorder.py @@ -2,8 +2,9 @@ # Licensed under the MIT License. import mlflow -import shutil, os, pickle, tempfile, codecs, datetime +import shutil, os, pickle, tempfile, codecs from pathlib import Path +from datetime import datetime from ..utils.objm import FileManager from ..log import get_module_logger @@ -167,7 +168,7 @@ class MLflowRecorder(Recorder): use file manager to help maintain the objects in the project. """ - def __init__(self, name, experiment_id, uri): + def __init__(self, name, experiment_id, uri, mlflow_run=None): super(MLflowRecorder, self).__init__(name, experiment_id) self._uri = uri self.artifact_uri = None @@ -175,6 +176,22 @@ class MLflowRecorder(Recorder): self.temp_dir = tempfile.mkdtemp() self.fm = FileManager(Path(self.temp_dir).absolute()) self.client = mlflow.tracking.MlflowClient(tracking_uri=self._uri) + # construct from mlflow run + if mlflow_run is not None: + assert isinstance(mlflow_run, mlflow.entities.run.Run), "Please input with a MLflow Run object." + self.name = mlflow_run.data.tags["mlflow.runName"] if mlflow_run.data.tags["mlflow.runName"] != "" else name + self.id = mlflow_run.info.run_id + self.status = mlflow_run.info.status + self.start_time = ( + datetime.fromtimestamp(float(mlflow_run.info.start_time) / 1000.0).strftime("%Y-%m-%d %H:%M:%S") + if mlflow_run.info.start_time is not None + else None + ) + self.end_time = ( + datetime.fromtimestamp(float(mlflow_run.info.end_time) / 1000.0).strftime("%Y-%m-%d %H:%M:%S") + if mlflow_run.info.end_time is not None + else None + ) def start_run(self): # start the run @@ -182,7 +199,7 @@ class MLflowRecorder(Recorder): # save the run id and artifact_uri self.id = run.info.run_id self.artifact_uri = run.info.artifact_uri - self.start_time = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") + self.start_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S") self.status = Recorder.STATUS_R logger.info(f"Recorder {self.id} starts running under Experiment {self.experiment_id} ...") @@ -196,7 +213,7 @@ class MLflowRecorder(Recorder): Recorder.STATUS_FA, ], f"The status type {status} is not supported." mlflow.end_run(status) - self.end_time = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") + self.end_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S") if self.status != Recorder.STATUS_S: self.status = status shutil.rmtree(self.temp_dir) @@ -213,31 +230,23 @@ class MLflowRecorder(Recorder): def load_object(self, name): assert self._uri is not None, "Please start the experiment and recorder first before using recorder directly." path = self.client.download_artifacts(self.id, name) - try: - with Path(path).open("rb") as f: - f.seek(0) - return pickle.load(f) - except: - with codecs.open(path, mode="r", encoding="utf-8") as f: - return f.read() + with Path(path).open("rb") as f: + return pickle.load(f) def log_params(self, **kwargs): - keys = list(kwargs.keys()) for name, data in kwargs.items(): self.client.log_param(self.id, name, data) def log_metrics(self, step=None, **kwargs): - keys = list(kwargs.keys()) for name, data in kwargs.items(): self.client.log_metric(self.id, name, data) def set_tags(self, **kwargs): - keys = list(kwargs.keys()) for name, data in kwargs.items(): self.client.set_tag(self.id, name, data) def delete_tags(self, *keys): - for count, key in enumerate(keys): + for key in keys: self.client.delete_tag(self.id, key) def get_artifact_uri(self): diff --git a/qlib/workflow/utils.py b/qlib/workflow/utils.py index 196669fea..d4594d28e 100644 --- a/qlib/workflow/utils.py +++ b/qlib/workflow/utils.py @@ -1,13 +1,25 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. -import sys, traceback, signal +import sys, traceback, signal, atexit from . import R from .recorder import Recorder from ..log import get_module_logger logger = get_module_logger("workflow", "INFO") +# function to handle the experiment when unusual program ending occurs +def experiment_exit_handler(): + """ + Method for handling the experiment when any unusual program ending occurs. + The `atexit` handler should be put in the last, since, as long as the program ends, it will be called. + Thus, if any exception or user interuption occurs beforehead, we should handle them first. Once `R` is + ended, another call of `R.end_exp` will not take effect. + """ + signal.signal(signal.SIGINT, experiment_kill_signal_handler) # handle user keyboard interupt + sys.excepthook = experiment_exception_hook # handle uncaught exception + atexit.register(R.end_exp, recorder_status=Recorder.STATUS_FI) # will not take effect if experiment ends + def experiment_exception_hook(type, value, tb): """ @@ -19,15 +31,16 @@ def experiment_exception_hook(type, value, tb): value: Exception's value tb: Exception's traceback """ - error_msg = "An exception has been raised.\n" f"Type: {type}\n" f"Value: {value}\n" + error_msg = "An exception has been raised.\n" f"Type: {type}\n" logger.error(error_msg) traceback.print_tb(tb) + logger.error(f"Value: {value}") R.end_exp(recorder_status=Recorder.STATUS_FA) def experiment_kill_signal_handler(signum, frame): """ - End an experiment when user kill the program (CTRL+C, etc.). + End an experiment when user kill the program through keyboard (CTRL+C, etc.). """ R.end_exp(recorder_status=Recorder.STATUS_FA) diff --git a/setup.py b/setup.py index 38a84ef7c..8ad124750 100644 --- a/setup.py +++ b/setup.py @@ -100,7 +100,7 @@ setup( entry_points={ # 'console_scripts': ['mycli=mymodule:cli'], "console_scripts": [ - "estimator=qlib.contrib.estimator.launcher:run", + "workflow_by_config=qlib.workflow.cli:run", ], }, ext_modules=extensions, From 58bd2339c050152503be1fc21b3bcb5954fcb0ee Mon Sep 17 00:00:00 2001 From: Jactus Date: Wed, 18 Nov 2020 17:55:45 +0800 Subject: [PATCH 052/241] Update expm and exp --- qlib/utils/__init__.py | 25 +++++ qlib/workflow/__init__.py | 1 - qlib/workflow/exp.py | 140 ++++++++++++---------------- qlib/workflow/expm.py | 171 +++++++++++++++++------------------ qlib/workflow/record_temp.py | 33 +------ qlib/workflow/recorder.py | 8 +- qlib/workflow/utils.py | 5 +- 7 files changed, 176 insertions(+), 207 deletions(-) diff --git a/qlib/utils/__init__.py b/qlib/utils/__init__.py index 30483af2e..575ed24aa 100644 --- a/qlib/utils/__init__.py +++ b/qlib/utils/__init__.py @@ -20,6 +20,7 @@ import requests import tempfile import importlib import contextlib +import collections import numpy as np import pandas as pd from pathlib import Path @@ -641,6 +642,30 @@ def lexsort_index(df: pd.DataFrame) -> pd.DataFrame: return df.sort_index() +def flatten_dict(d, parent_key="", sep="."): + """flatten_dict. + >>> flatten_dict({'a': 1, 'c': {'a': 2, 'b': {'x': 5, 'y' : 10}}, 'd': [1, 2, 3]}) + >>> {'a': 1, 'c.a': 2, 'c.b.x': 5, 'd': [1, 2, 3], 'c.b.y': 10} + + Parameters + ---------- + d : + d + parent_key : + parent_key + sep : + sep + """ + items = [] + for k, v in d.items(): + new_key = parent_key + sep + k if parent_key else k + if isinstance(v, collections.MutableMapping): + items.extend(flatten_dict(v, new_key, sep=sep).items()) + else: + items.append((new_key, v)) + return dict(items) + + #################### Wrapper ##################### class Wrapper(object): """Wrapper class for anything that needs to set up during qlib.init""" diff --git a/qlib/workflow/__init__.py b/qlib/workflow/__init__.py index 6a8b857fc..9da65480f 100644 --- a/qlib/workflow/__init__.py +++ b/qlib/workflow/__init__.py @@ -323,7 +323,6 @@ class QlibRecorder: experiment_name : str name of the experiment. - Returns ------- A recorder instance. diff --git a/qlib/workflow/exp.py b/qlib/workflow/exp.py index b7ef160df..5a74ab28a 100644 --- a/qlib/workflow/exp.py +++ b/qlib/workflow/exp.py @@ -165,6 +165,7 @@ class MLflowExperiment(Experiment): super(MLflowExperiment, self).__init__(id, name) self._uri = uri self._default_name = None + self._default_rec_name = "mlflow_recorder" self.client = mlflow.tracking.MlflowClient(tracking_uri=self._uri) def start(self, recorder_name=None): @@ -175,7 +176,7 @@ class MLflowExperiment(Experiment): recorder = self.create_recorder(recorder_name) self.active_recorder = recorder # start the recorder - run = self.active_recorder.start_run() + self.active_recorder.start_run() return self.active_recorder @@ -186,13 +187,66 @@ class MLflowExperiment(Experiment): def create_recorder(self, recorder_name=None): if recorder_name is None: - recorders = self.list_recorders() - num = len(recorders) - recorder_name = "Recorder_{}".format(num + 1) - recorder = MLflowRecorder(recorder_name, self.id, self._uri) + recorder_name = self._default_rec_name + recorder = MLflowRecorder(self.id, self._uri, recorder_name) return recorder + def get_recorder(self, recorder_id=None, recorder_name=None, create=True): + # special case of getting the recorder + if recorder_id is None and recorder_name is None: + if self.active_recorder is not None: + return self.active_recorder + recorder_name = self._default_rec_name + if create: + recorder, is_new = self._get_or_create_rec(recorder_id=recorder_id, recorder_name=recorder_name) + else: + recorder, is_new = self._get_recorder(recorder_id=recorder_id, recorder_name=recorder_name), False + if is_new: + mlflow.set_experiment(self.name) + self.active_recorder = recorder + # start the recorder + self.active_recorder.start_run() + return recorder + + def _get_or_create_rec(self, recorder_id=None, recorder_name=None) -> (object, bool): + """ + Method for getting or creating a recorder. It will try to first get a valid recorder, if exception occurs, it will + automatically create a new recorder based on the given id and name. + """ + try: + return self._get_recorder(recorder_id=recorder_id, recorder_name=recorder_name), False + except ValueError: + if recorder_name is None: + recorder_name = self._default_rec_name + logger.info(f"No valid recorder found. Create a new recorder with name {recorder_name}.") + return self.create(recorder_name), True + + def _get_recorder(self, recorder_id=None, recorder_name=None): + """ + Method for getting or creating a recorder. It will try to first get a valid recorder, if exception occurs, it will + raise errors. + """ + assert ( + recorder_id is not None or recorder_name is not None + ), "Please input at least one of recorder id or name before retrieving recorder." + if recorder_id is not None: + try: + run = self.client.get_run(recorder_id) + recorder = MLflowRecorder(self.id, self._uri, mlflow_run=run) + return recorder + except MlflowException as e: + raise ValueError("No valid recorder has been found, please make sure the input recorder id is correct.") + elif recorder_name is not None: + logger.warning( + f"Please make sure the recorder name {recorder_name} is unique, we will only return the first recorder if there exist several matched the given name." + ) + recorders = self.list_recorders() + for rid in recorders: + if recorders[rid].name == recorder_name: + return recorders[rid] + raise ValueError("No valid recorder has been found, please make sure the input recorder name is correct.") + def search_records(self, **kwargs): filter_string = "" if kwargs.get("filter_string") is None else kwargs.get("filter_string") run_view_type = 1 if kwargs.get("run_view_type") is None else kwargs.get("run_view_type") @@ -209,7 +263,6 @@ class MLflowExperiment(Experiment): if recorder_id is not None: self.client.delete_run(recorder_id) else: - recorders = self.list_recorders() recorder = self._get_recorder_by_name(recorder_name) self.client.delete_run(recorder.id) except MlflowException as e: @@ -217,84 +270,11 @@ class MLflowExperiment(Experiment): f"Error: {e}. Something went wrong when deleting recorder. Please check if the name/id of the recorder is correct." ) - def _get_recorder_by_id(self, recorder_id=None, create=False): - """ - Get a recorder by its id. If the `create` is set to True, this method will also start to run the recorder. - - Parameters - ---------- - recorder_id : str - the id of the recorder to be returned. - create : boolean - create the recorder if it hasn't been created before. - - Returns - ------- - The specific recorder with given id. - """ - recorders = self.list_recorders() - if recorder_id in recorders: - return recorders[recorder_id] - else: - if create: - logger.warning(f"No valid recorder found. Create a new recorder with name {recorder_name}.") - self.start(recorder_name) - return self.active_recorder - else: - raise Exception( - "Something went wrong when retrieving recorders. Please check if id of the recorder is correct." - ) - - def _get_recorder_by_name(self, recorder_name=None, create=False): - """ - Get a recorder by its name. If the `create` is set to True, this method will also start to run the recorder. - - Parameters - ---------- - recorder_name : str - the name of the recorder to be returned. - create : boolean - create the recorder if it hasn't been created before. - - Returns - ------- - The specific recorder with given name. - """ - recorders = self.list_recorders() - for rid in recorders: - if recorders[rid].name == recorder_name: - return recorders[rid] - if create: - logger.warning(f"No valid recorder found. Create a new recorder with name {recorder_name}.") - self.start(recorder_name) - return self.active_recorder - else: - raise Exception( - "Something went wrong when retrieving recorders. Please check if the name of the experiment is correct." - ) - - def get_recorder(self, recorder_id=None, recorder_name=None, create=True): - """ - MLflow doesn't support create recorder with a specific id. Thus, when user only provides recorder id and `create` - is set to True, this method will not automatically create an active recorder. - """ - # retrive all the recorders under this experiment - if recorder_id is None and recorder_name is None: - if self.active_recorder: - return self.active_recorder - else: - return self._get_recorder_by_name(create=create) - else: - if recorder_id is not None: - return self._get_recorder_by_id(recorder_id, create=create) - else: - return self._get_recorder_by_name(recorder_name, create=create) - def list_recorders(self): runs = self.client.search_runs(self.id, run_view_type=1)[::-1] recorders = dict() for i in range(len(runs)): - recorder = MLflowRecorder(f"Recorder_{i+1}", self.id, self._uri, runs[i]) + recorder = MLflowRecorder(self.id, self._uri, mlflow_run=runs[i]) recorders[runs[i].info.run_id] = recorder return recorders diff --git a/qlib/workflow/expm.py b/qlib/workflow/expm.py index 00637e29d..8fb7962e9 100644 --- a/qlib/workflow/expm.py +++ b/qlib/workflow/expm.py @@ -57,6 +57,21 @@ class ExpManager: """ raise NotImplementedError(f"Please implement the `end_exp` method.") + def create_exp(self, experiment_name=None): + """ + Create an experiment. + + Parameters + ---------- + experiment_name : str + the experiment name, which must be unique. + + Returns + ------- + An experiment object. + """ + raise NotImplementedError(f"Please implement the `create_exp` method.") + def search_records(self, experiment_ids=None, **kwargs): """ Get a pandas DataFrame of records that fit the search criteria of the experiment. @@ -71,7 +86,7 @@ class ExpManager: """ raise NotImplementedError(f"Please implement the `search_records` method.") - def get_exp(self, experiment_id=None, experiment_name=None, create: bool = True, run: bool = False): + def get_exp(self, experiment_id=None, experiment_name=None, create: bool = True): """ Retrieve an experiment. This method includes getting an active experiment, and get_or_create a specific experiment. The returned experiment will be running. @@ -108,8 +123,6 @@ class ExpManager: name of the experiment to return. create : boolean create the experiment it if hasn't been created before. - run : boolean - run the experiment when it is created for the first time. Returns ------- @@ -162,7 +175,7 @@ class MLflowExpManager(ExpManager): def start_exp(self, experiment_name=None, recorder_name=None, uri=None): # create experiment - experiment = self.get_exp(experiment_name=experiment_name, run=False) + experiment, _ = self._get_or_create_exp(experiment_name=experiment_name) # set up active experiment self.active_experiment = experiment # start the experiment @@ -183,94 +196,72 @@ class MLflowExpManager(ExpManager): self.active_experiment.end(recorder_status) self.active_experiment = None - def __get_exp_by_id(self, experiment_id=None, create=False, run=False): - """ - Method for retrieving an experiment by its id. If the `create` is set to True, this method will also start to run the experiment. + def create_exp(self, experiment_name=None): + # init experiment + experiment_id = self.client.create_experiment(experiment_name) + experiment = MLflowExperiment(experiment_id, experiment_name, self.uri) + experiment._default_name = self.default_exp_name - Parameters - ---------- - experiment_id : str - the id of the experiment to be returned. - create : boolean - create the experiment if it hasn't been created before. + return experiment - Returns - ------- - The specific experiment with given id. - """ - # retrive all created experiments - experiments = self.list_experiments() - for name in experiments: - if experiments[name].id == experiment_id: - return experiments[name] - if create: - logger.warning(f"No valid experiment found. Use the Default experiment for further process.") - return self.__get_exp_by_name(create=create, run=True) - else: - raise Exception( - "Something went wrong when retrieving experiments. Please check if QlibRecorder is running or the name/id of the experiment is correct." - ) - - def __get_exp_by_name(self, experiment_name=None, create=False, run=False): - """ - Method for retrieving an experiment by its name. If the `create` is set to True, this method will also start to run the experiment. - - Parameters - ---------- - experiment_name : str - the name of the experiment to be returned. - create : boolean - create the experiment if it hasn't been created before. - - Returns - ------- - The specific experiment with given name. - """ - # retrive all created experiments - experiments = self.list_experiments() - if experiment_name in experiments: - return experiments[experiment_name] - if create: - if experiment_name is None: - logger.info( - f"No experiment name provided. Create experiment with name {self.default_exp_name} for further process." - ) - experiment_name = self.default_exp_name - if self.client.get_experiment_by_name(experiment_name) is not None: - logger.info( - "The experiment has already been created before and deleted. Try to restore the experiment with a new recorder..." - ) - experiment_id = self.client.get_experiment_by_name(experiment_name).experiment_id - self.client.restore_experiment(experiment_id) - else: - experiment_id = self.client.create_experiment(experiment_name) - - # init experiment - experiment = MLflowExperiment(experiment_id, experiment_name, self.uri) - experiment._default_name = self.default_exp_name - if run: - self.active_experiment = experiment - self.active_experiment.start() - - return experiment - else: - if experiment_name is None and self.default_exp_name in experiments: - return experiments[self.default_exp_name] - raise Exception( - "Something went wrong when retrieving experiments. Please check if QlibRecorder is running or the name/id of the experiment is correct." - ) - - def get_exp(self, experiment_id=None, experiment_name=None, create=True, run=True): + def get_exp(self, experiment_id=None, experiment_name=None, create=True): + # special case of getting experiment if experiment_id is None and experiment_name is None: - if self.active_experiment: + if self.active_experiment is not None: return self.active_experiment - else: - return self.__get_exp_by_name(create=create, run=run) + if create: + exp, is_new = self._get_or_create_exp(experiment_id=experiment_id, experiment_name=experiment_name) else: - if experiment_name is not None: - return self.__get_exp_by_name(experiment_name, create=create, run=run) - else: - return self.__get_exp_by_id(experiment_id, create=create, run=run) + exp, is_new = self._get_exp(experiment_id=experiment_id, experiment_name=experiment_name), False + if is_new: + self.active_experiment = exp + # start the recorder + self.active_experiment.start() + return exp + + def _get_or_create_exp(self, experiment_id=None, experiment_name=None) -> (object, bool): + """ + Method for getting or creating an experiment. It will try to first get a valid experiment, if exception occurs, it will + automatically create a new experiment based on the given id and name. + """ + try: + return self._get_exp(experiment_id=experiment_id, experiment_name=experiment_name), False + except ValueError: + if experiment_name is None: + experiment = self.default_exp_name + logger.info(f"No valid experiment found. Create a new experiment with name {experiment_name}.") + return self.create_exp(experiment_name), True + + def _get_exp(self, experiment_id=None, experiment_name=None): + """ + Method for getting or creating an experiment. It will try to first get a valid experiment, if exception occurs, it will + raise errors. + """ + assert ( + experiment_id is not None or experiment_name is not None + ), "Please input at least one of experiment/recorder id or name before retrieving experiment/recorder." + if experiment_id is not None: + try: + exp = self.client.get_experiment(experiment_id) + if exp.lifecycle_stage.upper() == "DELETED": + raise MlflowException("No valid experiment has been found.") + experiment = MLflowExperiment(exp.experiment_id, exp.name, self.uri) + return experiment + except MlflowException as e: + raise ValueError( + "No valid experiment has been found, please make sure the input experiment id is correct." + ) + elif experiment_name is not None: + try: + exp = self.client.get_experiment_by_name(experiment_name) + if exp is None or exp.lifecycle_stage.upper() == "DELETED": + raise MlflowException("No valid experiment has been found.") + experiment = MLflowExperiment(exp.experiment_id, experiment_name, self.uri) + return experiment + except MlflowException as e: + raise ValueError( + "No valid experiment has been found, please make sure the input experiment name is correct." + ) def search_records(self, experiment_ids, **kwargs): filter_string = "" if kwargs.get("filter_string") is None else kwargs.get("filter_string") @@ -288,6 +279,8 @@ class MLflowExpManager(ExpManager): self.client.delete_experiment(experiment_id) else: experiment = self.client.get_experiment_by_name(experiment_name) + if experiment is None: + raise MlflowException("No valid experiment has been found.") self.client.delete_experiment(experiment.experiment_id) except MlflowException as e: raise Exception( @@ -299,9 +292,7 @@ class MLflowExpManager(ExpManager): exps = self.client.list_experiments(view_type=1) experiments = dict() for exp in exps: - eid = exp.experiment_id - ename = exp.name - experiment = MLflowExperiment(eid, ename, self.uri) + experiment = MLflowExperiment(exp.experiment_id, exp.name, self.uri) experiments[ename] = experiment return experiments diff --git a/qlib/workflow/record_temp.py b/qlib/workflow/record_temp.py index e3e19bd10..d6b4d608e 100644 --- a/qlib/workflow/record_temp.py +++ b/qlib/workflow/record_temp.py @@ -10,6 +10,7 @@ from ..contrib.evaluate import ( ) from ..utils import init_instance_by_config, get_module_by_module_path from ..log import get_module_logger +from ..utils import flatten_dict logger = get_module_logger("workflow", "INFO") @@ -149,37 +150,11 @@ class PortAnaRecord(SignalRecord): analysis["excess_return_with_cost"] = risk_analysis( report_normal["return"] - report_normal["bench"] - report_normal["cost"] ) - # log metrics - self.recorder.log_metrics( - excess_return_without_cost_mean=analysis["excess_return_without_cost"]["risk"]["mean"] - ) - self.recorder.log_metrics(excess_return_without_cost_std=analysis["excess_return_without_cost"]["risk"]["std"]) - self.recorder.log_metrics( - excess_return_without_cost_annualized_return=analysis["excess_return_without_cost"]["risk"][ - "annualized_return" - ] - ) - self.recorder.log_metrics( - excess_return_without_cost_information_ratio=analysis["excess_return_without_cost"]["risk"][ - "information_ratio" - ] - ) - self.recorder.log_metrics( - excess_return_without_cost_max_drawdown=analysis["excess_return_without_cost"]["risk"]["max_drawdown"] - ) - self.recorder.log_metrics(excess_return_with_cost_mean=analysis["excess_return_with_cost"]["risk"]["mean"]) - self.recorder.log_metrics(excess_return_with_cost_std=analysis["excess_return_with_cost"]["risk"]["std"]) - self.recorder.log_metrics( - excess_return_with_cost_annualized_return=analysis["excess_return_with_cost"]["risk"]["annualized_return"] - ) - self.recorder.log_metrics( - excess_return_with_cost_information_ratio=analysis["excess_return_with_cost"]["risk"]["information_ratio"] - ) - self.recorder.log_metrics( - excess_return_with_cost_max_drawdown=analysis["excess_return_with_cost"]["risk"]["max_drawdown"] - ) # save portfolio analysis results 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(**{"port_analysis.pkl": analysis_df}, artifact_path=self.artifact_path) logger.info( f"Portfolio analysis record 'port_analysis.pkl' has been saved as the artifact of the Experiment {self.recorder.experiment_id}" diff --git a/qlib/workflow/recorder.py b/qlib/workflow/recorder.py index a2cddadcb..71f13381f 100644 --- a/qlib/workflow/recorder.py +++ b/qlib/workflow/recorder.py @@ -25,7 +25,7 @@ class Recorder: STATUS_FI = "FINISHED" STATUS_FA = "FAILED" - def __init__(self, name, experiment_id): + def __init__(self, experiment_id, name): self.id = None self.name = name self.experiment_id = experiment_id @@ -168,8 +168,8 @@ class MLflowRecorder(Recorder): use file manager to help maintain the objects in the project. """ - def __init__(self, name, experiment_id, uri, mlflow_run=None): - super(MLflowRecorder, self).__init__(name, experiment_id) + def __init__(self, experiment_id, uri, name=None, mlflow_run=None): + super(MLflowRecorder, self).__init__(experiment_id, name) self._uri = uri self.artifact_uri = None # set up file manager for saving objects @@ -179,7 +179,7 @@ class MLflowRecorder(Recorder): # construct from mlflow run if mlflow_run is not None: assert isinstance(mlflow_run, mlflow.entities.run.Run), "Please input with a MLflow Run object." - self.name = mlflow_run.data.tags["mlflow.runName"] if mlflow_run.data.tags["mlflow.runName"] != "" else name + self.name = mlflow_run.data.tags["mlflow.runName"] self.id = mlflow_run.info.run_id self.status = mlflow_run.info.status self.start_time = ( diff --git a/qlib/workflow/utils.py b/qlib/workflow/utils.py index d4594d28e..b57879d0e 100644 --- a/qlib/workflow/utils.py +++ b/qlib/workflow/utils.py @@ -31,10 +31,9 @@ def experiment_exception_hook(type, value, tb): value: Exception's value tb: Exception's traceback """ - error_msg = "An exception has been raised.\n" f"Type: {type}\n" - logger.error(error_msg) + logger.error("An exception has been raised.") traceback.print_tb(tb) - logger.error(f"Value: {value}") + print(f"{type}: {value}") R.end_exp(recorder_status=Recorder.STATUS_FA) From d8414b949ae7aba700365543645b513082c8517d Mon Sep 17 00:00:00 2001 From: Jactus Date: Wed, 18 Nov 2020 19:17:19 +0800 Subject: [PATCH 053/241] Update pytorch_nn --- qlib/contrib/model/pytorch_nn.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/qlib/contrib/model/pytorch_nn.py b/qlib/contrib/model/pytorch_nn.py index 82b7d0950..9bad755b6 100644 --- a/qlib/contrib/model/pytorch_nn.py +++ b/qlib/contrib/model/pytorch_nn.py @@ -193,7 +193,6 @@ class DNNModelPytorch(Model): w_val_auto = w_val_auto.cuda() for step in range(self.max_steps): - self.logger.info(step) if stop_steps >= self.early_stop_rounds: if verbose: self.logger.info("\tearly stop") @@ -201,7 +200,6 @@ class DNNModelPytorch(Model): loss = AverageMeter() self.dnn_model.train() self.train_optimizer.zero_grad() - self.logger.info("INIT") choice = np.random.choice(train_num, self.batch_size) x_batch_auto = x_train_values[choice] From e6a902c659f511186c571940d6cdbf5935010d2b Mon Sep 17 00:00:00 2001 From: lwwang1995 Date: Wed, 18 Nov 2020 21:53:28 +0800 Subject: [PATCH 054/241] Add LSTM and Gats --- examples/workflow_by_code_gats.py | 145 +++++++++++ examples/workflow_by_code_lstm.py | 144 +++++++++++ qlib/contrib/model/pytorch_gats.py | 383 +++++++++++++++++++++++++++++ qlib/contrib/model/pytorch_lstm.py | 340 +++++++++++++++++++++++++ 4 files changed, 1012 insertions(+) create mode 100755 examples/workflow_by_code_gats.py create mode 100755 examples/workflow_by_code_lstm.py create mode 100755 qlib/contrib/model/pytorch_gats.py create mode 100755 qlib/contrib/model/pytorch_lstm.py diff --git a/examples/workflow_by_code_gats.py b/examples/workflow_by_code_gats.py new file mode 100755 index 000000000..06845d448 --- /dev/null +++ b/examples/workflow_by_code_gats.py @@ -0,0 +1,145 @@ +# 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.pytorch_gats import GAT +from qlib.contrib.data.handler import ALPHA360_Denoise +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 + +# from qlib.model.learner import train_model +from qlib.utils import init_instance_by_config + +import pickle + +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_cn(target_dir=provider_uri) + + qlib.init(provider_uri=provider_uri, region=REG_CN) + + MARKET = "csi300" + BENCHMARK = "SH000300" + + ################################### + # train model + ################################### + DATA_HANDLER_CONFIG = { + "start_time": "2008-01-01", + "end_time": "2020-08-01", + "fit_start_time": "2008-01-01", + "fit_end_time": "2014-12-31", + "instruments": MARKET, + } + + TRAINER_CONFIG = { + "train_start_time": "2008-01-01", + "train_end_time": "2014-12-31", + "validate_start_time": "2015-01-01", + "validate_end_time": "2016-12-31", + "test_start_time": "2017-01-01", + "test_end_time": "2020-08-01", + } + + task = { + "model": { + "class": "GAT", + "module_path": "qlib.contrib.model.pytorch_gats", + "kwargs": { + "d_feat": 6, + "hidden_size": 64, + "num_layers": 2, + "dropout": 0.0, + "n_epochs": 200, + "lr": 1e-3, + "early_stop": 20, + "batch_size": 800, + "metric": "IC", + "loss": "mse", + "base_model":"GRU", + "seed": 0, + "GPU": 0, + }, + }, + "dataset": { + "class": "DatasetH", + "module_path": "qlib.data.dataset", + "kwargs": { + "handler": { + "class": "ALPHA360_Denoise", + "module_path": "qlib.contrib.data.handler", + "kwargs": DATA_HANDLER_CONFIG, + }, + "segments": { + "train": ("2008-01-01", "2014-12-31"), + "valid": ("2015-01-01", "2016-12-31"), + "test": ("2017-01-01", "2020-08-01"), + }, + }, + } + # You shoud record the data in specific sequence + # "record": ['SignalRecord', 'SigAnaRecord', 'PortAnaRecord'], + } + + # model = train_model(task) + model = init_instance_by_config(task["model"]) + dataset = init_instance_by_config(task["dataset"]) + model.fit(dataset) + + pred_score = model.predict(dataset) + + # save pred_score to file + pred_score_path = Path("~/tmp/qlib/pred_score.pkl").expanduser() + pred_score_path.parent.mkdir(exist_ok=True, parents=True) + pred_score.to_pickle(pred_score_path) + + ################################### + # backtest + ################################### + STRATEGY_CONFIG = { + "topk": 50, + "n_drop": 5, + } + BACKTEST_CONFIG = { + "verbose": False, + "limit_threshold": 0.095, + "account": 100000000, + "benchmark": BENCHMARK, + "deal_price": "close", + "open_cost": 0.0005, + "close_cost": 0.0015, + "min_cost": 5, + } + + # use default strategy + # custom Strategy, refer to: TODO: Strategy API url + strategy = TopkDropoutStrategy(**STRATEGY_CONFIG) + report_normal, positions_normal = normal_backtest(pred_score, strategy=strategy, **BACKTEST_CONFIG) + + ################################### + # analyze + # If need a more detailed analysis, refer to: examples/train_and_bakctest.ipynb + ################################### + 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 + print(analysis_df) diff --git a/examples/workflow_by_code_lstm.py b/examples/workflow_by_code_lstm.py new file mode 100755 index 000000000..1815d2fec --- /dev/null +++ b/examples/workflow_by_code_lstm.py @@ -0,0 +1,144 @@ +# 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.pytorch_lstm import LSTM +from qlib.contrib.data.handler import ALPHA360_Denoise +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 + +# from qlib.model.learner import train_model +from qlib.utils import init_instance_by_config + +import pickle + +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_cn(target_dir=provider_uri) + + qlib.init(provider_uri=provider_uri, region=REG_CN) + + MARKET = "csi300" + BENCHMARK = "SH000300" + + ################################### + # train model + ################################### + DATA_HANDLER_CONFIG = { + "start_time": "2008-01-01", + "end_time": "2020-08-01", + "fit_start_time": "2008-01-01", + "fit_end_time": "2014-12-31", + "instruments": MARKET, + } + + TRAINER_CONFIG = { + "train_start_time": "2008-01-01", + "train_end_time": "2014-12-31", + "validate_start_time": "2015-01-01", + "validate_end_time": "2016-12-31", + "test_start_time": "2017-01-01", + "test_end_time": "2020-08-01", + } + + task = { + "model": { + "class": "LSTM", + "module_path": "qlib.contrib.model.pytorch_lstm", + "kwargs": { + "d_feat": 6, + "hidden_size": 64, + "num_layers": 2, + "dropout": 0.0, + "n_epochs": 200, + "lr": 1e-3, + "early_stop": 20, + "batch_size": 800, + "metric": "IC", + "loss": "mse", + "seed": 0, + "GPU": 0, + }, + }, + "dataset": { + "class": "DatasetH", + "module_path": "qlib.data.dataset", + "kwargs": { + "handler": { + "class": "ALPHA360_Denoise", + "module_path": "qlib.contrib.data.handler", + "kwargs": DATA_HANDLER_CONFIG, + }, + "segments": { + "train": ("2008-01-01", "2014-12-31"), + "valid": ("2015-01-01", "2016-12-31"), + "test": ("2017-01-01", "2020-08-01"), + }, + }, + } + # You shoud record the data in specific sequence + # "record": ['SignalRecord', 'SigAnaRecord', 'PortAnaRecord'], + } + + # model = train_model(task) + model = init_instance_by_config(task["model"]) + dataset = init_instance_by_config(task["dataset"]) + model.fit(dataset) + + pred_score = model.predict(dataset) + + # save pred_score to file + pred_score_path = Path("~/tmp/qlib/pred_score.pkl").expanduser() + pred_score_path.parent.mkdir(exist_ok=True, parents=True) + pred_score.to_pickle(pred_score_path) + + ################################### + # backtest + ################################### + STRATEGY_CONFIG = { + "topk": 50, + "n_drop": 5, + } + BACKTEST_CONFIG = { + "verbose": False, + "limit_threshold": 0.095, + "account": 100000000, + "benchmark": BENCHMARK, + "deal_price": "close", + "open_cost": 0.0005, + "close_cost": 0.0015, + "min_cost": 5, + } + + # use default strategy + # custom Strategy, refer to: TODO: Strategy API url + strategy = TopkDropoutStrategy(**STRATEGY_CONFIG) + report_normal, positions_normal = normal_backtest(pred_score, strategy=strategy, **BACKTEST_CONFIG) + + ################################### + # analyze + # If need a more detailed analysis, refer to: examples/train_and_bakctest.ipynb + ################################### + 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 + print(analysis_df) diff --git a/qlib/contrib/model/pytorch_gats.py b/qlib/contrib/model/pytorch_gats.py new file mode 100755 index 000000000..edfb26d72 --- /dev/null +++ b/qlib/contrib/model/pytorch_gats.py @@ -0,0 +1,383 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + + +from __future__ import division +from __future__ import print_function + +import os +import numpy as np +import pandas as pd +import copy +from sklearn.metrics import roc_auc_score, mean_squared_error +import logging +from ...utils import unpack_archive_with_buffer, save_multiple_parts_file, create_save_path, drop_nan_by_y_index +from ...log import get_module_logger, TimeInspector + +import torch +import torch.nn as nn +import torch.optim as optim + +from ...model.base import Model +from ...data.dataset import DatasetH +from ...data.dataset.handler import DataHandlerLP + + +class GAT(Model): + """GAT Model + + Parameters + ---------- + input_dim : int + input dimension + output_dim : int + output dimension + layers : tuple + layer sizes + lr : float + learning rate + optimizer : str + optimizer name + GPU : str + the GPU ID(s) used for training + """ + + def __init__( + self, + d_feat=6, + hidden_size=64, + num_layers=2, + dropout=0.0, + n_epochs=200, + lr=0.001, + metric="IC", + batch_size=2000, + early_stop=20, + loss="mse", + base_model="GRU", + optimizer="adam", + GPU="0", + seed=0, + **kwargs + ): + # Set logger. + self.logger = get_module_logger("GAT") + self.logger.info("GAT pytorch version...") + + # set hyper-parameters. + self.d_feat = d_feat + self.hidden_size = hidden_size + self.num_layers = num_layers + self.dropout = dropout + self.n_epochs = n_epochs + self.lr = lr + self.metric = metric + self.batch_size = batch_size + self.early_stop = early_stop + self.optimizer = optimizer.lower() + self.loss = loss + self.base_model = base_model + self.visible_GPU = GPU + self.use_gpu = torch.cuda.is_available() + self.seed = seed + + self.logger.info( + "GAT parameters setting:" + "\nd_feat : {}" + "\nhidden_size : {}" + "\nnum_layers : {}" + "\ndropout : {}" + "\nn_epochs : {}" + "\nlr : {}" + "\nmetric : {}" + "\nbatch_size : {}" + "\nearly_stop : {}" + "\noptimizer : {}" + "\nloss_type : {}" + "\nbase_model : {}" + "\nvisible_GPU : {}" + "\nuse_GPU : {}" + "\nseed : {}".format( + d_feat, + hidden_size, + num_layers, + dropout, + n_epochs, + lr, + metric, + batch_size, + early_stop, + optimizer.lower(), + loss, + base_model, + GPU, + self.use_gpu, + seed, + ) + ) + + if loss not in {"mse", "binary"}: + raise NotImplementedError("loss {} is not supported!".format(loss)) + self._scorer = mean_squared_error if loss == "mse" else roc_auc_score + + self.GAT_model = GATModel( + d_feat=self.d_feat, hidden_size=self.hidden_size, num_layers=self.num_layers, dropout=self.dropout, base_model=self.base_model + ) + if optimizer.lower() == "adam": + self.train_optimizer = optim.Adam(self.GAT_model.parameters(), lr=self.lr) + elif optimizer.lower() == "gd": + self.train_optimizer = optim.SGD(self.GAT_model.parameters(), lr=self.lr) + else: + raise NotImplementedError("optimizer {} is not supported!".format(optimizer)) + + self._fitted = False + if self.use_gpu: + self.GAT_model.cuda() + # set the visible GPU + if self.visible_GPU: + os.environ["CUDA_VISIBLE_DEVICES"] = self.visible_GPU + + def mse(self, pred, label): + loss = (pred - label) ** 2 + return torch.mean(loss) + + def loss_fn(self, pred, label): + mask = ~torch.isnan(label) + + if self.loss == "mse": + return self.mse(pred[mask], label[mask]) + + raise ValueError("unknown loss `%s`" % self.loss) + + def metric_fn(self, pred, label): + + mask = torch.isfinite(label) + if self.metric == "IC": + return self.cal_ic(pred[mask], label[mask]) + + if self.metric == "" or self.metric == "loss": # use loss + return -self.loss_fn(pred[mask], label[mask]) + + raise ValueError("unknown metric `%s`" % self.metric) + + def cal_ic(self, pred, label): + return torch.mean(pred * label) + + def train_epoch(self, x_train, y_train): + + x_train_values = x_train.values + y_train_values = np.squeeze(y_train.values) * 100 + + self.GAT_model.train() + + indices = np.arange(len(x_train_values)) + np.random.shuffle(indices) + + for i in range(len(indices))[:: self.batch_size]: + + if len(indices) - i < self.batch_size: + break + + feature = torch.from_numpy(x_train_values[indices[i : i + self.batch_size]]).float() + label = torch.from_numpy(y_train_values[indices[i : i + self.batch_size]]).float() + + if self.use_gpu: + feature = feature.cuda() + label = label.cuda() + + pred = self.GAT_model(feature) + loss = self.loss_fn(pred, label) + + self.train_optimizer.zero_grad() + loss.backward() + torch.nn.utils.clip_grad_value_(self.GAT_model.parameters(), 3.0) + self.train_optimizer.step() + + def test_epoch(self, data_x, data_y): + + # prepare training data + x_values = data_x.values + y_values = np.squeeze(data_y.values) + + self.GAT_model.eval() + + scores = [] + losses = [] + + indices = np.arange(len(x_values)) + np.random.shuffle(indices) + + for i in range(len(indices))[:: self.batch_size]: + + if len(indices) - i < self.batch_size: + break + + feature = torch.from_numpy(x_values[indices[i : i + self.batch_size]]).float() + label = torch.from_numpy(y_values[indices[i : i + self.batch_size]]).float() + + if self.use_gpu: + feature = feature.cuda() + label = label.cuda() + + pred = self.GAT_model(feature) + loss = self.loss_fn(pred, label) + losses.append(loss.item()) + + score = self.metric_fn(pred, label) + scores.append(score.item()) + + return np.mean(losses), np.mean(scores) + + def fit( + self, + dataset: DatasetH, + evals_result=dict(), + verbose=True, + save_path=None, + ): + + df_train, df_valid, df_test = dataset.prepare( + ["train", "valid", "test"], col_set=["feature", "label"], data_key=DataHandlerLP.DK_L + ) + + x_train, y_train = df_train["feature"], df_train["label"] + x_valid, y_valid = df_valid["feature"], df_valid["label"] + + if save_path == None: + save_path = create_save_path(save_path) + stop_steps = 0 + train_loss = 0 + best_score = -np.inf + best_epoch = 0 + evals_result["train"] = [] + evals_result["valid"] = [] + + # train + self.logger.info("training...") + self._fitted = True + # return + + for step in range(self.n_epochs): + self.logger.info("Epoch%d:", step) + self.logger.info("training...") + self.train_epoch(x_train, y_train) + self.logger.info("evaluating...") + train_loss, train_score = self.test_epoch(x_train, y_train) + val_loss, val_score = self.test_epoch(x_valid, y_valid) + self.logger.info("train %.6f, valid %.6f" % (train_score, val_score)) + evals_result["train"].append(train_score) + evals_result["valid"].append(val_score) + + if val_score > best_score: + best_score = val_score + stop_steps = 0 + best_epoch = step + best_param = copy.deepcopy(self.GAT_model.state_dict()) + else: + stop_steps += 1 + if stop_steps >= self.early_stop: + self.logger.info("early stop") + break + + self.logger.info("best score: %.6lf @ %d" % (best_score, best_epoch)) + self.GAT_model.load_state_dict(best_param) + torch.save(best_param, save_path) + + if self.use_gpu: + torch.cuda.empty_cache() + + def predict(self, dataset): + if not self._fitted: + raise ValueError("model is not fitted yet!") + + x_test = dataset.prepare("test", col_set="feature") + index = x_test.index + self.GAT_model.eval() + x_values = x_test.values + sample_num = x_values.shape[0] + preds = [] + + for begin in range(sample_num)[:: self.batch_size]: + + if sample_num - begin < self.batch_size: + end = sample_num + else: + end = begin + self.batch_size + + x_batch = torch.from_numpy(x_values[begin:end]).float() + + if self.use_gpu: + x_batch = x_batch.cuda() + + with torch.no_grad(): + if self.use_gpu: + pred = self.GAT_model(x_batch).detach().cpu().numpy() + else: + pred = self.GAT_model(x_batch).detach().numpy() + + preds.append(pred) + + return pd.Series(np.concatenate(preds), index=index) + + +class GATModel(nn.Module): + + def __init__(self, d_feat=6, hidden_size=64, num_layers=2, dropout=0.0, base_model='GRU'): + super().__init__() + + if base_model == 'GRU': + self.rnn = nn.GRU( + input_size=d_feat, + hidden_size=hidden_size, + num_layers=num_layers, + batch_first=True, + dropout=dropout, + ) + elif base_model == 'LSTM': + self.rnn = nn.LSTM( + input_size=d_feat, + hidden_size=hidden_size, + num_layers=num_layers, + batch_first=True, + dropout=dropout, + ) + else: + raise ValueError('unknown base model name `%s`'%base_model) + + self.hidden_size = hidden_size + self.bn1 = nn.BatchNorm1d(num_features=hidden_size, track_running_stats=False) + self.fc = nn.Linear(hidden_size, hidden_size) + self.bn2 = nn.BatchNorm1d(num_features=hidden_size, track_running_stats=False) + self.fc_out = nn.Linear(hidden_size, 1) + self.leaky_relu = nn.LeakyReLU() + self.softmax = nn.Softmax(dim=1) + + self.d_feat = d_feat + + def cal_convariance(self, x, y): # the 2nd dimension of x and y are the same + e_x = torch.mean(x, dim = 1).reshape(-1, 1) + e_y = torch.mean(y, dim = 1).reshape(-1, 1) + e_x_e_y = e_x.mm(torch.t(e_y)) + x_extend = x.reshape(x.shape[0], 1, x.shape[1]).repeat(1, y.shape[0], 1) + y_extend = y.reshape(1, y.shape[0], y.shape[1]).repeat(x.shape[0], 1, 1) + e_xy = torch.mean(x_extend*y_extend, dim = 2) + return e_xy - e_x_e_y + + def forward(self, x): + # x: [N, F*T] + x = x.reshape(len(x), self.d_feat, -1) # [N, F, T] + x = x.permute(0, 2, 1) # [N, T, F] + out, _ = self.rnn(x) + hidden = out[:, -1, :] + hidden = self.bn1(hidden) + + gamma = self.cal_convariance(hidden, hidden) + # gamma = hidden.mm(torch.t(hidden)) + # gamma = self.leaky_relu(gamma) + # gamma = self.softmax(gamma) + # gamma = gamma * (torch.ones(x.shape[0], x.shape[0]).to(device) - torch.diag(torch.ones(x.shape[0])).to(device)) + output = gamma.mm(hidden) + output = self.fc(output) + output = self.bn2(output) + output = self.leaky_relu(output) + return self.fc_out(output).squeeze() \ No newline at end of file diff --git a/qlib/contrib/model/pytorch_lstm.py b/qlib/contrib/model/pytorch_lstm.py new file mode 100755 index 000000000..4eb41c250 --- /dev/null +++ b/qlib/contrib/model/pytorch_lstm.py @@ -0,0 +1,340 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + + +from __future__ import division +from __future__ import print_function + +import os +import numpy as np +import pandas as pd +import copy +from sklearn.metrics import roc_auc_score, mean_squared_error +import logging +from ...utils import unpack_archive_with_buffer, save_multiple_parts_file, create_save_path, drop_nan_by_y_index +from ...log import get_module_logger, TimeInspector + +import torch +import torch.nn as nn +import torch.optim as optim + +from ...model.base import Model +from ...data.dataset import DatasetH +from ...data.dataset.handler import DataHandlerLP + + +class LSTM(Model): + """LSTM Model + + Parameters + ---------- + input_dim : int + input dimension + output_dim : int + output dimension + layers : tuple + layer sizes + lr : float + learning rate + optimizer : str + optimizer name + GPU : str + the GPU ID(s) used for training + """ + + def __init__( + self, + d_feat=6, + hidden_size=64, + num_layers=2, + dropout=0.0, + n_epochs=200, + lr=0.001, + metric="IC", + batch_size=2000, + early_stop=20, + loss="mse", + optimizer="adam", + GPU="0", + seed=0, + **kwargs + ): + # Set logger. + self.logger = get_module_logger("LSTM") + self.logger.info("LSTM pytorch version...") + + # set hyper-parameters. + self.d_feat = d_feat + self.hidden_size = hidden_size + self.num_layers = num_layers + self.dropout = dropout + self.n_epochs = n_epochs + self.lr = lr + self.metric = metric + self.batch_size = batch_size + self.early_stop = early_stop + self.optimizer = optimizer.lower() + self.loss = loss + self.visible_GPU = GPU + self.use_gpu = torch.cuda.is_available() + self.seed = seed + + self.logger.info( + "LSTM parameters setting:" + "\nd_feat : {}" + "\nhidden_size : {}" + "\nnum_layers : {}" + "\ndropout : {}" + "\nn_epochs : {}" + "\nlr : {}" + "\nmetric : {}" + "\nbatch_size : {}" + "\nearly_stop : {}" + "\noptimizer : {}" + "\nloss_type : {}" + "\nvisible_GPU : {}" + "\nuse_GPU : {}" + "\nseed : {}".format( + d_feat, + hidden_size, + num_layers, + dropout, + n_epochs, + lr, + metric, + batch_size, + early_stop, + optimizer.lower(), + loss, + GPU, + self.use_gpu, + seed, + ) + ) + + if loss not in {"mse", "binary"}: + raise NotImplementedError("loss {} is not supported!".format(loss)) + self._scorer = mean_squared_error if loss == "mse" else roc_auc_score + + self.lstm_model = LSTMModel( + d_feat=self.d_feat, hidden_size=self.hidden_size, num_layers=self.num_layers, dropout=self.dropout + ) + if optimizer.lower() == "adam": + self.train_optimizer = optim.Adam(self.lstm_model.parameters(), lr=self.lr) + elif optimizer.lower() == "gd": + self.train_optimizer = optim.SGD(self.lstm_model.parameters(), lr=self.lr) + else: + raise NotImplementedError("optimizer {} is not supported!".format(optimizer)) + + self._fitted = False + if self.use_gpu: + self.lstm_model.cuda() + # set the visible GPU + if self.visible_GPU: + os.environ["CUDA_VISIBLE_DEVICES"] = self.visible_GPU + + def mse(self, pred, label): + loss = (pred - label) ** 2 + return torch.mean(loss) + + def loss_fn(self, pred, label): + mask = ~torch.isnan(label) + + if self.loss == "mse": + return self.mse(pred[mask], label[mask]) + + raise ValueError("unknown loss `%s`" % self.loss) + + def metric_fn(self, pred, label): + + mask = torch.isfinite(label) + if self.metric == "IC": + return self.cal_ic(pred[mask], label[mask]) + + if self.metric == "" or self.metric == "loss": # use loss + return -self.loss_fn(pred[mask], label[mask]) + + raise ValueError("unknown metric `%s`" % self.metric) + + def cal_ic(self, pred, label): + return torch.mean(pred * label) + + def train_epoch(self, x_train, y_train): + + x_train_values = x_train.values + y_train_values = np.squeeze(y_train.values) * 100 + + self.lstm_model.train() + + indices = np.arange(len(x_train_values)) + np.random.shuffle(indices) + + for i in range(len(indices))[:: self.batch_size]: + + if len(indices) - i < self.batch_size: + break + + feature = torch.from_numpy(x_train_values[indices[i : i + self.batch_size]]).float() + label = torch.from_numpy(y_train_values[indices[i : i + self.batch_size]]).float() + + if self.use_gpu: + feature = feature.cuda() + label = label.cuda() + + pred = self.lstm_model(feature) + loss = self.loss_fn(pred, label) + + self.train_optimizer.zero_grad() + loss.backward() + torch.nn.utils.clip_grad_value_(self.lstm_model.parameters(), 3.0) + self.train_optimizer.step() + + def test_epoch(self, data_x, data_y): + + # prepare training data + x_values = data_x.values + y_values = np.squeeze(data_y.values) + + self.lstm_model.eval() + + scores = [] + losses = [] + + indices = np.arange(len(x_values)) + np.random.shuffle(indices) + + for i in range(len(indices))[:: self.batch_size]: + + if len(indices) - i < self.batch_size: + break + + feature = torch.from_numpy(x_values[indices[i : i + self.batch_size]]).float() + label = torch.from_numpy(y_values[indices[i : i + self.batch_size]]).float() + + if self.use_gpu: + feature = feature.cuda() + label = label.cuda() + + pred = self.lstm_model(feature) + loss = self.loss_fn(pred, label) + losses.append(loss.item()) + + score = self.metric_fn(pred, label) + scores.append(score.item()) + + return np.mean(losses), np.mean(scores) + + def fit( + self, + dataset: DatasetH, + evals_result=dict(), + verbose=True, + save_path=None, + ): + + df_train, df_valid, df_test = dataset.prepare( + ["train", "valid", "test"], col_set=["feature", "label"], data_key=DataHandlerLP.DK_L + ) + + x_train, y_train = df_train["feature"], df_train["label"] + x_valid, y_valid = df_valid["feature"], df_valid["label"] + + if save_path == None: + save_path = create_save_path(save_path) + stop_steps = 0 + train_loss = 0 + best_score = -np.inf + best_epoch = 0 + evals_result["train"] = [] + evals_result["valid"] = [] + + # train + self.logger.info("training...") + self._fitted = True + # return + + for step in range(self.n_epochs): + self.logger.info("Epoch%d:", step) + self.logger.info("training...") + self.train_epoch(x_train, y_train) + self.logger.info("evaluating...") + train_loss, train_score = self.test_epoch(x_train, y_train) + val_loss, val_score = self.test_epoch(x_valid, y_valid) + self.logger.info("train %.6f, valid %.6f" % (train_score, val_score)) + evals_result["train"].append(train_score) + evals_result["valid"].append(val_score) + + if val_score > best_score: + best_score = val_score + stop_steps = 0 + best_epoch = step + best_param = copy.deepcopy(self.lstm_model.state_dict()) + else: + stop_steps += 1 + if stop_steps >= self.early_stop: + self.logger.info("early stop") + break + + self.logger.info("best score: %.6lf @ %d" % (best_score, best_epoch)) + self.lstm_model.load_state_dict(best_param) + torch.save(best_param, save_path) + + if self.use_gpu: + torch.cuda.empty_cache() + + def predict(self, dataset): + if not self._fitted: + raise ValueError("model is not fitted yet!") + + x_test = dataset.prepare("test", col_set="feature") + index = x_test.index + self.lstm_model.eval() + x_values = x_test.values + sample_num = x_values.shape[0] + preds = [] + + for begin in range(sample_num)[:: self.batch_size]: + + if sample_num - begin < self.batch_size: + end = sample_num + else: + end = begin + self.batch_size + + x_batch = torch.from_numpy(x_values[begin:end]).float() + + if self.use_gpu: + x_batch = x_batch.cuda() + + with torch.no_grad(): + if self.use_gpu: + pred = self.lstm_model(x_batch).detach().cpu().numpy() + else: + pred = self.lstm_model(x_batch).detach().numpy() + + preds.append(pred) + + return pd.Series(np.concatenate(preds), index=index) + + +class LSTMModel(nn.Module): + + def __init__(self, d_feat=6, hidden_size=64, num_layers=2, dropout=0.0): + super().__init__() + + self.rnn = nn.LSTM( + input_size=d_feat, + hidden_size=hidden_size, + num_layers=num_layers, + batch_first=True, + dropout=dropout, + ) + self.fc_out = nn.Linear(hidden_size, 1) + + self.d_feat = d_feat + + def forward(self, x): + # x: [N, F*T] + x = x.reshape(len(x), self.d_feat, -1) # [N, F, T] + x = x.permute(0, 2, 1) # [N, T, F] + out, _ = self.rnn(x) + return self.fc_out(out[:, -1, :]).squeeze() \ No newline at end of file From dfc93510963fd5b7bc6e7ef8860fc081c6c586e7 Mon Sep 17 00:00:00 2001 From: Dong Zhou Date: Thu, 19 Nov 2020 09:09:56 +0800 Subject: [PATCH 055/241] fix mlflow uri --- qlib/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qlib/config.py b/qlib/config.py index 002134f9d..90369c79f 100644 --- a/qlib/config.py +++ b/qlib/config.py @@ -130,7 +130,7 @@ _default_config = { "class": "MLflowExpManager", "module_path": "qlib.workflow.expm", "kwargs": { - "uri": str(Path(os.getcwd()).resolve() / "mlruns"), + "uri": 'file:' + str(Path(os.getcwd()).resolve() / "mlruns"), "default_exp_name": "Experiment", }, }, From afcfa0a4780632b197f169fdaad4d8780023efde Mon Sep 17 00:00:00 2001 From: Young Date: Thu, 19 Nov 2020 04:08:05 +0000 Subject: [PATCH 056/241] adjust for SepDataframe --- qlib/data/dataset/handler.py | 47 ++++++++++++++++++++++++++---------- qlib/data/dataset/utils.py | 13 +++++++--- qlib/workflow/utils.py | 8 +++--- 3 files changed, 48 insertions(+), 20 deletions(-) diff --git a/qlib/data/dataset/handler.py b/qlib/data/dataset/handler.py index 9812864af..d32b251de 100644 --- a/qlib/data/dataset/handler.py +++ b/qlib/data/dataset/handler.py @@ -56,7 +56,24 @@ class DataHandler(Serializable): end_time=None, data_loader: Tuple[dict, str, DataLoader] = None, init_data=True, + fetch_orig=True, ): + """ + Parameters + ---------- + instruments : + The stock list to retrive + start_time : + start_time of the original data + end_time : + end_time of the original data + data_loader : Tuple[dict, str, DataLoader] + data loader to load the data + init_data : + intialize the original data in the constructor + fetch_orig : bool + Return the original data instead of copy if possible + """ # Set logger self.logger = get_module_logger("DataHandler") @@ -72,6 +89,7 @@ class DataHandler(Serializable): self.instruments = instruments self.start_time = start_time self.end_time = end_time + self.fetch_orig = fetch_orig if init_data: with TimeInspector.logt("Init data"): self.init() @@ -138,7 +156,7 @@ class DataHandler(Serializable): ------- pd.DataFrame: """ - df = fetch_df_by_index(self._data, selector, level) + df = fetch_df_by_index(self._data, selector, level, fetch_orig=self.fetch_orig) df = self._fetch_df_by_col(df, col_set) if squeeze: # squeeze columns @@ -269,8 +287,10 @@ class DataHandlerLP(DataHandler): for pname in "infer_processors", "learn_processors": for proc in locals()[pname]: getattr(self, pname).append( - init_instance_by_config(proc, processor_module, accept_types=(processor_module.Processor,)) - ) + init_instance_by_config( + proc, + None if (isinstance(data_loader, dict) and "module_path" in data_loader) else data_loader_module, + accept_types=processor_module.Processor)) self.process_type = process_type super().__init__(instruments, start_time, end_time, data_loader, **kwargs) @@ -354,15 +374,16 @@ class DataHandlerLP(DataHandler): # init raw data super().init(enable_cache=enable_cache) - if init_type == DataHandlerLP.IT_FIT_IND: - self.fit() - self.process_data() - elif init_type == DataHandlerLP.IT_LS: - self.process_data() - elif init_type == DataHandlerLP.IT_FIT_SEQ: - self.fit_process_data() - else: - raise NotImplementedError(f"This type of input is not supported") + with TimeInspector.logt("fit & process data"): + if init_type == DataHandlerLP.IT_FIT_IND: + self.fit() + self.process_data() + elif init_type == DataHandlerLP.IT_LS: + self.process_data() + elif init_type == DataHandlerLP.IT_FIT_SEQ: + self.fit_process_data() + else: + raise NotImplementedError(f"This type of input is not supported") # TODO: Be able to cache handler data. Save the memory for data processing @@ -396,7 +417,7 @@ class DataHandlerLP(DataHandler): pd.DataFrame: """ df = self._get_df_by_key(data_key) - df = fetch_df_by_index(df, selector, level) + df = fetch_df_by_index(df, selector, level, fetch_orig=self.fetch_orig) return self._fetch_df_by_col(df, col_set) def get_cols(self, col_set=DataHandler.CS_ALL, data_key: str = DK_I) -> list: diff --git a/qlib/data/dataset/utils.py b/qlib/data/dataset/utils.py index 8ee199bc0..85a5e8389 100644 --- a/qlib/data/dataset/utils.py +++ b/qlib/data/dataset/utils.py @@ -32,7 +32,7 @@ def get_level_index(df: pd.DataFrame, level=Union[str, int]) -> int: def fetch_df_by_index( - df: pd.DataFrame, selector: Union[pd.Timestamp, slice, str, list], level: Union[str, int] + df: pd.DataFrame, selector: Union[pd.Timestamp, slice, str, list], level: Union[str, int], fetch_orig=True, ) -> pd.DataFrame: """ fetch data from `data` with `selector` and `level` @@ -52,6 +52,11 @@ def fetch_df_by_index( idx_slc = (selector, slice(None, None)) if get_level_index(df, level) == 1: idx_slc = idx_slc[1], idx_slc[0] - return df.loc[ - pd.IndexSlice[idx_slc], - ] # This could be faster than df.loc(axis=0)[idx_slc] + if fetch_orig: + for slc in idx_slc: + if slc != slice(None, None): + return df.loc[pd.IndexSlice[idx_slc],] + else: + return df + else: + return df.loc[pd.IndexSlice[idx_slc],] diff --git a/qlib/workflow/utils.py b/qlib/workflow/utils.py index d4594d28e..f5a73a157 100644 --- a/qlib/workflow/utils.py +++ b/qlib/workflow/utils.py @@ -5,9 +5,9 @@ import sys, traceback, signal, atexit from . import R from .recorder import Recorder from ..log import get_module_logger - logger = get_module_logger("workflow", "INFO") + # function to handle the experiment when unusual program ending occurs def experiment_exit_handler(): """ @@ -31,10 +31,12 @@ def experiment_exception_hook(type, value, tb): value: Exception's value tb: Exception's traceback """ - error_msg = "An exception has been raised.\n" f"Type: {type}\n" + error_msg = f"An exception has been raised[{type.__name__}: {value}]." logger.error(error_msg) + + # Same as original format traceback.print_tb(tb) - logger.error(f"Value: {value}") + print(f"{type.__name__}: {value}") R.end_exp(recorder_status=Recorder.STATUS_FA) From a8ad2120c90c469b1279782033a1c1c81b68ad30 Mon Sep 17 00:00:00 2001 From: Jactus Date: Thu, 19 Nov 2020 16:50:16 +0800 Subject: [PATCH 057/241] Update recordTemp and report --- .../estimator/analyze_from_estimator.ipynb | 222 ------------ examples/estimator/estimator_config.yaml | 53 --- examples/estimator/estimator_config_dnn.yaml | 55 --- examples/train_and_backtest.py | 121 ------- examples/train_backtest_analyze.ipynb | 338 ------------------ examples/workflow_by_code_gats.py | 145 -------- examples/workflow_by_code_gru.py | 144 -------- examples/workflow_by_code_lstm.py | 144 -------- examples/workflow_by_code_xgboost.py | 142 -------- qlib/contrib/model/pytorch_gats.py | 29 +- qlib/contrib/model/pytorch_lstm.py | 7 +- .../report/analysis_position/report.py | 5 +- qlib/contrib/report/graph.py | 4 +- qlib/workflow/cli.py | 7 +- qlib/workflow/record_temp.py | 40 ++- 15 files changed, 62 insertions(+), 1394 deletions(-) delete mode 100644 examples/estimator/analyze_from_estimator.ipynb delete mode 100644 examples/estimator/estimator_config.yaml delete mode 100644 examples/estimator/estimator_config_dnn.yaml delete mode 100644 examples/train_and_backtest.py delete mode 100644 examples/train_backtest_analyze.ipynb delete mode 100755 examples/workflow_by_code_gats.py delete mode 100755 examples/workflow_by_code_gru.py delete mode 100755 examples/workflow_by_code_lstm.py delete mode 100755 examples/workflow_by_code_xgboost.py diff --git a/examples/estimator/analyze_from_estimator.ipynb b/examples/estimator/analyze_from_estimator.ipynb deleted file mode 100644 index 6554eba29..000000000 --- a/examples/estimator/analyze_from_estimator.ipynb +++ /dev/null @@ -1,222 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import sys\n", - "import json\n", - "import yaml\n", - "import pickle\n", - "from pathlib import Path\n", - "\n", - "import qlib\n", - "import pandas as pd\n", - "from qlib.config import REG_CN\n", - "from qlib.utils import exists_qlib_data" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "CUR_DIR = Path.cwd()\n", - "MARKET = \"csi300\"" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# use default data\n", - "# NOTE: need to download data from remote: python scripts/get_data.py qlib_data --target_dir ~/.qlib/qlib_data/cn_data\n", - "provider_uri = \"~/.qlib/qlib_data/cn_data\" # target_dir\n", - "if not exists_qlib_data(provider_uri):\n", - " print(f\"Qlib data is not found in {provider_uri}\")\n", - " sys.path.append(str(CUR_DIR.parent.parent.joinpath(\"scripts\")))\n", - " from get_data import GetData\n", - " GetData().qlib_data_cn(target_dir=provider_uri)\n", - "qlib.init(provider_uri=provider_uri, region=REG_CN)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "with CUR_DIR.joinpath('estimator_config.yaml').open() as fp:\n", - " estimator_name = yaml.load(fp, Loader=yaml.FullLoader)['experiment']['name']\n", - "with CUR_DIR.joinpath(estimator_name, 'exp_info.json').open() as fp:\n", - " latest_id = json.load(fp)['id']\n", - " \n", - "estimator_dir = CUR_DIR.joinpath(estimator_name, 'sacred', latest_id)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# read estimator result" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "pred_df = pd.read_pickle(estimator_dir.joinpath('pred.pkl'))\n", - "report_normal_df = pd.read_pickle(estimator_dir.joinpath('report_normal.pkl'))\n", - "report_normal_df.index.names = ['index']\n", - "\n", - "analysis_df = pd.read_pickle(estimator_dir.joinpath('analysis.pkl'))\n", - "positions = pickle.load(estimator_dir.joinpath('positions.pkl').open('rb'))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# analyze graphs" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from qlib.data import D\n", - "from qlib.contrib.report import analysis_model, analysis_position\n", - "pred_df_dates = pred_df.index.get_level_values(level='datetime')" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## analysis position" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "stock_ret = D.features(D.instruments(MARKET), ['Ref($close, -1)/$close - 1'], pred_df_dates.min(), pred_df_dates.max())\n", - "stock_ret.columns = ['label']" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### report" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "analysis_position.report_graph(report_normal_df)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### risk analysis" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "analysis_position.risk_analysis_graph(analysis_df, report_normal_df)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## analysis model" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "label_df = D.features(D.instruments(MARKET), ['Ref($close, -2)/Ref($close, -1) - 1'], pred_df_dates.min(), pred_df_dates.max())\n", - "label_df.columns = ['label']" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### score IC" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "pred_label = pd.concat([label_df, pred_df], axis=1, sort=True).reindex(label_df.index)\n", - "analysis_position.score_ic_graph(pred_label)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### model performance" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "analysis_model.model_performance_graph(pred_label)" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.8.5" - } - }, - "nbformat": 4, - "nbformat_minor": 4 -} diff --git a/examples/estimator/estimator_config.yaml b/examples/estimator/estimator_config.yaml deleted file mode 100644 index eaffc181b..000000000 --- a/examples/estimator/estimator_config.yaml +++ /dev/null @@ -1,53 +0,0 @@ -experiment: - name: estimator_example - observer_type: file_storage - mode: train - -model: - class: LGBModel - module_path: qlib.gbdt.model.gbdt - args: - 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 -data: - class: Alpha158 - args: - dropna_label: True - filter: - market: csi300 -trainer: - class: StaticTrainer - args: - train_start_date: 2008-01-01 - train_end_date: 2014-12-31 - validate_start_date: 2015-01-01 - validate_end_date: 2016-12-31 - test_start_date: 2017-01-01 - test_end_date: 2020-08-01 -strategy: - class: TopkDropoutStrategy - args: - topk: 50 - n_drop: 5 -backtest: - normal_backtest_args: - verbose: False - limit_threshold: 0.095 - account: 100000000 - benchmark: SH000300 - deal_price: close - open_cost: 0.0005 - close_cost: 0.0015 - min_cost: 5 - -qlib_data: - # when testing, please modify the following parameters according to the specific environment - provider_uri: "~/.qlib/qlib_data/cn_data" - region: "cn" diff --git a/examples/estimator/estimator_config_dnn.yaml b/examples/estimator/estimator_config_dnn.yaml deleted file mode 100644 index 1aa122313..000000000 --- a/examples/estimator/estimator_config_dnn.yaml +++ /dev/null @@ -1,55 +0,0 @@ -experiment: - name: estimator_example - observer_type: file_storage - mode: train - -model: - module_path: qlib.model.pytorch_nn - class: DNNModelPytorch - args: - loss: mse - input_dim: 158 - output_dim: 1 - lr: 0.002 - lr_decay: 0.96 - lr_decay_steps: 100 - optimizer: 'adam' - max_steps: 8000 - batch_size: 4096 - GPU: '0' -data: - class: Alpha158 - args: - dropna_label: True - dropna_feature: True - filter: - market: csi300 -trainer: - class: StaticTrainer - args: - train_start_date: 2007-01-01 - train_end_date: 2014-12-31 - validate_start_date: 2015-01-01 - validate_end_date: 2016-12-31 - test_start_date: 2017-01-01 - test_end_date: 2020-08-01 -strategy: - class: TopkDropoutStrategy - args: - topk: 50 - n_drop: 5 -backtest: - normal_backtest_args: - verbose: False - limit_threshold: 0.095 - account: 100000000 - benchmark: SH000300 - deal_price: close - open_cost: 0.0005 - close_cost: 0.0015 - min_cost: 5 - -qlib_data: - # when testing, please modify the following parameters according to the specific environment - provider_uri: "~/.qlib/qlib_data/cn_data" - region: "cn" diff --git a/examples/train_and_backtest.py b/examples/train_and_backtest.py deleted file mode 100644 index def50b75a..000000000 --- a/examples/train_and_backtest.py +++ /dev/null @@ -1,121 +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 - - -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_cn(target_dir=provider_uri) - - qlib.init(provider_uri=provider_uri, region=REG_CN) - - MARKET = "CSI300" - BENCHMARK = "SH000300" - - ################################### - # train model - ################################### - DATA_HANDLER_CONFIG = { - "dropna_label": True, - "start_date": "2008-01-01", - "end_date": "2020-08-01", - "market": MARKET, - } - - TRAINER_CONFIG = { - "train_start_date": "2008-01-01", - "train_end_date": "2014-12-31", - "validate_start_date": "2015-01-01", - "validate_end_date": "2016-12-31", - "test_start_date": "2017-01-01", - "test_end_date": "2020-08-01", - } - - # use default DataHandler - # custom DataHandler, refer to: TODO: DataHandler API url - x_train, y_train, x_validate, y_validate, x_test, y_test = Alpha158(**DATA_HANDLER_CONFIG).get_split_data( - **TRAINER_CONFIG - ) - - MODEL_CONFIG = { - "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, - } - # use default model - # custom Model, refer to: TODO: Model API url - model = LGBModel(**MODEL_CONFIG) - model.fit(x_train, y_train, x_validate, y_validate) - _pred = model.predict(x_test) - _pred = pd.DataFrame(_pred, index=x_test.index, columns=y_test.columns) - - # backtest requires pred_score - pred_score = pd.DataFrame(index=_pred.index) - pred_score["score"] = _pred.iloc(axis=1)[0] - - # save pred_score to file - pred_score_path = Path("~/tmp/qlib/pred_score.pkl").expanduser() - pred_score_path.parent.mkdir(exist_ok=True, parents=True) - pred_score.to_pickle(pred_score_path) - - ################################### - # backtest - ################################### - STRATEGY_CONFIG = { - "topk": 50, - "n_drop": 5, - } - BACKTEST_CONFIG = { - "verbose": False, - "limit_threshold": 0.095, - "account": 100000000, - "benchmark": BENCHMARK, - "deal_price": "close", - "open_cost": 0.0005, - "close_cost": 0.0015, - "min_cost": 5, - } - - # use default strategy - # custom Strategy, refer to: TODO: Strategy API url - strategy = TopkDropoutStrategy(**STRATEGY_CONFIG) - report_normal, positions_normal = normal_backtest(pred_score, strategy=strategy, **BACKTEST_CONFIG) - - ################################### - # analyze - # If need a more detailed analysis, refer to: examples/train_and_bakctest.ipynb - ################################### - 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 - print(analysis_df) diff --git a/examples/train_backtest_analyze.ipynb b/examples/train_backtest_analyze.ipynb deleted file mode 100644 index e70fe17b4..000000000 --- a/examples/train_backtest_analyze.ipynb +++ /dev/null @@ -1,338 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import sys\n", - "from pathlib import Path\n", - "\n", - "import qlib\n", - "import pandas as pd\n", - "from qlib.config import REG_CN\n", - "from qlib.contrib.model.gbdt import LGBModel\n", - "from qlib.contrib.estimator.handler import Alpha158\n", - "from qlib.contrib.strategy.strategy import TopkDropoutStrategy\n", - "from qlib.contrib.evaluate import (\n", - " backtest as normal_backtest,\n", - " risk_analysis,\n", - ")\n", - "from qlib.utils import exists_qlib_data" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "# use default data\n", - "# NOTE: need to download data from remote: python scripts/get_data.py qlib_data_cn --target_dir ~/.qlib/qlib_data/cn_data\n", - "provider_uri = \"~/.qlib/qlib_data/cn_data\" # target_dir\n", - "if not exists_qlib_data(provider_uri):\n", - " print(f\"Qlib data is not found in {provider_uri}\")\n", - " sys.path.append(str(Path.cwd().parent.joinpath(\"scripts\")))\n", - " from get_data import GetData\n", - " GetData().qlib_data_cn(target_dir=provider_uri)\n", - "qlib.init(provider_uri=provider_uri, region=REG_CN)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "MARKET = \"csi300\"\n", - "BENCHMARK = \"SH000300\"" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# train model" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "###################################\n", - "# train model\n", - "###################################\n", - "DATA_HANDLER_CONFIG = {\n", - " \"dropna_label\": True,\n", - " \"start_date\": \"2008-01-01\",\n", - " \"end_date\": \"2020-08-01\",\n", - " \"market\": MARKET,\n", - "}\n", - "\n", - "TRAINER_CONFIG = {\n", - " \"train_start_date\": \"2008-01-01\",\n", - " \"train_end_date\": \"2014-12-31\",\n", - " \"validate_start_date\": \"2015-01-01\",\n", - " \"validate_end_date\": \"2016-12-31\",\n", - " \"test_start_date\": \"2017-01-01\",\n", - " \"test_end_date\": \"2020-08-01\",\n", - "}\n", - "\n", - "# use default DataHandler\n", - "# custom DataHandler, refer to: TODO: DataHandler api url\n", - "x_train, y_train, x_validate, y_validate, x_test, y_test = Alpha158(**DATA_HANDLER_CONFIG).get_split_data(**TRAINER_CONFIG)\n", - "\n", - "\n", - "MODEL_CONFIG = {\n", - " \"loss\": \"mse\",\n", - " \"colsample_bytree\": 0.8879,\n", - " \"learning_rate\": 0.0421,\n", - " \"subsample\": 0.8789,\n", - " \"lambda_l1\": 205.6999,\n", - " \"lambda_l2\": 580.9768,\n", - " \"max_depth\": 8,\n", - " \"num_leaves\": 210,\n", - " \"num_threads\": 20,\n", - "}\n", - "# use default model\n", - "# custom Model, refer to: TODO: Model api url\n", - "model = LGBModel(**MODEL_CONFIG)\n", - "model.fit(x_train, y_train, x_validate, y_validate)\n", - "_pred = model.predict(x_test)\n", - "_pred = pd.DataFrame(_pred, index=x_test.index, columns=y_test.columns)\n", - "\n", - "# backtest requires pred_score\n", - "pred_score = pd.DataFrame(index=_pred.index)\n", - "pred_score[\"score\"] = _pred.iloc(axis=1)[0]\n", - "\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# backtest" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "###################################\n", - "# backtest\n", - "###################################\n", - "STRATEGY_CONFIG = {\n", - " \"topk\": 50,\n", - " \"n_drop\": 5}\n", - "BACKTEST_CONFIG = {\n", - " \"verbose\": False,\n", - " \"limit_threshold\": 0.095,\n", - " \"account\": 100000000,\n", - " \"benchmark\": BENCHMARK,\n", - " \"deal_price\": \"close\",\n", - " \"open_cost\": 0.0005,\n", - " \"close_cost\": 0.0015,\n", - " \"min_cost\": 5,\n", - " \n", - "}\n", - "\n", - "# use default strategy\n", - "# custom Strategy, refer to: TODO: Strategy api url\n", - "strategy = TopkDropoutStrategy(**STRATEGY_CONFIG)\n", - "report_normal, positions_normal = normal_backtest(pred_score, strategy=strategy, **BACKTEST_CONFIG)\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# analyze" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "###################################\n", - "# analyze\n", - "# If need a more detailed analysis, refer to: examples/train_and_bakctest.ipynb\n", - "###################################\n", - "analysis = dict()\n", - "analysis[\"excess_return_without_cost\"] = risk_analysis(report_normal[\"return\"] - report_normal[\"bench\"])\n", - "analysis[\"excess_return_with_cost\"] = risk_analysis(\n", - " report_normal[\"return\"] - report_normal[\"bench\"] - report_normal[\"cost\"]\n", - ")\n", - "analysis_df = pd.concat(analysis) # type: pd.DataFrame\n", - "print(analysis_df)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# analyze graphs" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from qlib.contrib.report import analysis_model, analysis_position\n", - "from qlib.data import D\n", - "pred_df_dates = pred_score.index.get_level_values(level='datetime')\n", - "report_normal_df = report_normal\n", - "positions = positions_normal\n", - "pred_df = pred_score" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## analysis position" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "stock_ret = D.features(D.instruments(MARKET), ['Ref($close, -1)/$close - 1'], pred_df_dates.min(), pred_df_dates.max())\n", - "stock_ret.columns = ['label']" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### report" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "analysis_position.report_graph(report_normal_df)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### risk analysis" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "analysis_position.risk_analysis_graph(analysis_df, report_normal_df)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## analysis model" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "label_df = D.features(D.instruments(MARKET), ['Ref($close, -2)/Ref($close, -1) - 1'], pred_df_dates.min(), pred_df_dates.max())\n", - "label_df.columns = ['label']" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### score IC" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "pred_label = pd.concat([label_df, pred_df], axis=1, sort=True).reindex(label_df.index)\n", - "analysis_position.score_ic_graph(pred_label)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### model performance" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "analysis_model.model_performance_graph(pred_label)" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3" - }, - "toc": { - "base_numbering": 1, - "nav_menu": {}, - "number_sections": true, - "sideBar": true, - "skip_h1_title": false, - "title_cell": "Table of Contents", - "title_sidebar": "Contents", - "toc_cell": false, - "toc_position": {}, - "toc_section_display": true, - "toc_window_display": false - } - }, - "nbformat": 4, - "nbformat_minor": 4 -} diff --git a/examples/workflow_by_code_gats.py b/examples/workflow_by_code_gats.py deleted file mode 100755 index 06845d448..000000000 --- a/examples/workflow_by_code_gats.py +++ /dev/null @@ -1,145 +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.pytorch_gats import GAT -from qlib.contrib.data.handler import ALPHA360_Denoise -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 - -# from qlib.model.learner import train_model -from qlib.utils import init_instance_by_config - -import pickle - -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_cn(target_dir=provider_uri) - - qlib.init(provider_uri=provider_uri, region=REG_CN) - - MARKET = "csi300" - BENCHMARK = "SH000300" - - ################################### - # train model - ################################### - DATA_HANDLER_CONFIG = { - "start_time": "2008-01-01", - "end_time": "2020-08-01", - "fit_start_time": "2008-01-01", - "fit_end_time": "2014-12-31", - "instruments": MARKET, - } - - TRAINER_CONFIG = { - "train_start_time": "2008-01-01", - "train_end_time": "2014-12-31", - "validate_start_time": "2015-01-01", - "validate_end_time": "2016-12-31", - "test_start_time": "2017-01-01", - "test_end_time": "2020-08-01", - } - - task = { - "model": { - "class": "GAT", - "module_path": "qlib.contrib.model.pytorch_gats", - "kwargs": { - "d_feat": 6, - "hidden_size": 64, - "num_layers": 2, - "dropout": 0.0, - "n_epochs": 200, - "lr": 1e-3, - "early_stop": 20, - "batch_size": 800, - "metric": "IC", - "loss": "mse", - "base_model":"GRU", - "seed": 0, - "GPU": 0, - }, - }, - "dataset": { - "class": "DatasetH", - "module_path": "qlib.data.dataset", - "kwargs": { - "handler": { - "class": "ALPHA360_Denoise", - "module_path": "qlib.contrib.data.handler", - "kwargs": DATA_HANDLER_CONFIG, - }, - "segments": { - "train": ("2008-01-01", "2014-12-31"), - "valid": ("2015-01-01", "2016-12-31"), - "test": ("2017-01-01", "2020-08-01"), - }, - }, - } - # You shoud record the data in specific sequence - # "record": ['SignalRecord', 'SigAnaRecord', 'PortAnaRecord'], - } - - # model = train_model(task) - model = init_instance_by_config(task["model"]) - dataset = init_instance_by_config(task["dataset"]) - model.fit(dataset) - - pred_score = model.predict(dataset) - - # save pred_score to file - pred_score_path = Path("~/tmp/qlib/pred_score.pkl").expanduser() - pred_score_path.parent.mkdir(exist_ok=True, parents=True) - pred_score.to_pickle(pred_score_path) - - ################################### - # backtest - ################################### - STRATEGY_CONFIG = { - "topk": 50, - "n_drop": 5, - } - BACKTEST_CONFIG = { - "verbose": False, - "limit_threshold": 0.095, - "account": 100000000, - "benchmark": BENCHMARK, - "deal_price": "close", - "open_cost": 0.0005, - "close_cost": 0.0015, - "min_cost": 5, - } - - # use default strategy - # custom Strategy, refer to: TODO: Strategy API url - strategy = TopkDropoutStrategy(**STRATEGY_CONFIG) - report_normal, positions_normal = normal_backtest(pred_score, strategy=strategy, **BACKTEST_CONFIG) - - ################################### - # analyze - # If need a more detailed analysis, refer to: examples/train_and_bakctest.ipynb - ################################### - 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 - print(analysis_df) diff --git a/examples/workflow_by_code_gru.py b/examples/workflow_by_code_gru.py deleted file mode 100755 index e55f0ae45..000000000 --- a/examples/workflow_by_code_gru.py +++ /dev/null @@ -1,144 +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.pytorch_gru import GRU -from qlib.contrib.data.handler import ALPHA360_Denoise -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 - -# from qlib.model.learner import train_model -from qlib.utils import init_instance_by_config - -import pickle - -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_cn(target_dir=provider_uri) - - qlib.init(provider_uri=provider_uri, region=REG_CN) - - MARKET = "csi300" - BENCHMARK = "SH000300" - - ################################### - # train model - ################################### - DATA_HANDLER_CONFIG = { - "start_time": "2008-01-01", - "end_time": "2020-08-01", - "fit_start_time": "2008-01-01", - "fit_end_time": "2014-12-31", - "instruments": MARKET, - } - - TRAINER_CONFIG = { - "train_start_time": "2008-01-01", - "train_end_time": "2014-12-31", - "validate_start_time": "2015-01-01", - "validate_end_time": "2016-12-31", - "test_start_time": "2017-01-01", - "test_end_time": "2020-08-01", - } - - task = { - "model": { - "class": "GRU", - "module_path": "qlib.contrib.model.pytorch_gru", - "kwargs": { - "d_feat": 6, - "hidden_size": 64, - "num_layers": 2, - "dropout": 0.0, - "n_epochs": 200, - "lr": 1e-3, - "early_stop": 20, - "batch_size": 800, - "metric": "IC", - "loss": "mse", - "seed": 0, - "GPU": 0, - }, - }, - "dataset": { - "class": "DatasetH", - "module_path": "qlib.data.dataset", - "kwargs": { - "handler": { - "class": "ALPHA360_Denoise", - "module_path": "qlib.contrib.data.handler", - "kwargs": DATA_HANDLER_CONFIG, - }, - "segments": { - "train": ("2008-01-01", "2014-12-31"), - "valid": ("2015-01-01", "2016-12-31"), - "test": ("2017-01-01", "2020-08-01"), - }, - }, - } - # You shoud record the data in specific sequence - # "record": ['SignalRecord', 'SigAnaRecord', 'PortAnaRecord'], - } - - # model = train_model(task) - model = init_instance_by_config(task["model"]) - dataset = init_instance_by_config(task["dataset"]) - model.fit(dataset) - - pred_score = model.predict(dataset) - - # save pred_score to file - pred_score_path = Path("~/tmp/qlib/pred_score.pkl").expanduser() - pred_score_path.parent.mkdir(exist_ok=True, parents=True) - pred_score.to_pickle(pred_score_path) - - ################################### - # backtest - ################################### - STRATEGY_CONFIG = { - "topk": 50, - "n_drop": 5, - } - BACKTEST_CONFIG = { - "verbose": False, - "limit_threshold": 0.095, - "account": 100000000, - "benchmark": BENCHMARK, - "deal_price": "close", - "open_cost": 0.0005, - "close_cost": 0.0015, - "min_cost": 5, - } - - # use default strategy - # custom Strategy, refer to: TODO: Strategy API url - strategy = TopkDropoutStrategy(**STRATEGY_CONFIG) - report_normal, positions_normal = normal_backtest(pred_score, strategy=strategy, **BACKTEST_CONFIG) - - ################################### - # analyze - # If need a more detailed analysis, refer to: examples/train_and_bakctest.ipynb - ################################### - 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 - print(analysis_df) diff --git a/examples/workflow_by_code_lstm.py b/examples/workflow_by_code_lstm.py deleted file mode 100755 index 1815d2fec..000000000 --- a/examples/workflow_by_code_lstm.py +++ /dev/null @@ -1,144 +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.pytorch_lstm import LSTM -from qlib.contrib.data.handler import ALPHA360_Denoise -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 - -# from qlib.model.learner import train_model -from qlib.utils import init_instance_by_config - -import pickle - -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_cn(target_dir=provider_uri) - - qlib.init(provider_uri=provider_uri, region=REG_CN) - - MARKET = "csi300" - BENCHMARK = "SH000300" - - ################################### - # train model - ################################### - DATA_HANDLER_CONFIG = { - "start_time": "2008-01-01", - "end_time": "2020-08-01", - "fit_start_time": "2008-01-01", - "fit_end_time": "2014-12-31", - "instruments": MARKET, - } - - TRAINER_CONFIG = { - "train_start_time": "2008-01-01", - "train_end_time": "2014-12-31", - "validate_start_time": "2015-01-01", - "validate_end_time": "2016-12-31", - "test_start_time": "2017-01-01", - "test_end_time": "2020-08-01", - } - - task = { - "model": { - "class": "LSTM", - "module_path": "qlib.contrib.model.pytorch_lstm", - "kwargs": { - "d_feat": 6, - "hidden_size": 64, - "num_layers": 2, - "dropout": 0.0, - "n_epochs": 200, - "lr": 1e-3, - "early_stop": 20, - "batch_size": 800, - "metric": "IC", - "loss": "mse", - "seed": 0, - "GPU": 0, - }, - }, - "dataset": { - "class": "DatasetH", - "module_path": "qlib.data.dataset", - "kwargs": { - "handler": { - "class": "ALPHA360_Denoise", - "module_path": "qlib.contrib.data.handler", - "kwargs": DATA_HANDLER_CONFIG, - }, - "segments": { - "train": ("2008-01-01", "2014-12-31"), - "valid": ("2015-01-01", "2016-12-31"), - "test": ("2017-01-01", "2020-08-01"), - }, - }, - } - # You shoud record the data in specific sequence - # "record": ['SignalRecord', 'SigAnaRecord', 'PortAnaRecord'], - } - - # model = train_model(task) - model = init_instance_by_config(task["model"]) - dataset = init_instance_by_config(task["dataset"]) - model.fit(dataset) - - pred_score = model.predict(dataset) - - # save pred_score to file - pred_score_path = Path("~/tmp/qlib/pred_score.pkl").expanduser() - pred_score_path.parent.mkdir(exist_ok=True, parents=True) - pred_score.to_pickle(pred_score_path) - - ################################### - # backtest - ################################### - STRATEGY_CONFIG = { - "topk": 50, - "n_drop": 5, - } - BACKTEST_CONFIG = { - "verbose": False, - "limit_threshold": 0.095, - "account": 100000000, - "benchmark": BENCHMARK, - "deal_price": "close", - "open_cost": 0.0005, - "close_cost": 0.0015, - "min_cost": 5, - } - - # use default strategy - # custom Strategy, refer to: TODO: Strategy API url - strategy = TopkDropoutStrategy(**STRATEGY_CONFIG) - report_normal, positions_normal = normal_backtest(pred_score, strategy=strategy, **BACKTEST_CONFIG) - - ################################### - # analyze - # If need a more detailed analysis, refer to: examples/train_and_bakctest.ipynb - ################################### - 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 - print(analysis_df) diff --git a/examples/workflow_by_code_xgboost.py b/examples/workflow_by_code_xgboost.py deleted file mode 100755 index 94b43f449..000000000 --- a/examples/workflow_by_code_xgboost.py +++ /dev/null @@ -1,142 +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.xgboost import XGBModel -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 - -# from qlib.model.learner import train_model -from qlib.utils import init_instance_by_config - -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_cn(target_dir=provider_uri) - - qlib.init(provider_uri=provider_uri, region=REG_CN) - - MARKET = "csi300" - BENCHMARK = "SH000300" - - ################################### - # train model - ################################### - DATA_HANDLER_CONFIG = { - "start_time": "2008-01-01", - "end_time": "2020-08-01", - "fit_start_time": "2008-01-01", - "fit_end_time": "2014-12-31", - "instruments": MARKET, - } - - TRAINER_CONFIG = { - "train_start_time": "2008-01-01", - "train_end_time": "2014-12-31", - "validate_start_time": "2015-01-01", - "validate_end_time": "2016-12-31", - "test_start_time": "2017-01-01", - "test_end_time": "2020-08-01", - } - - task = { - "model": { - "class": "XGBModel", - "module_path": "qlib.contrib.model.xgboost", - "kwargs": { - "objective": "reg:linear", - "n_estimators": 5000, - "colsample_bytree": 0.85, - "learning_rate": 0.0421, - "subsample": 0.8789, - "max_depth": 8, - "num_leaves": 210, - "num_threads": 20, - "missing": -1, - "min_child_weight": 1, - "nthread": 4, - "tree_method": "hist", - }, - }, - "dataset": { - "class": "DatasetH", - "module_path": "qlib.data.dataset", - "kwargs": { - "handler": { - "class": "Alpha158", - "module_path": "qlib.contrib.data.handler", - "kwargs": DATA_HANDLER_CONFIG, - }, - "segments": { - "train": ("2008-01-01", "2014-12-31"), - "valid": ("2015-01-01", "2016-12-31"), - "test": ("2017-01-01", "2020-08-01"), - }, - }, - } - # You shoud record the data in specific sequence - # "record": ['SignalRecord', 'SigAnaRecord', 'PortAnaRecord'], - } - - # model = train_model(task) - model = init_instance_by_config(task["model"]) - dataset = init_instance_by_config(task["dataset"]) - - model.fit(dataset) - pred_score = model.predict(dataset) - - # save pred_score to file - pred_score_path = Path("~/tmp/qlib/pred_score.pkl").expanduser() - pred_score_path.parent.mkdir(exist_ok=True, parents=True) - pred_score.to_pickle(pred_score_path) - - ################################### - # backtest - ################################### - STRATEGY_CONFIG = { - "topk": 50, - "n_drop": 5, - } - BACKTEST_CONFIG = { - "verbose": False, - "limit_threshold": 0.095, - "account": 100000000, - "benchmark": BENCHMARK, - "deal_price": "close", - "open_cost": 0.0005, - "close_cost": 0.0015, - "min_cost": 5, - } - - # use default strategy - # custom Strategy, refer to: TODO: Strategy API url - strategy = TopkDropoutStrategy(**STRATEGY_CONFIG) - report_normal, positions_normal = normal_backtest(pred_score, strategy=strategy, **BACKTEST_CONFIG) - - ################################### - # analyze - # If need a more detailed analysis, refer to: examples/train_and_bakctest.ipynb - ################################### - 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 - print(analysis_df) diff --git a/qlib/contrib/model/pytorch_gats.py b/qlib/contrib/model/pytorch_gats.py index edfb26d72..22ed6812d 100755 --- a/qlib/contrib/model/pytorch_gats.py +++ b/qlib/contrib/model/pytorch_gats.py @@ -121,7 +121,11 @@ class GAT(Model): self._scorer = mean_squared_error if loss == "mse" else roc_auc_score self.GAT_model = GATModel( - d_feat=self.d_feat, hidden_size=self.hidden_size, num_layers=self.num_layers, dropout=self.dropout, base_model=self.base_model + d_feat=self.d_feat, + hidden_size=self.hidden_size, + num_layers=self.num_layers, + dropout=self.dropout, + base_model=self.base_model, ) if optimizer.lower() == "adam": self.train_optimizer = optim.Adam(self.GAT_model.parameters(), lr=self.lr) @@ -321,11 +325,10 @@ class GAT(Model): class GATModel(nn.Module): - - def __init__(self, d_feat=6, hidden_size=64, num_layers=2, dropout=0.0, base_model='GRU'): + def __init__(self, d_feat=6, hidden_size=64, num_layers=2, dropout=0.0, base_model="GRU"): super().__init__() - if base_model == 'GRU': + if base_model == "GRU": self.rnn = nn.GRU( input_size=d_feat, hidden_size=hidden_size, @@ -333,7 +336,7 @@ class GATModel(nn.Module): batch_first=True, dropout=dropout, ) - elif base_model == 'LSTM': + elif base_model == "LSTM": self.rnn = nn.LSTM( input_size=d_feat, hidden_size=hidden_size, @@ -342,7 +345,7 @@ class GATModel(nn.Module): dropout=dropout, ) else: - raise ValueError('unknown base model name `%s`'%base_model) + raise ValueError("unknown base model name `%s`" % base_model) self.hidden_size = hidden_size self.bn1 = nn.BatchNorm1d(num_features=hidden_size, track_running_stats=False) @@ -354,19 +357,19 @@ class GATModel(nn.Module): self.d_feat = d_feat - def cal_convariance(self, x, y): # the 2nd dimension of x and y are the same - e_x = torch.mean(x, dim = 1).reshape(-1, 1) - e_y = torch.mean(y, dim = 1).reshape(-1, 1) + def cal_convariance(self, x, y): # the 2nd dimension of x and y are the same + e_x = torch.mean(x, dim=1).reshape(-1, 1) + e_y = torch.mean(y, dim=1).reshape(-1, 1) e_x_e_y = e_x.mm(torch.t(e_y)) x_extend = x.reshape(x.shape[0], 1, x.shape[1]).repeat(1, y.shape[0], 1) y_extend = y.reshape(1, y.shape[0], y.shape[1]).repeat(x.shape[0], 1, 1) - e_xy = torch.mean(x_extend*y_extend, dim = 2) + e_xy = torch.mean(x_extend * y_extend, dim=2) return e_xy - e_x_e_y def forward(self, x): # x: [N, F*T] - x = x.reshape(len(x), self.d_feat, -1) # [N, F, T] - x = x.permute(0, 2, 1) # [N, T, F] + x = x.reshape(len(x), self.d_feat, -1) # [N, F, T] + x = x.permute(0, 2, 1) # [N, T, F] out, _ = self.rnn(x) hidden = out[:, -1, :] hidden = self.bn1(hidden) @@ -380,4 +383,4 @@ class GATModel(nn.Module): output = self.fc(output) output = self.bn2(output) output = self.leaky_relu(output) - return self.fc_out(output).squeeze() \ No newline at end of file + return self.fc_out(output).squeeze() diff --git a/qlib/contrib/model/pytorch_lstm.py b/qlib/contrib/model/pytorch_lstm.py index 4eb41c250..8b8454380 100755 --- a/qlib/contrib/model/pytorch_lstm.py +++ b/qlib/contrib/model/pytorch_lstm.py @@ -317,7 +317,6 @@ class LSTM(Model): class LSTMModel(nn.Module): - def __init__(self, d_feat=6, hidden_size=64, num_layers=2, dropout=0.0): super().__init__() @@ -334,7 +333,7 @@ class LSTMModel(nn.Module): def forward(self, x): # x: [N, F*T] - x = x.reshape(len(x), self.d_feat, -1) # [N, F, T] - x = x.permute(0, 2, 1) # [N, T, F] + x = x.reshape(len(x), self.d_feat, -1) # [N, F, T] + x = x.permute(0, 2, 1) # [N, T, F] out, _ = self.rnn(x) - return self.fc_out(out[:, -1, :]).squeeze() \ No newline at end of file + return self.fc_out(out[:, -1, :]).squeeze() diff --git a/qlib/contrib/report/analysis_position/report.py b/qlib/contrib/report/analysis_position/report.py index e8bb5313f..714cfdd9c 100644 --- a/qlib/contrib/report/analysis_position/report.py +++ b/qlib/contrib/report/analysis_position/report.py @@ -75,11 +75,12 @@ def _report_figure(df: pd.DataFrame) -> [list, tuple]: max_start_date, max_end_date = _calculate_maximum(report_df) ex_max_start_date, ex_max_end_date = _calculate_maximum(report_df, True) + index_name = report_df.index.name _temp_df = report_df.reset_index() _temp_df.loc[-1] = 0 _temp_df = _temp_df.shift(1) - _temp_df.loc[0, "index"] = "T0" - _temp_df.set_index("index", inplace=True) + _temp_df.loc[0, index_name] = "T0" + _temp_df.set_index(index_name, inplace=True) _temp_df.iloc[0] = 0 report_df = _temp_df diff --git a/qlib/contrib/report/graph.py b/qlib/contrib/report/graph.py index 082eafa49..07ed94f90 100644 --- a/qlib/contrib/report/graph.py +++ b/qlib/contrib/report/graph.py @@ -11,7 +11,7 @@ import pandas as pd import plotly.offline as py import plotly.graph_objs as go -from plotly.tools import make_subplots +from plotly.subplots import make_subplots from plotly.figure_factory import create_distplot from ...utils import get_module_by_module_path @@ -357,7 +357,7 @@ class SubplotsGraph(object): # _item.pop('yaxis', None) for _g_obj in _graph_data: - self._figure.append_trace(_g_obj, row=row, col=col) + self._figure.add_trace(_g_obj, row=row, col=col) if self._sub_graph_layout is not None: for k, v in self._sub_graph_layout.items(): diff --git a/qlib/workflow/cli.py b/qlib/workflow/cli.py index 6acbee66e..f660a8098 100644 --- a/qlib/workflow/cli.py +++ b/qlib/workflow/cli.py @@ -6,8 +6,8 @@ from pathlib import Path import qlib import fire -import yaml import pandas as pd +import ruamel.yaml as yaml from qlib.config import REG_CN from qlib.utils import init_instance_by_config from qlib.workflow import R @@ -16,7 +16,7 @@ from qlib.workflow.record_temp import SignalRecord # worflow handler function def workflow(config_path): with open(config_path) as fp: - config = yaml.load(fp, Loader=yaml.FullLoader) + config = yaml.load(fp, Loader=yaml.Loader) provider_uri = config.get("provider_uri") qlib.init(provider_uri=provider_uri, region=REG_CN) @@ -26,7 +26,8 @@ def workflow(config_path): dataset = init_instance_by_config(config.get("task")["dataset"]) # start exp - with R.start("workflow"): + with R.start(experiment_name="workflow"): + R.log_paramters(**flatten_dict(task)) model.fit(dataset) recorder = R.get_recorder() diff --git a/qlib/workflow/record_temp.py b/qlib/workflow/record_temp.py index d6b4d608e..7d4c79364 100644 --- a/qlib/workflow/record_temp.py +++ b/qlib/workflow/record_temp.py @@ -1,6 +1,7 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. +import re import pandas as pd from pathlib import Path from pprint import pprint @@ -37,12 +38,14 @@ class RecordTemp: """ raise NotImplementedError(f"Please implement the `generate` method.") - def load(self, **kwargs): + def load(self, name, **kwargs): """ Load the stored records. Parameters ---------- + name : str + the name for the file to be load. kwargs Return @@ -51,6 +54,16 @@ class RecordTemp: """ raise NotImplementedError(f"Please implement the `load` method.") + def list(self): + """ + List the stored records. + + Return + ------ + A list of all the stored records. + """ + raise NotImplementedError(f"Please implement the `list` method.") + def check(self, **kwargs): """ Check if the records is properly generated and saved. @@ -81,6 +94,8 @@ class SignalRecord(RecordTemp): def generate(self, **kwargs): # generate prediciton pred = self.model.predict(self.dataset) + if isinstance(pred, pd.Series): + pred = pred.to_frame("score") self.recorder.save_objects(**{"pred.pkl": pred}) logger.info( f"Signal record 'pred.pkl' has been saved as the artifact of the Experiment {self.recorder.experiment_id}" @@ -89,11 +104,14 @@ class SignalRecord(RecordTemp): pprint(f"The following are prediction results of the {type(self.model).__name__} model.") pprint(pred.head(5)) - def load(self): + def load(self, name="pred.pkl"): # try to load the saved object - pred = self.recorder.load_object("pred.pkl") + pred = self.recorder.load_object(name) return pred + def list(self): + return ["pred.pkl"] + def check(self, **kwargs): artifacts = self.recorder.list_artifacts() for artifact in artifacts: @@ -165,10 +183,20 @@ class PortAnaRecord(SignalRecord): pprint("The following are analysis results of the excess return with cost.") pprint(analysis["excess_return_with_cost"]) - def load(self): + def load(self, name): # try to load the saved object - pred = self.recorder.load_object(self.artifact_path / "port_analysis.pkl") - return pred + if self.artifact_path not in name: + file_name = re.split(r" |/|\\", name)[-1] + name = f"{self.artifact_path}/{file_name}" + result = self.recorder.load_object(name) + return result + + def list(self): + return [ + f"{self.artifact_path}/report_normal.pkl", + f"{self.artifact_path}/positions_normal.pkl", + f"{self.artifact_path}/port_analysis.pkl", + ] def check(self): artifacts = self.recorder.list_artifacts(self.artifact_path) From 21c0dae03c4161427904875fa3e5509b0e950b12 Mon Sep 17 00:00:00 2001 From: Jactus Date: Thu, 19 Nov 2020 16:51:09 +0800 Subject: [PATCH 058/241] Init benchmarks --- examples/benchmarks/CatBoost/requirements.txt | 3 + examples/benchmarks/DNN/requirements.txt | 4 ++ .../benchmarks/DNN/workflow_config_dnn.yaml | 62 ++++++++++++++++++ examples/benchmarks/GATs/requirements.txt | 4 ++ .../benchmarks/GATs/worflow_config_gats.yaml | 63 ++++++++++++++++++ examples/benchmarks/GBDT/requirements.txt | 3 + .../benchmarks/GBDT/workflow_config_gbdt.yaml | 59 +++++++++++++++++ examples/benchmarks/GRU/requirements.txt | 4 ++ .../benchmarks/GRU/workflow_config_gru.yaml | 62 ++++++++++++++++++ examples/benchmarks/LSTM/requirements.txt | 4 ++ .../benchmarks/LSTM/workflow_config_lstm.yaml | 62 ++++++++++++++++++ examples/benchmarks/XGBoost/requirements.txt | 3 + .../XGBoost/workflow_config_xgboost.yaml | 62 ++++++++++++++++++ examples/benchmarks/XGBoost/xgboost.py | 64 +++++++++++++++++++ 14 files changed, 459 insertions(+) create mode 100644 examples/benchmarks/CatBoost/requirements.txt create mode 100644 examples/benchmarks/DNN/requirements.txt create mode 100644 examples/benchmarks/DNN/workflow_config_dnn.yaml create mode 100644 examples/benchmarks/GATs/requirements.txt create mode 100644 examples/benchmarks/GATs/worflow_config_gats.yaml create mode 100644 examples/benchmarks/GBDT/requirements.txt create mode 100644 examples/benchmarks/GBDT/workflow_config_gbdt.yaml create mode 100644 examples/benchmarks/GRU/requirements.txt create mode 100644 examples/benchmarks/GRU/workflow_config_gru.yaml create mode 100644 examples/benchmarks/LSTM/requirements.txt create mode 100644 examples/benchmarks/LSTM/workflow_config_lstm.yaml create mode 100644 examples/benchmarks/XGBoost/requirements.txt create mode 100644 examples/benchmarks/XGBoost/workflow_config_xgboost.yaml create mode 100755 examples/benchmarks/XGBoost/xgboost.py diff --git a/examples/benchmarks/CatBoost/requirements.txt b/examples/benchmarks/CatBoost/requirements.txt new file mode 100644 index 000000000..507a65944 --- /dev/null +++ b/examples/benchmarks/CatBoost/requirements.txt @@ -0,0 +1,3 @@ +pandas==1.1.2 +numpy==1.17.4 +catboost==0.24.3 diff --git a/examples/benchmarks/DNN/requirements.txt b/examples/benchmarks/DNN/requirements.txt new file mode 100644 index 000000000..16de0a438 --- /dev/null +++ b/examples/benchmarks/DNN/requirements.txt @@ -0,0 +1,4 @@ +pandas==1.1.2 +numpy==1.17.4 +scikit_learn==0.23.2 +torch==1.7.0 diff --git a/examples/benchmarks/DNN/workflow_config_dnn.yaml b/examples/benchmarks/DNN/workflow_config_dnn.yaml new file mode 100644 index 000000000..0f50cbb25 --- /dev/null +++ b/examples/benchmarks/DNN/workflow_config_dnn.yaml @@ -0,0 +1,62 @@ +provider_uri: "~/.qlib/qlib_data/cn_data" +market: &market csi300 +benchmark: &benchmark SH000300 +data_handler_config: &data_handler_config + start_time: 2008-01-01 + end_time: 2020-08-01 + fit_start_time: 2008-01-01 + fit_end_time: 2014-12-31 + instruments: *market +port_analysis_config: &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 +task: + model: + class: DNNModelPytorch + module_path: qlib.contrib.model.pytorch_nn + kwargs: + input_dim: 360 + output_dim: 1 + layers: [256, 512, 1024, 512, 256, 128, 64] + lr: 0.001 + max_steps: 300 + batch_size: 2000 + early_stop_rounds: 50 + eval_steps: 20 + lr_decay: 0.96 + lr_decay_steps: 100 + optimizer: gd + loss: mse + dataset: + class: DatasetH + module_path: qlib.data.dataset + kwargs: + handler: + class: ALPHA360_Denoise + module_path: qlib.contrib.data.handler + kwargs: *data_handler_config + segments: + train: [2008-01-01, 2014-12-31] + valid: [2015-01-01, 2016-12-31] + test: [2017-01-01, 2020-08-01] + record: + - class: SignalRecord + module_path: qlib.workflow.record_temp + kwargs: {} + - class: PortAnaRecord + module_path: qlib.workflow.record_temp + kwargs: + config: *port_analysis_config \ No newline at end of file diff --git a/examples/benchmarks/GATs/requirements.txt b/examples/benchmarks/GATs/requirements.txt new file mode 100644 index 000000000..16de0a438 --- /dev/null +++ b/examples/benchmarks/GATs/requirements.txt @@ -0,0 +1,4 @@ +pandas==1.1.2 +numpy==1.17.4 +scikit_learn==0.23.2 +torch==1.7.0 diff --git a/examples/benchmarks/GATs/worflow_config_gats.yaml b/examples/benchmarks/GATs/worflow_config_gats.yaml new file mode 100644 index 000000000..6c8db2e77 --- /dev/null +++ b/examples/benchmarks/GATs/worflow_config_gats.yaml @@ -0,0 +1,63 @@ +provider_uri: "~/.qlib/qlib_data/cn_data" +market: &market csi300 +benchmark: &benchmark SH000300 +data_handler_config: &data_handler_config + start_time: 2008-01-01 + end_time: 2020-08-01 + fit_start_time: 2008-01-01 + fit_end_time: 2014-12-31 + instruments: *market +port_analysis_config: &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 +task: + model: + class: GAT + module_path: qlib.contrib.model.pytorch_gats + kwargs: + d_feat: 6 + hidden_size: 64 + num_layers: 2 + dropout: 0.0 + n_epochs: 200 + lr: 1e-3 + early_stop: 20 + batch_size: 800 + metric: IC + loss: mse + base_model: GRU + seed: 0 + GPU: 0 + dataset: + class: DatasetH + module_path: qlib.data.dataset + kwargs: + handler: + class: ALPHA360_Denoise + module_path: qlib.contrib.data.handler + kwargs: *data_handler_config + segments: + train: [2008-01-01, 2014-12-31] + valid: [2015-01-01, 2016-12-31] + test: [2017-01-01, 2020-08-01] + record: + - class: SignalRecord + module_path: qlib.workflow.record_temp + kwargs: {} + - class: PortAnaRecord + module_path: qlib.workflow.record_temp + kwargs: + config: *port_analysis_config \ No newline at end of file diff --git a/examples/benchmarks/GBDT/requirements.txt b/examples/benchmarks/GBDT/requirements.txt new file mode 100644 index 000000000..507d2d453 --- /dev/null +++ b/examples/benchmarks/GBDT/requirements.txt @@ -0,0 +1,3 @@ +pandas==1.1.2 +numpy==1.17.4 +lightgbm==3.1.0 diff --git a/examples/benchmarks/GBDT/workflow_config_gbdt.yaml b/examples/benchmarks/GBDT/workflow_config_gbdt.yaml new file mode 100644 index 000000000..212558044 --- /dev/null +++ b/examples/benchmarks/GBDT/workflow_config_gbdt.yaml @@ -0,0 +1,59 @@ +provider_uri: "~/.qlib/qlib_data/cn_data" +market: &market csi300 +benchmark: &benchmark SH000300 +data_handler_config: &data_handler_config + start_time: 2008-01-01 + end_time: 2020-08-01 + fit_start_time: 2008-01-01 + fit_end_time: 2014-12-31 + instruments: *market +port_analysis_config: &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 +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: [2008-01-01, 2014-12-31] + valid: [2015-01-01, 2016-12-31] + test: [2017-01-01, 2020-08-01] + record: + - class: SignalRecord + module_path: qlib.workflow.record_temp + kwargs: {} + - class: PortAnaRecord + module_path: qlib.workflow.record_temp + kwargs: + config: *port_analysis_config \ No newline at end of file diff --git a/examples/benchmarks/GRU/requirements.txt b/examples/benchmarks/GRU/requirements.txt new file mode 100644 index 000000000..1fc2779c0 --- /dev/null +++ b/examples/benchmarks/GRU/requirements.txt @@ -0,0 +1,4 @@ +numpy==1.17.4 +pandas==1.1.2 +scikit_learn==0.23.2 +torch==1.7.0 diff --git a/examples/benchmarks/GRU/workflow_config_gru.yaml b/examples/benchmarks/GRU/workflow_config_gru.yaml new file mode 100644 index 000000000..49b6159dc --- /dev/null +++ b/examples/benchmarks/GRU/workflow_config_gru.yaml @@ -0,0 +1,62 @@ +provider_uri: "~/.qlib/qlib_data/cn_data" +market: &market csi300 +benchmark: &benchmark SH000300 +data_handler_config: &data_handler_config + start_time: 2008-01-01 + end_time: 2020-08-01 + fit_start_time: 2008-01-01 + fit_end_time: 2014-12-31 + instruments: *market +port_analysis_config: &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 +task: + model: + class: GRU + module_path: qlib.contrib.model.pytorch_gru + kwargs: + d_feat: 6 + hidden_size: 64 + num_layers: 2 + dropout: 0.0 + n_epochs: 200 + lr: 1e-3 + early_stop: 20 + batch_size: 800 + metric: IC + loss: mse + seed: 0 + GPU: 0 + dataset: + class: DatasetH + module_path: qlib.data.dataset + kwargs: + handler: + class: ALPHA360_Denoise + module_path: qlib.contrib.data.handler + kwargs: *data_handler_config + segments: + train: [2008-01-01, 2014-12-31] + valid: [2015-01-01, 2016-12-31] + test: [2017-01-01, 2020-08-01] + record: + - class: SignalRecord + module_path: qlib.workflow.record_temp + kwargs: {} + - class: PortAnaRecord + module_path: qlib.workflow.record_temp + kwargs: + config: *port_analysis_config \ No newline at end of file diff --git a/examples/benchmarks/LSTM/requirements.txt b/examples/benchmarks/LSTM/requirements.txt new file mode 100644 index 000000000..1fc2779c0 --- /dev/null +++ b/examples/benchmarks/LSTM/requirements.txt @@ -0,0 +1,4 @@ +numpy==1.17.4 +pandas==1.1.2 +scikit_learn==0.23.2 +torch==1.7.0 diff --git a/examples/benchmarks/LSTM/workflow_config_lstm.yaml b/examples/benchmarks/LSTM/workflow_config_lstm.yaml new file mode 100644 index 000000000..1e3b309d2 --- /dev/null +++ b/examples/benchmarks/LSTM/workflow_config_lstm.yaml @@ -0,0 +1,62 @@ +provider_uri: "~/.qlib/qlib_data/cn_data" +market: &market csi300 +benchmark: &benchmark SH000300 +data_handler_config: &data_handler_config + start_time: 2008-01-01 + end_time: 2020-08-01 + fit_start_time: 2008-01-01 + fit_end_time: 2014-12-31 + instruments: *market +port_analysis_config: &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 +task: + model: + class: LSTM + module_path: qlib.contrib.model.pytorch_lstm + kwargs: + d_feat: 6 + hidden_size: 64 + num_layers: 2 + dropout: 0.0 + n_epochs: 200 + lr: 1e-3 + early_stop: 20 + batch_size: 800 + metric: IC + loss: mse + seed: 0 + GPU: 0 + dataset: + class: DatasetH + module_path: qlib.data.dataset + kwargs: + handler: + class: ALPHA360_Denoise + module_path: qlib.contrib.data.handler + kwargs: *data_handler_config + segments: + train: [2008-01-01, 2014-12-31] + valid: [2015-01-01, 2016-12-31] + test: [2017-01-01, 2020-08-01] + record: + - class: SignalRecord + module_path: qlib.workflow.record_temp + kwargs: {} + - class: PortAnaRecord + module_path: qlib.workflow.record_temp + kwargs: + config: *port_analysis_config \ No newline at end of file diff --git a/examples/benchmarks/XGBoost/requirements.txt b/examples/benchmarks/XGBoost/requirements.txt new file mode 100644 index 000000000..077f343e5 --- /dev/null +++ b/examples/benchmarks/XGBoost/requirements.txt @@ -0,0 +1,3 @@ +numpy==1.17.4 +pandas==1.1.2 +xgboost==1.2.1 \ No newline at end of file diff --git a/examples/benchmarks/XGBoost/workflow_config_xgboost.yaml b/examples/benchmarks/XGBoost/workflow_config_xgboost.yaml new file mode 100644 index 000000000..497ffa5b6 --- /dev/null +++ b/examples/benchmarks/XGBoost/workflow_config_xgboost.yaml @@ -0,0 +1,62 @@ +provider_uri: "~/.qlib/qlib_data/cn_data" +market: &market csi300 +benchmark: &benchmark SH000300 +data_handler_config: &data_handler_config + start_time: 2008-01-01 + end_time: 2020-08-01 + fit_start_time: 2008-01-01 + fit_end_time: 2014-12-31 + instruments: *market +port_analysis_config: &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 +task: + model: + class: XGBModel + module_path: qlib.contrib.model.xgboost + kwargs: + objective: reg:linear + n_estimators: 5000 + colsample_bytree: 0.85 + learning_rate: 0.0421 + subsample: 0.8789 + max_depth: 8 + num_leaves: 210 + num_threads: 20 + missing: -1 + min_child_weight: 1 + nthread: 4 + tree_method: hist + dataset: + class: DatasetH + module_path: qlib.data.dataset + kwargs: + handler: + class: Alpha158 + module_path: qlib.contrib.data.handler + kwargs: *data_handler_config + segments: + train: [2008-01-01, 2014-12-31] + valid: [2015-01-01, 2016-12-31] + test: [2017-01-01, 2020-08-01] + record: + - class: SignalRecord + module_path: qlib.workflow.record_temp + kwargs: {} + - class: PortAnaRecord + module_path: qlib.workflow.record_temp + kwargs: + config: *port_analysis_config \ No newline at end of file diff --git a/examples/benchmarks/XGBoost/xgboost.py b/examples/benchmarks/XGBoost/xgboost.py new file mode 100755 index 000000000..f1208eb93 --- /dev/null +++ b/examples/benchmarks/XGBoost/xgboost.py @@ -0,0 +1,64 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +import numpy as np +import pandas as pd +import xgboost as xgb + +from ...model.base import Model +from ...data.dataset import DatasetH +from ...data.dataset.handler import DataHandlerLP + + +class XGBModel(Model): + """XGBModel Model""" + + def __init__(self, obj="mse", **kwargs): + if obj not in {"mse", "binary"}: + raise NotImplementedError + self._params = {"obj": obj} + self._params.update(kwargs) + self.model = None + + def fit( + self, + dataset: DatasetH, + num_boost_round=1000, + early_stopping_rounds=50, + verbose_eval=20, + evals_result=dict(), + **kwargs + ): + + df_train, df_valid = dataset.prepare( + ["train", "valid"], col_set=["feature", "label"], data_key=DataHandlerLP.DK_L + ) + x_train, y_train = df_train["feature"], df_train["label"] + x_valid, y_valid = df_valid["feature"], df_valid["label"] + + # Lightgbm need 1D array as its label + if y_train.values.ndim == 2 and y_train.values.shape[1] == 1: + y_train_1d, y_valid_1d = np.squeeze(y_train.values), np.squeeze(y_valid.values) + else: + raise ValueError("XGBoost doesn't support multi-label training") + + dtrain = xgb.DMatrix(x_train.values, label=y_train_1d) + dvalid = xgb.DMatrix(x_valid.values, label=y_valid_1d) + self.model = xgb.train( + self._params, + dtrain=dtrain, + num_boost_round=num_boost_round, + evals=[(dtrain, "train"), (dvalid, "valid")], + early_stopping_rounds=early_stopping_rounds, + verbose_eval=verbose_eval, + evals_result=evals_result, + **kwargs + ) + evals_result["train"] = list(evals_result["train"].values())[0] + evals_result["valid"] = list(evals_result["valid"].values())[0] + + def predict(self, dataset): + if self.model is None: + raise ValueError("model is not fitted yet!") + x_test = dataset.prepare("test", col_set="feature") + return pd.Series(self.model.predict(xgb.DMatrix(np.squeeze(x_test.values))), index=x_test.index) From aa971e017a7ddde3af94bb4fb16a8d9cef23f592 Mon Sep 17 00:00:00 2001 From: Young Date: Thu, 19 Nov 2020 09:13:05 +0000 Subject: [PATCH 059/241] fix handler bug --- qlib/data/dataset/handler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qlib/data/dataset/handler.py b/qlib/data/dataset/handler.py index d32b251de..422cc6b1d 100644 --- a/qlib/data/dataset/handler.py +++ b/qlib/data/dataset/handler.py @@ -289,7 +289,7 @@ class DataHandlerLP(DataHandler): getattr(self, pname).append( init_instance_by_config( proc, - None if (isinstance(data_loader, dict) and "module_path" in data_loader) else data_loader_module, + None if (isinstance(proc, dict) and "module_path" in proc) else processor_module, accept_types=processor_module.Processor)) self.process_type = process_type From c91698287acaa0e5b7d6f0155a8acb7ba310fe2c Mon Sep 17 00:00:00 2001 From: Jactus Date: Thu, 19 Nov 2020 17:18:18 +0800 Subject: [PATCH 060/241] Add catboost config and notebook --- .../CatBoost/workflow_config_catboost.yaml | 53 +++ examples/benchmarks/XGBoost/xgboost.py | 64 ---- examples/workflow_by_code.ipynb | 330 ++++++++++++++++++ examples/workflow_by_code.py | 17 +- examples/workflow_by_code_finetune.py | 14 +- qlib/config.py | 2 +- qlib/data/dataset/handler.py | 8 +- qlib/data/dataset/utils.py | 13 +- qlib/workflow/utils.py | 1 + 9 files changed, 415 insertions(+), 87 deletions(-) create mode 100644 examples/benchmarks/CatBoost/workflow_config_catboost.yaml delete mode 100755 examples/benchmarks/XGBoost/xgboost.py create mode 100644 examples/workflow_by_code.ipynb diff --git a/examples/benchmarks/CatBoost/workflow_config_catboost.yaml b/examples/benchmarks/CatBoost/workflow_config_catboost.yaml new file mode 100644 index 000000000..187cd116b --- /dev/null +++ b/examples/benchmarks/CatBoost/workflow_config_catboost.yaml @@ -0,0 +1,53 @@ +provider_uri: "~/.qlib/qlib_data/cn_data" +market: &market csi300 +benchmark: &benchmark SH000300 +data_handler_config: &data_handler_config + start_time: 2008-01-01 + end_time: 2020-08-01 + fit_start_time: 2008-01-01 + fit_end_time: 2014-12-31 + instruments: *market +port_analysis_config: &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 +task: + model: + class: CatBoostModel + module_path: qlib.contrib.model.catboost_model + kwargs: + loss: RMSE + iterations: 5 + learning_rate: 0.03 + dataset: + class: DatasetH + module_path: qlib.data.dataset + kwargs: + handler: + class: ALPHA360_Denoise + module_path: qlib.contrib.data.handler + kwargs: *data_handler_config + segments: + train: [2008-01-01, 2014-12-31] + valid: [2015-01-01, 2016-12-31] + test: [2017-01-01, 2020-08-01] + record: + - class: SignalRecord + module_path: qlib.workflow.record_temp + kwargs: {} + - class: PortAnaRecord + module_path: qlib.workflow.record_temp + kwargs: + config: *port_analysis_config \ No newline at end of file diff --git a/examples/benchmarks/XGBoost/xgboost.py b/examples/benchmarks/XGBoost/xgboost.py deleted file mode 100755 index f1208eb93..000000000 --- a/examples/benchmarks/XGBoost/xgboost.py +++ /dev/null @@ -1,64 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. - -import numpy as np -import pandas as pd -import xgboost as xgb - -from ...model.base import Model -from ...data.dataset import DatasetH -from ...data.dataset.handler import DataHandlerLP - - -class XGBModel(Model): - """XGBModel Model""" - - def __init__(self, obj="mse", **kwargs): - if obj not in {"mse", "binary"}: - raise NotImplementedError - self._params = {"obj": obj} - self._params.update(kwargs) - self.model = None - - def fit( - self, - dataset: DatasetH, - num_boost_round=1000, - early_stopping_rounds=50, - verbose_eval=20, - evals_result=dict(), - **kwargs - ): - - df_train, df_valid = dataset.prepare( - ["train", "valid"], col_set=["feature", "label"], data_key=DataHandlerLP.DK_L - ) - x_train, y_train = df_train["feature"], df_train["label"] - x_valid, y_valid = df_valid["feature"], df_valid["label"] - - # Lightgbm need 1D array as its label - if y_train.values.ndim == 2 and y_train.values.shape[1] == 1: - y_train_1d, y_valid_1d = np.squeeze(y_train.values), np.squeeze(y_valid.values) - else: - raise ValueError("XGBoost doesn't support multi-label training") - - dtrain = xgb.DMatrix(x_train.values, label=y_train_1d) - dvalid = xgb.DMatrix(x_valid.values, label=y_valid_1d) - self.model = xgb.train( - self._params, - dtrain=dtrain, - num_boost_round=num_boost_round, - evals=[(dtrain, "train"), (dvalid, "valid")], - early_stopping_rounds=early_stopping_rounds, - verbose_eval=verbose_eval, - evals_result=evals_result, - **kwargs - ) - evals_result["train"] = list(evals_result["train"].values())[0] - evals_result["valid"] = list(evals_result["valid"].values())[0] - - def predict(self, dataset): - if self.model is None: - raise ValueError("model is not fitted yet!") - x_test = dataset.prepare("test", col_set="feature") - return pd.Series(self.model.predict(xgb.DMatrix(np.squeeze(x_test.values))), index=x_test.index) diff --git a/examples/workflow_by_code.ipynb b/examples/workflow_by_code.ipynb new file mode 100644 index 000000000..f07c4f19e --- /dev/null +++ b/examples/workflow_by_code.ipynb @@ -0,0 +1,330 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import sys\n", + "from pathlib import Path\n", + "\n", + "import qlib\n", + "import pandas as pd\n", + "from qlib.config import REG_CN\n", + "from qlib.contrib.model.gbdt import LGBModel\n", + "from qlib.contrib.estimator.handler import Alpha158\n", + "from qlib.contrib.strategy.strategy import TopkDropoutStrategy\n", + "from qlib.contrib.evaluate import (\n", + " backtest as normal_backtest,\n", + " risk_analysis,\n", + ")\n", + "from qlib.utils import exists_qlib_data, init_instance_by_config\n", + "from qlib.workflow import R\n", + "from qlib.workflow.record_temp import SignalRecord, PortAnaRecord" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# use default data\n", + "# NOTE: need to download data from remote: python scripts/get_data.py qlib_data_cn --target_dir ~/.qlib/qlib_data/cn_data\n", + "provider_uri = \"~/.qlib/qlib_data/cn_data\" # target_dir\n", + "if not exists_qlib_data(provider_uri):\n", + " print(f\"Qlib data is not found in {provider_uri}\")\n", + " sys.path.append(str(Path.cwd().parent.joinpath(\"scripts\")))\n", + " from get_data import GetData\n", + " GetData().qlib_data_cn(target_dir=provider_uri)\n", + "qlib.init(provider_uri=provider_uri, region=REG_CN)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "market = \"csi300\"\n", + "benchmark = \"SH000300\"" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# train model" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "###################################\n", + "# train model\n", + "###################################\n", + "data_handler_config = {\n", + " \"start_time\": \"2008-01-01\",\n", + " \"end_time\": \"2020-08-01\",\n", + " \"fit_start_time\": \"2008-01-01\",\n", + " \"fit_end_time\": \"2014-12-31\",\n", + " \"instruments\": market,\n", + "}\n", + "\n", + "task = {\n", + " \"model\": {\n", + " \"class\": \"LGBModel\",\n", + " \"module_path\": \"qlib.contrib.model.gbdt\",\n", + " \"kwargs\": {\n", + " \"loss\": \"mse\",\n", + " \"colsample_bytree\": 0.8879,\n", + " \"learning_rate\": 0.0421,\n", + " \"subsample\": 0.8789,\n", + " \"lambda_l1\": 205.6999,\n", + " \"lambda_l2\": 580.9768,\n", + " \"max_depth\": 8,\n", + " \"num_leaves\": 210,\n", + " \"num_threads\": 20,\n", + " },\n", + " },\n", + " \"dataset\": {\n", + " \"class\": \"DatasetH\",\n", + " \"module_path\": \"qlib.data.dataset\",\n", + " \"kwargs\": {\n", + " \"handler\": {\n", + " \"class\": \"Alpha158\",\n", + " \"module_path\": \"qlib.contrib.data.handler\",\n", + " \"kwargs\": data_handler_config,\n", + " },\n", + " \"segments\": {\n", + " \"train\": (\"2008-01-01\", \"2014-12-31\"),\n", + " \"valid\": (\"2015-01-01\", \"2016-12-31\"),\n", + " \"test\": (\"2017-01-01\", \"2020-08-01\"),\n", + " },\n", + " },\n", + " },\n", + "}\n", + "\n", + "# model initiaiton\n", + "model = init_instance_by_config(task[\"model\"])\n", + "dataset = init_instance_by_config(task[\"dataset\"])\n", + "\n", + "# start exp to train model\n", + "with R.start(experiment_name=\"train_model\"):\n", + " R.log_paramters(**flatten_dict(task))\n", + " model.fit(dataset)\n", + " R.save_objects(trained_model=model)\n", + " rid = R.get_recorder().id\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# prediction, backtest & analysis" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "###################################\n", + "# prediction, backtest & analysis\n", + "###################################\n", + "port_analysis_config = {\n", + " \"strategy\": {\n", + " \"class\": \"TopkDropoutStrategy\",\n", + " \"module_path\": \"qlib.contrib.strategy.strategy\",\n", + " \"kwargs\": {\n", + " \"topk\": 50,\n", + " \"n_drop\": 5,\n", + " },\n", + " },\n", + " \"backtest\": {\n", + " \"verbose\": False,\n", + " \"limit_threshold\": 0.095,\n", + " \"account\": 100000000,\n", + " \"benchmark\": benchmark,\n", + " \"deal_price\": \"close\",\n", + " \"open_cost\": 0.0005,\n", + " \"close_cost\": 0.0015,\n", + " \"min_cost\": 5,\n", + " },\n", + "}\n", + "\n", + "\n", + "# backtest and analysis\n", + "with R.start(experiment_name=\"backtest_analysis\"):\n", + " recorder = R.get_recorder(rid, experiment_name=\"train_model\")\n", + " model = recorder.load_object(\"trained_model\")\n", + "\n", + " # prediction\n", + " recorder = R.get_recorder()\n", + " ba_rid = recorder.id\n", + " sr = SignalRecord(model, dataset, recorder)\n", + " sr.generate()\n", + "\n", + " # backtest & analysis\n", + " par = PortAnaRecord(recorder, port_analysis_config)\n", + " par.generate()\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# analyze graphs" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from qlib.contrib.report import analysis_model, analysis_position\n", + "from qlib.data import D\n", + "recorder = R.get_recorder(ba_rid, experiment_name=\"backtest_analysis\")\n", + "pred_df = recorder.load_object(\"pred.pkl\")\n", + "pred_df_dates = pred_df.index.get_level_values(level='datetime')\n", + "report_normal_df = recorder.load_object(\"portfolio_analysis/report_normal.pkl\")\n", + "positions = recorder.load_object(\"portfolio_analysis/positions_normal.pkl\")\n", + "analysis_df = recorder.load_object(\"portfolio_analysis/port_analysis.pkl\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## analysis position" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### report" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "analysis_position.report_graph(report_normal_df)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### risk analysis" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "analysis_position.risk_analysis_graph(analysis_df, report_normal_df)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## analysis model" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "label_df = dataset.prepare(\"test\", col_set=\"label\")\n", + "label_df.columns = ['label']" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### score IC" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "pred_label = pd.concat([label_df, pred_df], axis=1, sort=True).reindex(label_df.index)\n", + "analysis_position.score_ic_graph(pred_label)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### model performance" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "analysis_model.model_performance_graph(pred_label)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3" + }, + "toc": { + "base_numbering": 1, + "nav_menu": {}, + "number_sections": true, + "sideBar": true, + "skip_h1_title": false, + "title_cell": "Table of Contents", + "title_sidebar": "Contents", + "toc_cell": false, + "toc_position": {}, + "toc_section_display": true, + "toc_window_display": false + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} \ No newline at end of file diff --git a/examples/workflow_by_code.py b/examples/workflow_by_code.py index b70a9e963..9aaa02f35 100644 --- a/examples/workflow_by_code.py +++ b/examples/workflow_by_code.py @@ -32,18 +32,18 @@ if __name__ == "__main__": qlib.init(provider_uri=provider_uri, region=REG_CN) - MARKET = "csi300" - BENCHMARK = "SH000300" + market = "csi300" + benchmark = "SH000300" ################################### # train model ################################### - DATA_HANDLER_CONFIG = { + data_handler_config = { "start_time": "2008-01-01", "end_time": "2020-08-01", "fit_start_time": "2008-01-01", "fit_end_time": "2014-12-31", - "instruments": MARKET, + "instruments": market, } task = { @@ -69,7 +69,7 @@ if __name__ == "__main__": "handler": { "class": "Alpha158", "module_path": "qlib.contrib.data.handler", - "kwargs": DATA_HANDLER_CONFIG, + "kwargs": data_handler_config, }, "segments": { "train": ("2008-01-01", "2014-12-31"), @@ -78,8 +78,6 @@ if __name__ == "__main__": }, }, }, - # You shoud record the data in specific sequence - "record": ["SignalRecord", "PortAnaRecord"], } port_analysis_config = { @@ -95,7 +93,7 @@ if __name__ == "__main__": "verbose": False, "limit_threshold": 0.095, "account": 100000000, - "benchmark": BENCHMARK, + "benchmark": benchmark, "deal_price": "close", "open_cost": 0.0005, "close_cost": 0.0015, @@ -108,7 +106,8 @@ if __name__ == "__main__": dataset = init_instance_by_config(task["dataset"]) # start exp - with R.start("workflow"): + with R.start(experiment_name="workflow"): + R.log_paramters(**flatten_dict(task)) model.fit(dataset) # prediction diff --git a/examples/workflow_by_code_finetune.py b/examples/workflow_by_code_finetune.py index 6df8c9821..c69ecc350 100644 --- a/examples/workflow_by_code_finetune.py +++ b/examples/workflow_by_code_finetune.py @@ -32,18 +32,18 @@ if __name__ == "__main__": qlib.init(provider_uri=provider_uri, region=REG_CN) - MARKET = "csi300" - BENCHMARK = "SH000300" + market = "csi300" + benchmark = "SH000300" ################################### # train model ################################### - DATA_HANDLER_CONFIG = { + data_handler_config = { "start_time": "2008-01-01", "end_time": "2020-08-01", "fit_start_time": "2008-01-01", "fit_end_time": "2014-12-31", - "instruments": MARKET, + "instruments": market, } task = { @@ -69,7 +69,7 @@ if __name__ == "__main__": "handler": { "class": "Alpha158", "module_path": "qlib.contrib.data.handler", - "kwargs": DATA_HANDLER_CONFIG, + "kwargs": data_handler_config, }, "segments": { "train": ("2008-01-01", "2014-12-31"), @@ -78,8 +78,6 @@ if __name__ == "__main__": }, }, }, - # You shoud record the data in specific sequence - "record": ["SignalRecord", "PortAnaRecord"], } port_analysis_config = { @@ -95,7 +93,7 @@ if __name__ == "__main__": "verbose": False, "limit_threshold": 0.095, "account": 100000000, - "benchmark": BENCHMARK, + "benchmark": benchmark, "deal_price": "close", "open_cost": 0.0005, "close_cost": 0.0015, diff --git a/qlib/config.py b/qlib/config.py index 90369c79f..d05161772 100644 --- a/qlib/config.py +++ b/qlib/config.py @@ -130,7 +130,7 @@ _default_config = { "class": "MLflowExpManager", "module_path": "qlib.workflow.expm", "kwargs": { - "uri": 'file:' + str(Path(os.getcwd()).resolve() / "mlruns"), + "uri": "file:" + str(Path(os.getcwd()).resolve() / "mlruns"), "default_exp_name": "Experiment", }, }, diff --git a/qlib/data/dataset/handler.py b/qlib/data/dataset/handler.py index d32b251de..1fead5070 100644 --- a/qlib/data/dataset/handler.py +++ b/qlib/data/dataset/handler.py @@ -289,8 +289,12 @@ class DataHandlerLP(DataHandler): getattr(self, pname).append( init_instance_by_config( proc, - None if (isinstance(data_loader, dict) and "module_path" in data_loader) else data_loader_module, - accept_types=processor_module.Processor)) + None + if (isinstance(data_loader, dict) and "module_path" in data_loader) + else data_loader_module, + accept_types=processor_module.Processor, + ) + ) self.process_type = process_type super().__init__(instruments, start_time, end_time, data_loader, **kwargs) diff --git a/qlib/data/dataset/utils.py b/qlib/data/dataset/utils.py index 85a5e8389..3fb3768a0 100644 --- a/qlib/data/dataset/utils.py +++ b/qlib/data/dataset/utils.py @@ -32,7 +32,10 @@ def get_level_index(df: pd.DataFrame, level=Union[str, int]) -> int: def fetch_df_by_index( - df: pd.DataFrame, selector: Union[pd.Timestamp, slice, str, list], level: Union[str, int], fetch_orig=True, + df: pd.DataFrame, + selector: Union[pd.Timestamp, slice, str, list], + level: Union[str, int], + fetch_orig=True, ) -> pd.DataFrame: """ fetch data from `data` with `selector` and `level` @@ -55,8 +58,12 @@ def fetch_df_by_index( if fetch_orig: for slc in idx_slc: if slc != slice(None, None): - return df.loc[pd.IndexSlice[idx_slc],] + return df.loc[ + pd.IndexSlice[idx_slc], + ] else: return df else: - return df.loc[pd.IndexSlice[idx_slc],] + return df.loc[ + pd.IndexSlice[idx_slc], + ] diff --git a/qlib/workflow/utils.py b/qlib/workflow/utils.py index 0f721e035..33d251dd8 100644 --- a/qlib/workflow/utils.py +++ b/qlib/workflow/utils.py @@ -5,6 +5,7 @@ import sys, traceback, signal, atexit from . import R from .recorder import Recorder from ..log import get_module_logger + logger = get_module_logger("workflow", "INFO") From 0f433571f63f2a97392c4ff76442a2cc2620672e Mon Sep 17 00:00:00 2001 From: Young Date: Thu, 19 Nov 2020 09:29:18 +0000 Subject: [PATCH 061/241] fix bug parameters --- examples/workflow_by_code.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/workflow_by_code.py b/examples/workflow_by_code.py index 9aaa02f35..b62ace155 100644 --- a/examples/workflow_by_code.py +++ b/examples/workflow_by_code.py @@ -14,7 +14,7 @@ from qlib.contrib.evaluate import ( backtest as normal_backtest, risk_analysis, ) -from qlib.utils import exists_qlib_data, init_instance_by_config +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 @@ -107,7 +107,7 @@ if __name__ == "__main__": # start exp with R.start(experiment_name="workflow"): - R.log_paramters(**flatten_dict(task)) + R.log_params(**flatten_dict(task)) model.fit(dataset) # prediction From 75d779ebe890bd3dab610ada1a6abf11a5bb9881 Mon Sep 17 00:00:00 2001 From: zhupr Date: Thu, 19 Nov 2020 22:48:24 +0800 Subject: [PATCH 062/241] fix report style --- qlib/contrib/report/analysis_position/report.py | 10 +++++----- qlib/contrib/report/analysis_position/risk_analysis.py | 6 +++++- qlib/contrib/report/graph.py | 8 ++++++-- setup.py | 2 +- 4 files changed, 17 insertions(+), 9 deletions(-) diff --git a/qlib/contrib/report/analysis_position/report.py b/qlib/contrib/report/analysis_position/report.py index 714cfdd9c..6d108cabf 100644 --- a/qlib/contrib/report/analysis_position/report.py +++ b/qlib/contrib/report/analysis_position/report.py @@ -100,13 +100,13 @@ def _report_figure(df: pd.DataFrame) -> [list, tuple]: ("cum_ex_return_wo_cost_mdd", dict(row=7, col=1, graph_kwargs=_temp_fill_args)), ] - _subplot_layout = dict( - xaxis=dict(showline=True, type="category", tickangle=45), - yaxis=dict(zeroline=True, showline=True, showticklabels=True), - ) - for i in range(2, 8): + _subplot_layout = dict() + for i in range(1, 8): # yaxis _subplot_layout.update({"yaxis{}".format(i): dict(zeroline=True, showline=True, showticklabels=True)}) + _show_line = i == 7 + _subplot_layout.update({"xaxis{}".format(i): dict(showline=_show_line, type="category", tickangle=45)}) + _layout_style = dict( height=1200, title=" ", diff --git a/qlib/contrib/report/analysis_position/risk_analysis.py b/qlib/contrib/report/analysis_position/risk_analysis.py index 89650c39e..124a9b3b0 100644 --- a/qlib/contrib/report/analysis_position/risk_analysis.py +++ b/qlib/contrib/report/analysis_position/risk_analysis.py @@ -116,7 +116,11 @@ def _get_risk_analysis_figure(analysis_df: pd.DataFrame) -> Iterable[py.Figure]: if analysis_df is None: return [] - _figure = SubplotsGraph(_get_all_risk_analysis(analysis_df), kind_map=dict(kind="BarGraph", kwargs={})).figure + _figure = SubplotsGraph( + _get_all_risk_analysis(analysis_df), + kind_map=dict(kind="BarGraph", kwargs={}), + subplots_kwargs={"rows": 4, "cols": 1}, + ).figure return (_figure,) diff --git a/qlib/contrib/report/graph.py b/qlib/contrib/report/graph.py index 07ed94f90..15cc5fd0e 100644 --- a/qlib/contrib/report/graph.py +++ b/qlib/contrib/report/graph.py @@ -125,7 +125,10 @@ class BaseGraph(object): :return: """ - return go.Figure(data=self.data, layout=self._get_layout()) + _figure = go.Figure(data=self.data, layout=self._get_layout()) + # NOTE: using default 3.x theme + _figure["layout"].update(template=None) + return _figure class ScatterGraph(BaseGraph): @@ -363,7 +366,8 @@ class SubplotsGraph(object): for k, v in self._sub_graph_layout.items(): self._figure["layout"][k].update(v) - self._figure["layout"].update(self._layout) + # NOTE: using default 3.x theme + self._figure["layout"].update(self._layout, template=None) @property def figure(self): diff --git a/setup.py b/setup.py index 8ad124750..581e66623 100644 --- a/setup.py +++ b/setup.py @@ -46,7 +46,7 @@ REQUIRED = [ "fire>=0.2.1", "statsmodels", "xlrd>=1.0.0", - "plotly==3.5.0", + "plotly==4.12.0", "matplotlib==3.1.3", "tables>=3.6.1", "pyyaml>=5.3.1", From 547697ddc6ff71b1449e46edf50395c3dde84554 Mon Sep 17 00:00:00 2001 From: Jactus Date: Fri, 20 Nov 2020 10:59:37 +0800 Subject: [PATCH 063/241] Modify cli --- qlib/workflow/cli.py | 9 +++++---- scripts/check_dump_bin.py | 4 +--- scripts/get_data.py | 4 +++- setup.py | 1 + tests/test_all_pipeline.py | 4 +++- tests/test_dump_data.py | 4 +++- 6 files changed, 16 insertions(+), 10 deletions(-) diff --git a/qlib/workflow/cli.py b/qlib/workflow/cli.py index f660a8098..7cce25809 100644 --- a/qlib/workflow/cli.py +++ b/qlib/workflow/cli.py @@ -14,7 +14,7 @@ from qlib.workflow import R from qlib.workflow.record_temp import SignalRecord # worflow handler function -def workflow(config_path): +def workflow(config_path, experiment_name="workflow"): with open(config_path) as fp: config = yaml.load(fp, Loader=yaml.Loader) @@ -26,12 +26,13 @@ def workflow(config_path): dataset = init_instance_by_config(config.get("task")["dataset"]) # start exp - with R.start(experiment_name="workflow"): - R.log_paramters(**flatten_dict(task)) + with R.start(experiment_name=experiment_name): + # train model + R.log_params(**flatten_dict(config.get("task"))) model.fit(dataset) recorder = R.get_recorder() - # generate records + # generate records: prediction, backtest, and analysis for record in config.get("task")["record"]: if record["class"] == SignalRecord.__name__: srconf = {"model": model, "dataset": dataset, "recorder": recorder} diff --git a/scripts/check_dump_bin.py b/scripts/check_dump_bin.py index 7c2ceccda..7c2e837af 100644 --- a/scripts/check_dump_bin.py +++ b/scripts/check_dump_bin.py @@ -108,9 +108,7 @@ class CheckBin: return self.COMPARE_ERROR def check(self): - """Check whether the bin file after ``dump_bin.py`` is executed is consistent with the original csv file data - - """ + """Check whether the bin file after ``dump_bin.py`` is executed is consistent with the original csv file data""" logger.info("start check......") error_list = [] diff --git a/scripts/get_data.py b/scripts/get_data.py index 661e31c5f..4c0595238 100644 --- a/scripts/get_data.py +++ b/scripts/get_data.py @@ -55,7 +55,9 @@ class GetData: for _file in tqdm(zp.namelist()): zp.extract(_file, str(target_dir.resolve())) - def qlib_data(self, name="qlib_data", target_dir="~/.qlib/qlib_data/cn_data", version="latest", interval="1d", region="cn"): + def qlib_data( + self, name="qlib_data", target_dir="~/.qlib/qlib_data/cn_data", version="latest", interval="1d", region="cn" + ): """download cn qlib data from remote Parameters diff --git a/setup.py b/setup.py index 8ad124750..d08e378cb 100644 --- a/setup.py +++ b/setup.py @@ -57,6 +57,7 @@ REQUIRED = [ "tornado", "joblib>=0.17.0", "fire>=0.3.1", + "ruamel.yaml>=0.16.12", ] # Numpy include diff --git a/tests/test_all_pipeline.py b/tests/test_all_pipeline.py index 2930489a2..04c399342 100644 --- a/tests/test_all_pipeline.py +++ b/tests/test_all_pipeline.py @@ -149,7 +149,9 @@ class TestAllFlow(unittest.TestCase): sys.path.append(str(Path(__file__).resolve().parent.parent.joinpath("scripts"))) from get_data import GetData - GetData().qlib_data(name="qlib_data_simple", region="cn", version="latest", interval="1d", target_dir=provider_uri) + GetData().qlib_data( + name="qlib_data_simple", region="cn", version="latest", interval="1d", target_dir=provider_uri + ) qlib.init(provider_uri=provider_uri, region=REG_CN) def test_0_train(self): diff --git a/tests/test_dump_data.py b/tests/test_dump_data.py index 01e6a3758..dfa7f8556 100644 --- a/tests/test_dump_data.py +++ b/tests/test_dump_data.py @@ -75,7 +75,9 @@ class TestDumpData(unittest.TestCase): def test_4_dump_features_simple(self): stock = self.STOCK_NAMES[0] - dump_data = DumpDataFix(csv_path=SOURCE_DIR.joinpath(f"{stock.lower()}.csv"), qlib_dir=QLIB_DIR, include_fields=self.FIELDS) + dump_data = DumpDataFix( + csv_path=SOURCE_DIR.joinpath(f"{stock.lower()}.csv"), qlib_dir=QLIB_DIR, include_fields=self.FIELDS + ) dump_data.dump() df = D.features([stock], self.QLIB_FIELDS) From 5107d46568ae5d54a908c5b31100a496bf0a6645 Mon Sep 17 00:00:00 2001 From: Jactus Date: Fri, 20 Nov 2020 11:10:44 +0800 Subject: [PATCH 064/241] Fix cli --- qlib/workflow/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qlib/workflow/cli.py b/qlib/workflow/cli.py index 7cce25809..b257d9237 100644 --- a/qlib/workflow/cli.py +++ b/qlib/workflow/cli.py @@ -9,7 +9,7 @@ import fire import pandas as pd import ruamel.yaml as yaml from qlib.config import REG_CN -from qlib.utils import init_instance_by_config +from qlib.utils import init_instance_by_config, flatten_dict from qlib.workflow import R from qlib.workflow.record_temp import SignalRecord From 38e76a6e406ff0cb922769a9f55694deef85f2a8 Mon Sep 17 00:00:00 2001 From: lwwang1995 Date: Fri, 20 Nov 2020 14:57:08 +0800 Subject: [PATCH 065/241] Add workflow example. --- examples/workflow_by_code_gats.py | 145 ++++++++++++++++++++++++++++++ examples/workflow_by_code_gru.py | 144 +++++++++++++++++++++++++++++ examples/workflow_by_code_lstm.py | 144 +++++++++++++++++++++++++++++ 3 files changed, 433 insertions(+) create mode 100644 examples/workflow_by_code_gats.py create mode 100644 examples/workflow_by_code_gru.py create mode 100644 examples/workflow_by_code_lstm.py diff --git a/examples/workflow_by_code_gats.py b/examples/workflow_by_code_gats.py new file mode 100644 index 000000000..06845d448 --- /dev/null +++ b/examples/workflow_by_code_gats.py @@ -0,0 +1,145 @@ +# 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.pytorch_gats import GAT +from qlib.contrib.data.handler import ALPHA360_Denoise +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 + +# from qlib.model.learner import train_model +from qlib.utils import init_instance_by_config + +import pickle + +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_cn(target_dir=provider_uri) + + qlib.init(provider_uri=provider_uri, region=REG_CN) + + MARKET = "csi300" + BENCHMARK = "SH000300" + + ################################### + # train model + ################################### + DATA_HANDLER_CONFIG = { + "start_time": "2008-01-01", + "end_time": "2020-08-01", + "fit_start_time": "2008-01-01", + "fit_end_time": "2014-12-31", + "instruments": MARKET, + } + + TRAINER_CONFIG = { + "train_start_time": "2008-01-01", + "train_end_time": "2014-12-31", + "validate_start_time": "2015-01-01", + "validate_end_time": "2016-12-31", + "test_start_time": "2017-01-01", + "test_end_time": "2020-08-01", + } + + task = { + "model": { + "class": "GAT", + "module_path": "qlib.contrib.model.pytorch_gats", + "kwargs": { + "d_feat": 6, + "hidden_size": 64, + "num_layers": 2, + "dropout": 0.0, + "n_epochs": 200, + "lr": 1e-3, + "early_stop": 20, + "batch_size": 800, + "metric": "IC", + "loss": "mse", + "base_model":"GRU", + "seed": 0, + "GPU": 0, + }, + }, + "dataset": { + "class": "DatasetH", + "module_path": "qlib.data.dataset", + "kwargs": { + "handler": { + "class": "ALPHA360_Denoise", + "module_path": "qlib.contrib.data.handler", + "kwargs": DATA_HANDLER_CONFIG, + }, + "segments": { + "train": ("2008-01-01", "2014-12-31"), + "valid": ("2015-01-01", "2016-12-31"), + "test": ("2017-01-01", "2020-08-01"), + }, + }, + } + # You shoud record the data in specific sequence + # "record": ['SignalRecord', 'SigAnaRecord', 'PortAnaRecord'], + } + + # model = train_model(task) + model = init_instance_by_config(task["model"]) + dataset = init_instance_by_config(task["dataset"]) + model.fit(dataset) + + pred_score = model.predict(dataset) + + # save pred_score to file + pred_score_path = Path("~/tmp/qlib/pred_score.pkl").expanduser() + pred_score_path.parent.mkdir(exist_ok=True, parents=True) + pred_score.to_pickle(pred_score_path) + + ################################### + # backtest + ################################### + STRATEGY_CONFIG = { + "topk": 50, + "n_drop": 5, + } + BACKTEST_CONFIG = { + "verbose": False, + "limit_threshold": 0.095, + "account": 100000000, + "benchmark": BENCHMARK, + "deal_price": "close", + "open_cost": 0.0005, + "close_cost": 0.0015, + "min_cost": 5, + } + + # use default strategy + # custom Strategy, refer to: TODO: Strategy API url + strategy = TopkDropoutStrategy(**STRATEGY_CONFIG) + report_normal, positions_normal = normal_backtest(pred_score, strategy=strategy, **BACKTEST_CONFIG) + + ################################### + # analyze + # If need a more detailed analysis, refer to: examples/train_and_bakctest.ipynb + ################################### + 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 + print(analysis_df) diff --git a/examples/workflow_by_code_gru.py b/examples/workflow_by_code_gru.py new file mode 100644 index 000000000..e55f0ae45 --- /dev/null +++ b/examples/workflow_by_code_gru.py @@ -0,0 +1,144 @@ +# 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.pytorch_gru import GRU +from qlib.contrib.data.handler import ALPHA360_Denoise +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 + +# from qlib.model.learner import train_model +from qlib.utils import init_instance_by_config + +import pickle + +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_cn(target_dir=provider_uri) + + qlib.init(provider_uri=provider_uri, region=REG_CN) + + MARKET = "csi300" + BENCHMARK = "SH000300" + + ################################### + # train model + ################################### + DATA_HANDLER_CONFIG = { + "start_time": "2008-01-01", + "end_time": "2020-08-01", + "fit_start_time": "2008-01-01", + "fit_end_time": "2014-12-31", + "instruments": MARKET, + } + + TRAINER_CONFIG = { + "train_start_time": "2008-01-01", + "train_end_time": "2014-12-31", + "validate_start_time": "2015-01-01", + "validate_end_time": "2016-12-31", + "test_start_time": "2017-01-01", + "test_end_time": "2020-08-01", + } + + task = { + "model": { + "class": "GRU", + "module_path": "qlib.contrib.model.pytorch_gru", + "kwargs": { + "d_feat": 6, + "hidden_size": 64, + "num_layers": 2, + "dropout": 0.0, + "n_epochs": 200, + "lr": 1e-3, + "early_stop": 20, + "batch_size": 800, + "metric": "IC", + "loss": "mse", + "seed": 0, + "GPU": 0, + }, + }, + "dataset": { + "class": "DatasetH", + "module_path": "qlib.data.dataset", + "kwargs": { + "handler": { + "class": "ALPHA360_Denoise", + "module_path": "qlib.contrib.data.handler", + "kwargs": DATA_HANDLER_CONFIG, + }, + "segments": { + "train": ("2008-01-01", "2014-12-31"), + "valid": ("2015-01-01", "2016-12-31"), + "test": ("2017-01-01", "2020-08-01"), + }, + }, + } + # You shoud record the data in specific sequence + # "record": ['SignalRecord', 'SigAnaRecord', 'PortAnaRecord'], + } + + # model = train_model(task) + model = init_instance_by_config(task["model"]) + dataset = init_instance_by_config(task["dataset"]) + model.fit(dataset) + + pred_score = model.predict(dataset) + + # save pred_score to file + pred_score_path = Path("~/tmp/qlib/pred_score.pkl").expanduser() + pred_score_path.parent.mkdir(exist_ok=True, parents=True) + pred_score.to_pickle(pred_score_path) + + ################################### + # backtest + ################################### + STRATEGY_CONFIG = { + "topk": 50, + "n_drop": 5, + } + BACKTEST_CONFIG = { + "verbose": False, + "limit_threshold": 0.095, + "account": 100000000, + "benchmark": BENCHMARK, + "deal_price": "close", + "open_cost": 0.0005, + "close_cost": 0.0015, + "min_cost": 5, + } + + # use default strategy + # custom Strategy, refer to: TODO: Strategy API url + strategy = TopkDropoutStrategy(**STRATEGY_CONFIG) + report_normal, positions_normal = normal_backtest(pred_score, strategy=strategy, **BACKTEST_CONFIG) + + ################################### + # analyze + # If need a more detailed analysis, refer to: examples/train_and_bakctest.ipynb + ################################### + 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 + print(analysis_df) diff --git a/examples/workflow_by_code_lstm.py b/examples/workflow_by_code_lstm.py new file mode 100644 index 000000000..1815d2fec --- /dev/null +++ b/examples/workflow_by_code_lstm.py @@ -0,0 +1,144 @@ +# 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.pytorch_lstm import LSTM +from qlib.contrib.data.handler import ALPHA360_Denoise +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 + +# from qlib.model.learner import train_model +from qlib.utils import init_instance_by_config + +import pickle + +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_cn(target_dir=provider_uri) + + qlib.init(provider_uri=provider_uri, region=REG_CN) + + MARKET = "csi300" + BENCHMARK = "SH000300" + + ################################### + # train model + ################################### + DATA_HANDLER_CONFIG = { + "start_time": "2008-01-01", + "end_time": "2020-08-01", + "fit_start_time": "2008-01-01", + "fit_end_time": "2014-12-31", + "instruments": MARKET, + } + + TRAINER_CONFIG = { + "train_start_time": "2008-01-01", + "train_end_time": "2014-12-31", + "validate_start_time": "2015-01-01", + "validate_end_time": "2016-12-31", + "test_start_time": "2017-01-01", + "test_end_time": "2020-08-01", + } + + task = { + "model": { + "class": "LSTM", + "module_path": "qlib.contrib.model.pytorch_lstm", + "kwargs": { + "d_feat": 6, + "hidden_size": 64, + "num_layers": 2, + "dropout": 0.0, + "n_epochs": 200, + "lr": 1e-3, + "early_stop": 20, + "batch_size": 800, + "metric": "IC", + "loss": "mse", + "seed": 0, + "GPU": 0, + }, + }, + "dataset": { + "class": "DatasetH", + "module_path": "qlib.data.dataset", + "kwargs": { + "handler": { + "class": "ALPHA360_Denoise", + "module_path": "qlib.contrib.data.handler", + "kwargs": DATA_HANDLER_CONFIG, + }, + "segments": { + "train": ("2008-01-01", "2014-12-31"), + "valid": ("2015-01-01", "2016-12-31"), + "test": ("2017-01-01", "2020-08-01"), + }, + }, + } + # You shoud record the data in specific sequence + # "record": ['SignalRecord', 'SigAnaRecord', 'PortAnaRecord'], + } + + # model = train_model(task) + model = init_instance_by_config(task["model"]) + dataset = init_instance_by_config(task["dataset"]) + model.fit(dataset) + + pred_score = model.predict(dataset) + + # save pred_score to file + pred_score_path = Path("~/tmp/qlib/pred_score.pkl").expanduser() + pred_score_path.parent.mkdir(exist_ok=True, parents=True) + pred_score.to_pickle(pred_score_path) + + ################################### + # backtest + ################################### + STRATEGY_CONFIG = { + "topk": 50, + "n_drop": 5, + } + BACKTEST_CONFIG = { + "verbose": False, + "limit_threshold": 0.095, + "account": 100000000, + "benchmark": BENCHMARK, + "deal_price": "close", + "open_cost": 0.0005, + "close_cost": 0.0015, + "min_cost": 5, + } + + # use default strategy + # custom Strategy, refer to: TODO: Strategy API url + strategy = TopkDropoutStrategy(**STRATEGY_CONFIG) + report_normal, positions_normal = normal_backtest(pred_score, strategy=strategy, **BACKTEST_CONFIG) + + ################################### + # analyze + # If need a more detailed analysis, refer to: examples/train_and_bakctest.ipynb + ################################### + 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 + print(analysis_df) From 5aa48524d66436766f65694e55912d976f0df7bf Mon Sep 17 00:00:00 2001 From: Jactus Date: Fri, 20 Nov 2020 16:09:40 +0800 Subject: [PATCH 066/241] Add run_all_model script --- .../CatBoost/workflow_config_catboost.yaml | 1 + .../benchmarks/DNN/workflow_config_dnn.yaml | 15 +- .../benchmarks/GATs/worflow_config_gats.yaml | 1 + .../benchmarks/GBDT/workflow_config_gbdt.yaml | 1 + .../benchmarks/GRU/workflow_config_gru.yaml | 1 + .../benchmarks/LSTM/workflow_config_lstm.yaml | 1 + .../XGBoost/workflow_config_xgboost.yaml | 1 + examples/run_all_model.py | 267 ++++++++++++++++++ examples/workflow_by_code.ipynb | 12 +- examples/workflow_config.yaml | 59 ---- qlib/data/dataset/handler.py | 4 +- qlib/workflow/cli.py | 4 +- qlib/workflow/recorder.py | 44 ++- 13 files changed, 340 insertions(+), 71 deletions(-) create mode 100644 examples/run_all_model.py delete mode 100644 examples/workflow_config.yaml diff --git a/examples/benchmarks/CatBoost/workflow_config_catboost.yaml b/examples/benchmarks/CatBoost/workflow_config_catboost.yaml index 187cd116b..d66418544 100644 --- a/examples/benchmarks/CatBoost/workflow_config_catboost.yaml +++ b/examples/benchmarks/CatBoost/workflow_config_catboost.yaml @@ -1,4 +1,5 @@ provider_uri: "~/.qlib/qlib_data/cn_data" +region: cn market: &market csi300 benchmark: &benchmark SH000300 data_handler_config: &data_handler_config diff --git a/examples/benchmarks/DNN/workflow_config_dnn.yaml b/examples/benchmarks/DNN/workflow_config_dnn.yaml index 0f50cbb25..8f785aa76 100644 --- a/examples/benchmarks/DNN/workflow_config_dnn.yaml +++ b/examples/benchmarks/DNN/workflow_config_dnn.yaml @@ -1,4 +1,5 @@ provider_uri: "~/.qlib/qlib_data/cn_data" +region: cn market: &market csi300 benchmark: &benchmark SH000300 data_handler_config: &data_handler_config @@ -28,18 +29,16 @@ task: class: DNNModelPytorch module_path: qlib.contrib.model.pytorch_nn kwargs: + loss: mse input_dim: 360 output_dim: 1 - layers: [256, 512, 1024, 512, 256, 128, 64] - lr: 0.001 - max_steps: 300 - batch_size: 2000 - early_stop_rounds: 50 - eval_steps: 20 + lr: 0.002 lr_decay: 0.96 lr_decay_steps: 100 - optimizer: gd - loss: mse + optimizer: adam + max_steps: 8000 + batch_size: 4096 + GPU: 0 dataset: class: DatasetH module_path: qlib.data.dataset diff --git a/examples/benchmarks/GATs/worflow_config_gats.yaml b/examples/benchmarks/GATs/worflow_config_gats.yaml index 6c8db2e77..382c14f01 100644 --- a/examples/benchmarks/GATs/worflow_config_gats.yaml +++ b/examples/benchmarks/GATs/worflow_config_gats.yaml @@ -1,4 +1,5 @@ provider_uri: "~/.qlib/qlib_data/cn_data" +region: cn market: &market csi300 benchmark: &benchmark SH000300 data_handler_config: &data_handler_config diff --git a/examples/benchmarks/GBDT/workflow_config_gbdt.yaml b/examples/benchmarks/GBDT/workflow_config_gbdt.yaml index 212558044..eeb9db7bd 100644 --- a/examples/benchmarks/GBDT/workflow_config_gbdt.yaml +++ b/examples/benchmarks/GBDT/workflow_config_gbdt.yaml @@ -1,4 +1,5 @@ provider_uri: "~/.qlib/qlib_data/cn_data" +region: cn market: &market csi300 benchmark: &benchmark SH000300 data_handler_config: &data_handler_config diff --git a/examples/benchmarks/GRU/workflow_config_gru.yaml b/examples/benchmarks/GRU/workflow_config_gru.yaml index 49b6159dc..4e9ebc670 100644 --- a/examples/benchmarks/GRU/workflow_config_gru.yaml +++ b/examples/benchmarks/GRU/workflow_config_gru.yaml @@ -1,4 +1,5 @@ provider_uri: "~/.qlib/qlib_data/cn_data" +region: cn market: &market csi300 benchmark: &benchmark SH000300 data_handler_config: &data_handler_config diff --git a/examples/benchmarks/LSTM/workflow_config_lstm.yaml b/examples/benchmarks/LSTM/workflow_config_lstm.yaml index 1e3b309d2..b45f94569 100644 --- a/examples/benchmarks/LSTM/workflow_config_lstm.yaml +++ b/examples/benchmarks/LSTM/workflow_config_lstm.yaml @@ -1,4 +1,5 @@ provider_uri: "~/.qlib/qlib_data/cn_data" +region: cn market: &market csi300 benchmark: &benchmark SH000300 data_handler_config: &data_handler_config diff --git a/examples/benchmarks/XGBoost/workflow_config_xgboost.yaml b/examples/benchmarks/XGBoost/workflow_config_xgboost.yaml index 497ffa5b6..fb88f1058 100644 --- a/examples/benchmarks/XGBoost/workflow_config_xgboost.yaml +++ b/examples/benchmarks/XGBoost/workflow_config_xgboost.yaml @@ -1,4 +1,5 @@ provider_uri: "~/.qlib/qlib_data/cn_data" +region: cn market: &market csi300 benchmark: &benchmark SH000300 data_handler_config: &data_handler_config diff --git a/examples/run_all_model.py b/examples/run_all_model.py new file mode 100644 index 000000000..6efde6a52 --- /dev/null +++ b/examples/run_all_model.py @@ -0,0 +1,267 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +import os +import sys +import venv +import glob +import shutil +import tempfile +from pathlib import Path +from subprocess import Popen, PIPE +from threading import Thread +from pprint import pprint +from urllib.parse import urlparse +from urllib.request import urlretrieve + +import qlib +from qlib.config import REG_CN +from qlib.workflow import R +from qlib.workflow.cli import workflow + +# init qlib +provider_uri = "~/.qlib/qlib_data/cn_data" +qlib.init(provider_uri=provider_uri, region=REG_CN) + + +class ExtendedEnvBuilder(venv.EnvBuilder): + """ + Thie class is modified based on https://docs.python.org/3/library/venv.html. + This builder installs setuptools and pip so that you can pip or + easy_install other packages into the created virtual environment. + + :param nodist: If true, setuptools and pip are not installed into the + created virtual environment. + :param nopip: If true, pip is not installed into the created + virtual environment. + :param progress: If setuptools or pip are installed, the progress of the + installation can be monitored by passing a progress + callable. If specified, it is called with two + arguments: a string indicating some progress, and a + context indicating where the string is coming from. + The context argument can have one of three values: + 'main', indicating that it is called from virtualize() + itself, and 'stdout' and 'stderr', which are obtained + by reading lines from the output streams of a subprocess + which is used to install the app. + + If a callable is not specified, default progress + information is output to sys.stderr. + """ + + def __init__(self, *args, **kwargs): + self.nodist = kwargs.pop("nodist", False) + self.nopip = kwargs.pop("nopip", False) + self.progress = kwargs.pop("progress", None) + self.verbose = kwargs.pop("verbose", False) + super().__init__(*args, **kwargs) + + def post_setup(self, context): + """ + Set up any packages which need to be pre-installed into the + virtual environment being created. + + :param context: The information for the virtual environment + creation request being processed. + """ + os.environ["VIRTUAL_ENV"] = context.env_dir + if not self.nodist: + self.install_setuptools(context) + # Can't install pip without setuptools + if not self.nopip and not self.nodist: + self.install_pip(context) + + def reader(self, stream, context): + """ + Read lines from a subprocess' output stream and either pass to a progress + callable (if specified) or write progress information to sys.stderr. + """ + progress = self.progress + while True: + s = stream.readline() + if not s: + break + if progress is not None: + progress(s, context) + else: + if not self.verbose: + sys.stderr.write(".") + else: + sys.stderr.write(s.decode("utf-8")) + sys.stderr.flush() + stream.close() + + def install_script(self, context, name, url): + _, _, path, _, _, _ = urlparse(url) + fn = os.path.split(path)[-1] + binpath = context.bin_path + distpath = os.path.join(binpath, fn) + # Download script into the virtual environment's binaries folder + urlretrieve(url, distpath) + progress = self.progress + if self.verbose: + term = "\n" + else: + term = "" + if progress is not None: + progress("Installing %s ...%s" % (name, term), "main") + else: + sys.stderr.write("Installing %s ...%s" % (name, term)) + sys.stderr.flush() + # Install in the virtual environment + args = [context.env_exe, fn] + p = Popen(args, stdout=PIPE, stderr=PIPE, cwd=binpath) + t1 = Thread(target=self.reader, args=(p.stdout, "stdout")) + t1.start() + t2 = Thread(target=self.reader, args=(p.stderr, "stderr")) + t2.start() + p.wait() + t1.join() + t2.join() + if progress is not None: + progress("done.", "main") + else: + sys.stderr.write("done.\n") + # Clean up - no longer needed + os.unlink(distpath) + + def install_setuptools(self, context): + """ + Install setuptools in the virtual environment. + + :param context: The information for the virtual environment + creation request being processed. + """ + url = "https://bootstrap.pypa.io/ez_setup.py" + self.install_script(context, "setuptools", url) + # clear up the setuptools archive which gets downloaded + pred = lambda o: o.startswith("setuptools-") and o.endswith(".tar.gz") + files = filter(pred, os.listdir(context.bin_path)) + for f in files: + f = os.path.join(context.bin_path, f) + os.unlink(f) + + def install_pip(self, context): + """ + Install pip in the virtual environment. + + :param context: The information for the virtual environment + creation request being processed. + """ + url = "https://bootstrap.pypa.io/get-pip.py" + self.install_script(context, "pip", url) + + +# function to get all the folders benchmark folder +def get_all_folders() -> dict: + folders = dict() + for f in os.scandir("benchmarks"): + path = Path("benchmarks") / f.name + if f.name != "TFT": + folders[f.name] = str(path.resolve()) + return folders + + +# function to get all the files under the model folder +def get_all_files(folder_path) -> (str, str): + yaml_path = str(Path(f"{folder_path}") / "*.yaml") + req_path = str(Path(f"{folder_path}") / "*.txt") + return glob.glob(yaml_path)[0], glob.glob(req_path)[0] + + +# function to retrieve all the results +def get_all_results(folders) -> dict: + results = dict() + for fn in folders: + exp = R.get_exp(experiment_name=fn, create=False) + recorders = exp.list_recorders() + recorder = R.get_recorder(recorder_id=next(iter(recorders)), experiment_name=fn) + metrics = recorder.list_metrics() + results[fn] = {key: metrics[key] for key in metrics if "with_cost" in key} + return results + + +# function to generate and save markdown tables +def gen_and_save_md_table(results): + table = "| Model Name | Annualized Return | Information Ratio | Max Drawdown |\n" + table += "|---|---|---|---|\n" + for fn in results: + ar = metrics[fn]["excess_return_with_cost.annualized_return"] + ir = metrics[fn]["excess_return_with_cost.information_ratio"] + md = metrics[fn]["excess_return_with_cost.max_drawdown"] + table += f"| {fn} | {ar:9.5f} | {ir:9.5f} | {md:9.5f} |\n" + pprint(table) + with open("table.md", "w") as f: + f.write(table) + return table + + +# function to run the all the models +def run(): + # get all folders + folders = get_all_folders() + # set up + compatible = True + if sys.version_info < (3, 3): + compatible = False + elif not hasattr(sys, "base_prefix"): + compatible = False + if not compatible: + raise ValueError("This script is only for use with " "Python 3.3 or later") + if os.name == "nt": + use_symlinks = False + else: + use_symlinks = True + builder = ExtendedEnvBuilder( + system_site_packages=False, + clear=False, + symlinks=use_symlinks, + upgrade=False, + nodist=False, + nopip=False, + verbose=False, + ) + for fn in folders: + # create env + temp_dir = tempfile.mkdtemp() + env_path = Path(temp_dir).absolute() + sys.stderr.write(f"Creating Virtual Environment with path: {env_path}...\n") + builder.create(str(env_path)) + python_path = env_path / "bin" / "python" # TODO: FIX ME! + sys.stderr.write("\n") + # get all files + sys.stderr.write("Retrieving files...\n") + yaml_path, req_path = get_all_files(folders[fn]) + sys.stderr.write("\n") + # install requirements.txt + sys.stderr.write("Installing requirements.txt...\n") + os.system(f"{python_path} -m pip install -r {req_path}") + sys.stderr.write("\n") + # install qlib + sys.stderr.write("Installing qlib...\n") + os.system(f"{python_path} -m pip install --upgrade cython") # TODO: FIX ME! + os.system(f"{python_path} -m pip install -e git+https://github.com/you-n-g/qlib#egg=pyqlib") # TODO: FIX ME! + sys.stderr.write("\n") + # run workflow_by_config + sys.stderr.write(f"Running the model: {fn}...\n") + os.system(f"{python_path} {env_path / 'src/pyqlib/qlib/workflow/cli.py'} {yaml_path} {fn}") + sys.stderr.write("\n") + # remove env + sys.stderr.write(f"Deleting the environment: {env_path}...\n") + shutil.rmtree(env_path) + # getting all results + sys.stderr.write(f"Retrieving results...\n") + results = get_all_results(folders) + # generating md table + sys.stderr.write(f"Generating markdown table...\n") + gen_and_save_md_table(results) + + +if __name__ == "__main__": + rc = 1 + try: + run() # run all the model + rc = 0 + except Exception as e: + print("Error: %s" % e, file=sys.stderr) + sys.exit(rc) diff --git a/examples/workflow_by_code.ipynb b/examples/workflow_by_code.ipynb index f07c4f19e..246d2b0c2 100644 --- a/examples/workflow_by_code.ipynb +++ b/examples/workflow_by_code.ipynb @@ -1,5 +1,15 @@ { "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Copyright (c) Microsoft Corporation.\n", + "# Licensed under the MIT License." + ] + }, { "cell_type": "code", "execution_count": null, @@ -13,7 +23,7 @@ "import pandas as pd\n", "from qlib.config import REG_CN\n", "from qlib.contrib.model.gbdt import LGBModel\n", - "from qlib.contrib.estimator.handler import Alpha158\n", + "from qlib.contrib.data.handler import Alpha158\n", "from qlib.contrib.strategy.strategy import TopkDropoutStrategy\n", "from qlib.contrib.evaluate import (\n", " backtest as normal_backtest,\n", diff --git a/examples/workflow_config.yaml b/examples/workflow_config.yaml deleted file mode 100644 index 212558044..000000000 --- a/examples/workflow_config.yaml +++ /dev/null @@ -1,59 +0,0 @@ -provider_uri: "~/.qlib/qlib_data/cn_data" -market: &market csi300 -benchmark: &benchmark SH000300 -data_handler_config: &data_handler_config - start_time: 2008-01-01 - end_time: 2020-08-01 - fit_start_time: 2008-01-01 - fit_end_time: 2014-12-31 - instruments: *market -port_analysis_config: &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 -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: [2008-01-01, 2014-12-31] - valid: [2015-01-01, 2016-12-31] - test: [2017-01-01, 2020-08-01] - record: - - class: SignalRecord - module_path: qlib.workflow.record_temp - kwargs: {} - - class: PortAnaRecord - module_path: qlib.workflow.record_temp - kwargs: - config: *port_analysis_config \ No newline at end of file diff --git a/qlib/data/dataset/handler.py b/qlib/data/dataset/handler.py index 422cc6b1d..7db6b90f2 100644 --- a/qlib/data/dataset/handler.py +++ b/qlib/data/dataset/handler.py @@ -290,7 +290,9 @@ class DataHandlerLP(DataHandler): init_instance_by_config( proc, None if (isinstance(proc, dict) and "module_path" in proc) else processor_module, - accept_types=processor_module.Processor)) + accept_types=processor_module.Processor, + ) + ) self.process_type = process_type super().__init__(instruments, start_time, end_time, data_loader, **kwargs) diff --git a/qlib/workflow/cli.py b/qlib/workflow/cli.py index b257d9237..307a466a1 100644 --- a/qlib/workflow/cli.py +++ b/qlib/workflow/cli.py @@ -13,13 +13,15 @@ from qlib.utils import init_instance_by_config, flatten_dict from qlib.workflow import R from qlib.workflow.record_temp import SignalRecord + # worflow handler function def workflow(config_path, experiment_name="workflow"): with open(config_path) as fp: config = yaml.load(fp, Loader=yaml.Loader) provider_uri = config.get("provider_uri") - qlib.init(provider_uri=provider_uri, region=REG_CN) + region = config.get("region") + qlib.init(provider_uri=provider_uri, region=region) # model initiaiton model = init_instance_by_config(config.get("task")["model"]) diff --git a/qlib/workflow/recorder.py b/qlib/workflow/recorder.py index 71f13381f..97f97105f 100644 --- a/qlib/workflow/recorder.py +++ b/qlib/workflow/recorder.py @@ -159,6 +159,36 @@ class Recorder: """ raise NotImplementedError(f"Please implement the `list_artifacts` method.") + def list_metrics(self): + """ + List all the metrics of a recorder. + + Returns + ------- + A dictionary of metrics that being stored. + """ + raise NotImplementedError(f"Please implement the `list_metrics` method.") + + def list_params(self): + """ + List all the params of a recorder. + + Returns + ------- + A dictionary of params that being stored. + """ + raise NotImplementedError(f"Please implement the `list_params` method.") + + def list_tags(self): + """ + List all the tags of a recorder. + + Returns + ------- + A dictionary of tags that being stored. + """ + raise NotImplementedError(f"Please implement the `list_tags` method.") + class MLflowRecorder(Recorder): """ @@ -239,7 +269,7 @@ class MLflowRecorder(Recorder): def log_metrics(self, step=None, **kwargs): for name, data in kwargs.items(): - self.client.log_metric(self.id, name, data) + self.client.log_metric(self.id, name, data, step=step) def set_tags(self, **kwargs): for name, data in kwargs.items(): @@ -261,3 +291,15 @@ class MLflowRecorder(Recorder): assert self._uri is not None, "Please start the experiment and recorder first before using recorder directly." artifacts = self.client.list_artifacts(self.id, artifact_path) return artifacts + + def list_metrics(self): + run = self.client.get_run(self.id) + return run.data.metrics + + def list_params(self): + run = self.client.get_run(self.id) + return run.data.params + + def list_tags(self): + run = self.client.get_run(self.id) + return run.data.tags From c22bd73f67ec445bc4f542ee9004667663c38f0e Mon Sep 17 00:00:00 2001 From: Jactus Date: Fri, 20 Nov 2020 17:54:56 +0800 Subject: [PATCH 067/241] Update CI --- .github/workflows/test.yml | 9 +- examples/workflow_by_code_gats.py | 2 +- qlib/utils/__init__.py | 2 +- tests/test_all_pipeline.py | 183 +++++++++++++++++------------- 4 files changed, 109 insertions(+), 87 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e196c124b..d1e01e46b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -50,9 +50,10 @@ jobs: cd tests pytest . --durations=0 - - name: Test data downloads and examples + - name: Test data downloads run: | python scripts/get_data.py qlib_data --target_dir ~/.qlib/qlib_data/cn_data --interval 1d --region cn - # cd examples - # estimator -c estimator/estimator_config.yaml - # jupyter nbconvert --execute estimator/analyze_from_estimator.ipynb --to html \ No newline at end of file + + - name: Test workflow by config + run: | + workflow_by_config examples/benchmarks/GBDT/workflow_config_gbdt.yaml \ No newline at end of file diff --git a/examples/workflow_by_code_gats.py b/examples/workflow_by_code_gats.py index 06845d448..222eb126f 100644 --- a/examples/workflow_by_code_gats.py +++ b/examples/workflow_by_code_gats.py @@ -72,7 +72,7 @@ if __name__ == "__main__": "batch_size": 800, "metric": "IC", "loss": "mse", - "base_model":"GRU", + "base_model": "GRU", "seed": 0, "GPU": 0, }, diff --git a/qlib/utils/__init__.py b/qlib/utils/__init__.py index 575ed24aa..f32cceba3 100644 --- a/qlib/utils/__init__.py +++ b/qlib/utils/__init__.py @@ -659,7 +659,7 @@ def flatten_dict(d, parent_key="", sep="."): items = [] for k, v in d.items(): new_key = parent_key + sep + k if parent_key else k - if isinstance(v, collections.MutableMapping): + if isinstance(v, collections.abc.MutableMapping): items.extend(flatten_dict(v, new_key, sep=sep).items()) else: items.append((new_key, v)) diff --git a/tests/test_all_pipeline.py b/tests/test_all_pipeline.py index 04c399342..16242189a 100644 --- a/tests/test_all_pipeline.py +++ b/tests/test_all_pipeline.py @@ -2,6 +2,7 @@ # Licensed under the MIT License. import sys +import shutil import unittest from pathlib import Path @@ -10,7 +11,7 @@ import pandas as pd from scipy.stats import pearsonr import qlib -from qlib.config import REG_CN +from qlib.config import REG_CN, C from qlib.utils import drop_nan_by_y_index from qlib.contrib.model.gbdt import LGBModel from qlib.contrib.data.handler import Alpha158 @@ -19,51 +20,78 @@ from qlib.contrib.evaluate import ( backtest as normal_backtest, risk_analysis, ) -from qlib.utils import exists_qlib_data +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 -DATA_HANDLER_CONFIG = { - "dropna_label": True, - "start_date": "2008-01-01", - "end_date": "2020-08-01", - "market": "CSI300", +market = "csi300" +benchmark = "SH000300" + +################################### +# train model +################################### +data_handler_config = { + "start_time": "2008-01-01", + "end_time": "2020-08-01", + "fit_start_time": "2008-01-01", + "fit_end_time": "2014-12-31", + "instruments": market, } -MODEL_CONFIG = { - "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, +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": ("2008-01-01", "2014-12-31"), + "valid": ("2015-01-01", "2016-12-31"), + "test": ("2017-01-01", "2020-08-01"), + }, + }, + }, } -TRAINER_CONFIG = { - "train_start_date": "2008-01-01", - "train_end_date": "2014-12-31", - "validate_start_date": "2015-01-01", - "validate_end_date": "2016-12-31", - "test_start_date": "2017-01-01", - "test_end_date": "2020-08-01", -} - -STRATEGY_CONFIG = { - "topk": 50, - "n_drop": 5, -} - -BACKTEST_CONFIG = { - "verbose": False, - "limit_threshold": 0.095, - "account": 100000000, - "benchmark": "SH000300", - "deal_price": "close", - "open_cost": 0.0005, - "close_cost": 0.0015, - "min_cost": 5, +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, + }, } @@ -78,34 +106,32 @@ def train(): performance: dict model performance """ - # get data - x_train, y_train, x_validate, y_validate, x_test, y_test = Alpha158(**DATA_HANDLER_CONFIG).get_split_data( - **TRAINER_CONFIG - ) - # train - model = LGBModel(**MODEL_CONFIG) - model.fit(x_train, y_train, x_validate, y_validate) - _pred = model.predict(x_test) - _pred = pd.DataFrame(_pred, index=x_test.index, columns=y_test.columns) - pred_score = pd.DataFrame(index=_pred.index) - pred_score["score"] = _pred.iloc(axis=1)[0] + # model initiaiton + model = init_instance_by_config(task["model"]) + dataset = init_instance_by_config(task["dataset"]) - # get performance - try: - model_score = model.score(x_test, y_test) - except NotImplementedError: - model_score = None - # Remove rows from x, y and w, which contain Nan in any columns in y_test. - x_test, y_test, __ = drop_nan_by_y_index(x_test, y_test) - pred_test = model.predict(x_test) - model_pearsonr = pearsonr(np.ravel(pred_test), np.ravel(y_test.values))[0] + # start exp + with R.start(experiment_name="workflow"): + R.log_params(**flatten_dict(task)) + model.fit(dataset) - return pred_score, {"model_score": model_score, "model_pearsonr": model_pearsonr} + # prediction + recorder = R.get_recorder() + rid = recorder.id + sr = SignalRecord(model, dataset, recorder) + sr.generate() + pred_score = sr.load() + + y_test = dataset.prepare("test", col_set="label") + pred_score, y_test, __ = drop_nan_by_y_index(pred_score, y_test) + model_pearsonr = pearsonr(np.ravel(pred_score.values), np.ravel(y_test.values))[0] + + return pred_score, {"model_pearsonr": model_pearsonr}, rid -def backtest(pred): - """backtest +def backtest_analysis(pred, rid): + """backtest and analysis Parameters ---------- @@ -114,23 +140,14 @@ def backtest(pred): Returns ------- - report_normal: pandas.DataFrame - - positions_normal: dict + analysis result : pandas.DataFrame """ - strategy = TopkDropoutStrategy(**STRATEGY_CONFIG) - _report_normal, _positions_normal = normal_backtest(pred, strategy=strategy, **BACKTEST_CONFIG) - return _report_normal, _positions_normal - - -def analyze(report_normal): - _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 + recorder = R.get_recorder(experiment_name="workflow", recorder_id=rid) + # backtest + par = PortAnaRecord(recorder, port_analysis_config) + par.generate() + analysis_df = par.load("port_analysis.pkl") print(analysis_df) return analysis_df @@ -139,6 +156,7 @@ class TestAllFlow(unittest.TestCase): PRED_SCORE = None REPORT_NORMAL = None POSITIONS = None + RID = None @classmethod def setUpClass(cls) -> None: @@ -154,13 +172,16 @@ class TestAllFlow(unittest.TestCase): ) qlib.init(provider_uri=provider_uri, region=REG_CN) + @classmethod + def tearDownClass(cls) -> None: + shutil.rmtree(str(Path(C["exp_manager"]["kwargs"]["uri"].strip("file:")).resolve())) + def test_0_train(self): - TestAllFlow.PRED_SCORE, model_pearsonr = train() + TestAllFlow.PRED_SCORE, model_pearsonr, TestAllFlow.RID = train() self.assertGreaterEqual(model_pearsonr["model_pearsonr"], 0, "train failed") def test_1_backtest(self): - TestAllFlow.REPORT_NORMAL, TestAllFlow.POSITIONS = backtest(TestAllFlow.PRED_SCORE) - analyze_df = analyze(TestAllFlow.REPORT_NORMAL) + analyze_df = backtest_analysis(TestAllFlow.PRED_SCORE, TestAllFlow.RID) self.assertGreaterEqual( analyze_df.loc(axis=0)["excess_return_with_cost", "annualized_return"].values[0], 0.10, From f476ada22d9ea7a050ee5e01465da3bcc6561d7e Mon Sep 17 00:00:00 2001 From: Young Date: Sat, 21 Nov 2020 08:54:11 +0000 Subject: [PATCH 068/241] Adjust interface --- qlib/data/dataset/__init__.py | 2 +- qlib/data/dataset/handler.py | 10 ++-- qlib/utils/serial.py | 42 +++++++++++++++- qlib/workflow/expm.py | 92 ++++++++++++++++++++++------------- 4 files changed, 106 insertions(+), 40 deletions(-) diff --git a/qlib/data/dataset/__init__.py b/qlib/data/dataset/__init__.py index d5b8a12e9..e7a149c65 100644 --- a/qlib/data/dataset/__init__.py +++ b/qlib/data/dataset/__init__.py @@ -96,7 +96,7 @@ class DatasetH(Dataset): } """ self._handler = init_instance_by_config(handler, accept_types=DataHandler) - self._segments = segments + self._segments = segments.copy() def prepare( self, segments: Union[List[str], Tuple[str], str, slice], col_set=DataHandler.CS_ALL, **kwargs diff --git a/qlib/data/dataset/handler.py b/qlib/data/dataset/handler.py index 422cc6b1d..b3608464d 100644 --- a/qlib/data/dataset/handler.py +++ b/qlib/data/dataset/handler.py @@ -156,8 +156,9 @@ class DataHandler(Serializable): ------- pd.DataFrame: """ - df = fetch_df_by_index(self._data, selector, level, fetch_orig=self.fetch_orig) - df = self._fetch_df_by_col(df, col_set) + # Fetch column first will be more friendly to SepDataFrame + df = self._fetch_df_by_col(self._data, col_set) + df = fetch_df_by_index(df, selector, level, fetch_orig=self.fetch_orig) if squeeze: # squeeze columns df = df.squeeze() @@ -417,8 +418,9 @@ class DataHandlerLP(DataHandler): pd.DataFrame: """ df = self._get_df_by_key(data_key) - df = fetch_df_by_index(df, selector, level, fetch_orig=self.fetch_orig) - return self._fetch_df_by_col(df, col_set) + # Fetch column first will be more friendly to SepDataFrame + df = self._fetch_df_by_col(df, col_set) + return fetch_df_by_index(df, selector, level, fetch_orig=self.fetch_orig) def get_cols(self, col_set=DataHandler.CS_ALL, data_key: str = DK_I) -> list: """ diff --git a/qlib/utils/serial.py b/qlib/utils/serial.py index 9bc8ce94a..b5734d726 100644 --- a/qlib/utils/serial.py +++ b/qlib/utils/serial.py @@ -10,13 +10,51 @@ class Serializable: Serializable behaves like pickle. But it only saves the state whose name **does not** start with `_` """ + def __init__(self): + self._dump_all = False + self._exclude = [] def __getstate__(self) -> dict: - return {k: v for k, v in self.__dict__.items() if not k.startswith("_")} + return { + k: v + for k, v in self.__dict__.items() if k not in self.exclude and (self.dump_all or not k.startswith("_")) + } def __setstate__(self, state: dict): self.__dict__.update(state) - def to_pickle(self, path: [Path, str]): + @property + def dump_all(self): + """ + will the object dump all object + + Parameters + ---------- + self : [TODO:type] + [TODO:description] + """ + return getattr(self, "_dump_all", False) + + @property + def exclude(self): + """ + What attribute will be dumped + + Parameters + ---------- + self : [TODO:type] + [TODO:description] + """ + return getattr(self, "_exclude", []) + + def config(self, dump_all: bool = None, exclude: list = None): + if dump_all is not None: + self._dump_all = dump_all + + if exclude is not None: + self._exclude = exclude + + def to_pickle(self, path: [Path, str], dump_all: bool = None, exclude: list = None): + self.config(dump_all=dump_all, exclude=exclude) with Path(path).open("wb") as f: pickle.dump(self, f) diff --git a/qlib/workflow/expm.py b/qlib/workflow/expm.py index 8fb7962e9..25c5d4661 100644 --- a/qlib/workflow/expm.py +++ b/qlib/workflow/expm.py @@ -6,7 +6,7 @@ from mlflow.exceptions import MlflowException import os from pathlib import Path from contextlib import contextmanager -from .exp import MLflowExperiment +from .exp import MLflowExperiment, Experiment from .recorder import Recorder, MLflowRecorder from ..log import get_module_logger @@ -128,7 +128,61 @@ class ExpManager: ------- An experiment object. """ - raise NotImplementedError(f"Please implement the `get_exp` method.") + # special case of getting experiment + if experiment_id is None and experiment_name is None: + if self.active_experiment is not None: + return self.active_experiment + # User don't want get active code now. + # Don't assume underlying code could handle the case of two None + if experiment_id is None and experiment_name is None: + experiment_name = self.default_exp_name + + if create: + exp, is_new = self._get_or_create_exp(experiment_id=experiment_id, experiment_name=experiment_name) + else: + exp, is_new = self._get_exp(experiment_id=experiment_id, experiment_name=experiment_name), False + if is_new: + self.active_experiment = exp + # start the recorder + self.active_experiment.start() + return exp + + def _get_or_create_exp(self, experiment_id=None, experiment_name=None) -> (object, bool): + """ + Method for getting or creating an experiment. It will try to first get a valid experiment, if exception occurs, it will + automatically create a new experiment based on the given id and name. + """ + try: + if experiment_id is None and experiment_name is None: + experiment_name = self.default_exp_name + return self._get_exp(experiment_id=experiment_id, experiment_name=experiment_name), False + except ValueError: + if experiment_name is None: + experiment_name = self.default_exp_name + logger.info(f"No valid experiment found. Create a new experiment with name {experiment_name}.") + return self.create_exp(experiment_name), True + + def _get_exp(self, experiment_id=None, experiment_name=None) -> Experiment: + """ + get specific experiment by name or id. If it does not exist, raise ValueError + + Parameters + ---------- + experiment_id : + The id of experiment + experiment_name : + The id name experiment + + Returns + ------- + Experiment: + The searched experiment + + Raises + ------ + ValueError + """ + raise NotImplementedError(f"Please implement the `_get_exp` method") def delete_exp(self, experiment_id=None, experiment_name=None): """ @@ -197,6 +251,7 @@ class MLflowExpManager(ExpManager): self.active_experiment = None def create_exp(self, experiment_name=None): + assert(experiment_name is not None) # init experiment experiment_id = self.client.create_experiment(experiment_name) experiment = MLflowExperiment(experiment_id, experiment_name, self.uri) @@ -204,34 +259,6 @@ class MLflowExpManager(ExpManager): return experiment - def get_exp(self, experiment_id=None, experiment_name=None, create=True): - # special case of getting experiment - if experiment_id is None and experiment_name is None: - if self.active_experiment is not None: - return self.active_experiment - if create: - exp, is_new = self._get_or_create_exp(experiment_id=experiment_id, experiment_name=experiment_name) - else: - exp, is_new = self._get_exp(experiment_id=experiment_id, experiment_name=experiment_name), False - if is_new: - self.active_experiment = exp - # start the recorder - self.active_experiment.start() - return exp - - def _get_or_create_exp(self, experiment_id=None, experiment_name=None) -> (object, bool): - """ - Method for getting or creating an experiment. It will try to first get a valid experiment, if exception occurs, it will - automatically create a new experiment based on the given id and name. - """ - try: - return self._get_exp(experiment_id=experiment_id, experiment_name=experiment_name), False - except ValueError: - if experiment_name is None: - experiment = self.default_exp_name - logger.info(f"No valid experiment found. Create a new experiment with name {experiment_name}.") - return self.create_exp(experiment_name), True - def _get_exp(self, experiment_id=None, experiment_name=None): """ Method for getting or creating an experiment. It will try to first get a valid experiment, if exception occurs, it will @@ -247,7 +274,7 @@ class MLflowExpManager(ExpManager): raise MlflowException("No valid experiment has been found.") experiment = MLflowExperiment(exp.experiment_id, exp.name, self.uri) return experiment - except MlflowException as e: + except MlflowException: raise ValueError( "No valid experiment has been found, please make sure the input experiment id is correct." ) @@ -293,6 +320,5 @@ class MLflowExpManager(ExpManager): experiments = dict() for exp in exps: experiment = MLflowExperiment(exp.experiment_id, exp.name, self.uri) - experiments[ename] = experiment - + experiments[exp.name] = experiment return experiments From e5923333f5cb47c5cdcc2962c08fa10424db156b Mon Sep 17 00:00:00 2001 From: Young Date: Sat, 21 Nov 2020 15:20:47 +0000 Subject: [PATCH 069/241] recorder refine; signalTemp; fixbug --- qlib/contrib/eva/__init__.py | 0 qlib/contrib/eva/alpha.py | 32 ++++++++ qlib/contrib/model/gbdt.py | 2 +- qlib/data/dataset/__init__.py | 33 +++++--- qlib/workflow/record_temp.py | 143 ++++++++++++++++++---------------- qlib/workflow/recorder.py | 2 +- 6 files changed, 133 insertions(+), 79 deletions(-) create mode 100644 qlib/contrib/eva/__init__.py create mode 100644 qlib/contrib/eva/alpha.py diff --git a/qlib/contrib/eva/__init__.py b/qlib/contrib/eva/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/qlib/contrib/eva/alpha.py b/qlib/contrib/eva/alpha.py new file mode 100644 index 000000000..1faebbe19 --- /dev/null +++ b/qlib/contrib/eva/alpha.py @@ -0,0 +1,32 @@ +''' +Here is a batch of evaluation functions. + +The interface should be redesigned carefully in the future. +''' +import pandas as pd + + +def calc_ic(pred: pd.Series, label: pd.Series, date_col='datetime', dropna=False) -> (pd.Series, pd.Series): + """calc_ic. + + Parameters + ---------- + pred : + pred + label : + label + date_col : + date_col + + Returns + ------- + (pd.Series, pd.Series) + ic and rank ic + """ + df = pd.DataFrame({'pred': pred, 'label': label}) + ic = df.groupby(date_col).apply(lambda df: df['pred'].corr(df['label'])) + ric = df.groupby(date_col).apply(lambda df: df['pred'].corr(df['label'], method='spearman')) + if dropna: + return ic.dropna(), ric.dropna() + else: + return ic, ric diff --git a/qlib/contrib/model/gbdt.py b/qlib/contrib/model/gbdt.py index 41b773756..58b76c355 100644 --- a/qlib/contrib/model/gbdt.py +++ b/qlib/contrib/model/gbdt.py @@ -64,7 +64,7 @@ class LGBModel(ModelFT): def predict(self, dataset): if self.model is None: raise ValueError("model is not fitted yet!") - x_test = dataset.prepare("test", col_set="feature") + x_test = dataset.prepare("test", col_set="feature", data_key=DataHandlerLP.DK_I) return pd.Series(self.model.predict(np.squeeze(x_test.values)), index=x_test.index) def finetune(self, dataset: DatasetH, num_boost_round=10, verbose_eval=20): diff --git a/qlib/data/dataset/__init__.py b/qlib/data/dataset/__init__.py index e7a149c65..7e41a7b7c 100644 --- a/qlib/data/dataset/__init__.py +++ b/qlib/data/dataset/__init__.py @@ -1,7 +1,9 @@ from ...utils.serial import Serializable from typing import Union, List, Tuple from ...utils import init_instance_by_config -from .handler import DataHandler +from ...log import get_module_logger +from .handler import DataHandler, DataHandlerLP +from inspect import getfullargspec import pandas as pd @@ -98,9 +100,11 @@ class DatasetH(Dataset): self._handler = init_instance_by_config(handler, accept_types=DataHandler) self._segments = segments.copy() - def prepare( - self, segments: Union[List[str], Tuple[str], str, slice], col_set=DataHandler.CS_ALL, **kwargs - ) -> Union[List[pd.DataFrame], pd.DataFrame]: + def prepare(self, + segments: Union[List[str], Tuple[str], str, slice], + col_set=DataHandler.CS_ALL, + data_key=DataHandlerLP.DK_I, + **kwargs) -> Union[List[pd.DataFrame], pd.DataFrame]: """ prepare the data for learning and inference @@ -111,22 +115,31 @@ class DatasetH(Dataset): Here are some examples 1) 'train' 2) ['train', 'valid'] - col_set : [TODO:type] - [TODO:description] + col_set : str + The col_set will be passed to self._handler when fetching data + data_key: str + The data to fetch: DK_* + Default is DK_I, which indicate fetching data for **inference** Returns ------- Union[List[pd.DataFrame], pd.DataFrame]: - [TODO:description] Raises ------ NotImplementedError: - [TODO:description] """ + logger = get_module_logger("DatasetH") + fetch_kwargs = {"col_set": col_set} + fetch_kwargs.update(kwargs) + if "data_key"in getfullargspec(self._handler.fetch).args: + fetch_kwargs['data_key'] = data_key + else: + logger.info(f"data_key[{data_key}] is ignored.") + if isinstance(segments, (list, tuple)): - return [self._handler.fetch(slice(*self._segments[seg]), col_set=col_set, **kwargs) for seg in segments] + return [self._handler.fetch(slice(*self._segments[seg]), **fetch_kwargs) for seg in segments] elif isinstance(segments, str): - return self._handler.fetch(slice(*self._segments[segments]), col_set=col_set, **kwargs) + return self._handler.fetch(slice(*self._segments[segments]), **fetch_kwargs) else: raise NotImplementedError(f"This type of input is not supported") diff --git a/qlib/workflow/record_temp.py b/qlib/workflow/record_temp.py index 7d4c79364..08daa1dae 100644 --- a/qlib/workflow/record_temp.py +++ b/qlib/workflow/record_temp.py @@ -9,9 +9,12 @@ from ..contrib.evaluate import ( backtest as normal_backtest, risk_analysis, ) +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 ..contrib.eva.alpha import calc_ic logger = get_module_logger("workflow", "INFO") @@ -22,8 +25,8 @@ class RecordTemp: backtest in a certain format. """ - def __init__(self, *args, **kwargs): - pass + def __init__(self, recorder): + self.recorder = recorder def generate(self, **kwargs): """ @@ -38,7 +41,7 @@ class RecordTemp: """ raise NotImplementedError(f"Please implement the `generate` method.") - def load(self, name, **kwargs): + def load(self, name): """ Load the stored records. @@ -46,13 +49,14 @@ class RecordTemp: ---------- name : str the name for the file to be load. - kwargs Return ------ The stored records. """ - raise NotImplementedError(f"Please implement the `load` method.") + # try to load the saved object + obj = self.recorder.load_object(name) + return obj def list(self): """ @@ -62,34 +66,36 @@ class RecordTemp: ------ A list of all the stored records. """ - raise NotImplementedError(f"Please implement the `list` method.") + return [] - def check(self, **kwargs): + def check(self, parent=False): """ Check if the records is properly generated and saved. - Parameters - ---------- - kwargs - - Return + Raise ------ - Boolean: whether the records are stored properly. + FileExistsError: whether the records are stored properly. """ - raise NotImplementedError(f"Please implement the `check` method.") + artifacts = set(self.recorder.list_artifacts()) + if parent: + # Downcasting have to be done here instead of using `super` + flist = self.__class__.__base__.list(self) + else: + flist = self.list() + for item in flist: + if item not in artifacts: + raise FileExistsError(item) -# TODO: this can only be run under R's running experiment. class SignalRecord(RecordTemp): """ This is the Signal Record class that generates the signal prediction. """ - def __init__(self, model, dataset, recorder, **kwargs): - super(SignalRecord, self).__init__() + def __init__(self, model=None, dataset=None, recorder=None, **kwargs): + super().__init__(recorder=recorder) self.model = model self.dataset = dataset - self.recorder = recorder def generate(self, **kwargs): # generate prediciton @@ -97,6 +103,7 @@ class SignalRecord(RecordTemp): if isinstance(pred, pd.Series): pred = pred.to_frame("score") self.recorder.save_objects(**{"pred.pkl": pred}) + logger.info( f"Signal record 'pred.pkl' has been saved as the artifact of the Experiment {self.recorder.experiment_id}" ) @@ -104,35 +111,50 @@ class SignalRecord(RecordTemp): pprint(f"The following are prediction results of the {type(self.model).__name__} model.") pprint(pred.head(5)) - def load(self, name="pred.pkl"): - # try to load the saved object - pred = self.recorder.load_object(name) - return pred + # save according label + if isinstance(self.dataset, DatasetH): + params = dict(self=self.dataset, segments="test", col_set="label", data_key=DataHandlerLP.DK_R) + try: + # Assume the backend handler is DataHandlerLP + raw_label = DatasetH.prepare(**params) + except TypeError: + # The argument number is not right + del params['data_key'] + # The backend handler should be DataHandler + raw_label = DatasetH.prepare(**params) + self.recorder.save_objects(**{"label.pkl": raw_label}) def list(self): - return ["pred.pkl"] + return ["pred.pkl", "label.pkl"] - def check(self, **kwargs): - artifacts = self.recorder.list_artifacts() - for artifact in artifacts: - if "pred.pkl" in artifact.path: - return True - return False + def load(self, name="pred.pkl"): + return super().load(name) -# TODO class SigAnaRecord(SignalRecord): - def __init__(self, recorder, config, **kwargs): - pass + def __init__(self, recorder, **kwargs): + super().__init__(recorder=recorder, **kwargs) + # The name must be unique. Otherwise it will be overridden + self.artifact_path_sig = "sig_analysis" def generate(self): - pass + self.check(parent=True) - def load(self): - pass + pred = self.load("pred.pkl") + label = self.load("label.pkl") + ic, ric = calc_ic(pred.iloc[:, 0], label.iloc[:, 0]) + metrics = { + "IC": ic.mean(), + "ICIR": ic.mean() / ic.std(), + "Rank IC": ric.mean(), + "Rank ICIR": ric.mean() / ric.std() + } + self.recorder.log_metrics(**metrics) + self.recorder.save_objects(**{"ic.pkl": ic, "ric.pkl": ric}, artifact_path=self.artifact_path_sig) + pprint(metrics) - def check(self): - pass + def list(self): + return ["{self.artifact_path_sig}/ic.pkl", "{self.artifact_path_sig}/ric.pkl"] class PortAnaRecord(SignalRecord): @@ -141,26 +163,28 @@ class PortAnaRecord(SignalRecord): """ def __init__(self, recorder, config, **kwargs): - self.recorder = recorder + """ + config["strategy"] : dict + define the strategy class as well as the kwargs. + config["backtest"] : dict + define the backtest kwargs. + """ + super().__init__(recorder=recorder) + self.strategy_config = config["strategy"] self.backtest_config = config["backtest"] self.strategy = init_instance_by_config(self.strategy_config) - self.artifact_path = "portfolio_analysis" + self.artifact_path_port = "portfolio_analysis" def generate(self, **kwargs): - """ - STRATEGY_CONFIG : dict - define the strategy class as well as the kwargs. - BACKTEST_CONFIG : dict - define the backtest kwargs. - """ # check previously stored prediction results - assert super().check(), "Make sure the parent process is completed and store the data properly." + self.check(parent=True) # "Make sure the parent process is completed and store the data properly." + # custom strategy and get backtest pred_score = super().load() report_normal, positions_normal = normal_backtest(pred_score, strategy=self.strategy, **self.backtest_config) - self.recorder.save_objects(**{"report_normal.pkl": report_normal}, artifact_path=self.artifact_path) - self.recorder.save_objects(**{"positions_normal.pkl": positions_normal}, artifact_path=self.artifact_path) + self.recorder.save_objects(**{"report_normal.pkl": report_normal}, artifact_path=self.artifact_path_port) + self.recorder.save_objects(**{"positions_normal.pkl": positions_normal}, artifact_path=self.artifact_path_port) # analysis analysis = dict() @@ -173,7 +197,7 @@ class PortAnaRecord(SignalRecord): # log metrics self.recorder.log_metrics(**flatten_dict(analysis_df["risk"].unstack().T.to_dict())) # save results - self.recorder.save_objects(**{"port_analysis.pkl": analysis_df}, artifact_path=self.artifact_path) + self.recorder.save_objects(**{"port_analysis.pkl": analysis_df}, artifact_path=self.artifact_path_port) logger.info( f"Portfolio analysis record 'port_analysis.pkl' has been saved as the artifact of the Experiment {self.recorder.experiment_id}" ) @@ -183,24 +207,9 @@ class PortAnaRecord(SignalRecord): pprint("The following are analysis results of the excess return with cost.") pprint(analysis["excess_return_with_cost"]) - def load(self, name): - # try to load the saved object - if self.artifact_path not in name: - file_name = re.split(r" |/|\\", name)[-1] - name = f"{self.artifact_path}/{file_name}" - result = self.recorder.load_object(name) - return result - def list(self): return [ - f"{self.artifact_path}/report_normal.pkl", - f"{self.artifact_path}/positions_normal.pkl", - f"{self.artifact_path}/port_analysis.pkl", + f"{self.artifact_path_port}/report_normal.pkl", + f"{self.artifact_path_port}/positions_normal.pkl", + f"{self.artifact_path_port}/port_analysis.pkl", ] - - def check(self): - artifacts = self.recorder.list_artifacts(self.artifact_path) - for artifact in artifacts: - if "port_analysis.pkl" in artifact.path: - return True - return False diff --git a/qlib/workflow/recorder.py b/qlib/workflow/recorder.py index 97f97105f..b3069b9ac 100644 --- a/qlib/workflow/recorder.py +++ b/qlib/workflow/recorder.py @@ -290,7 +290,7 @@ class MLflowRecorder(Recorder): def list_artifacts(self, artifact_path=None): assert self._uri is not None, "Please start the experiment and recorder first before using recorder directly." artifacts = self.client.list_artifacts(self.id, artifact_path) - return artifacts + return [art.path for art in artifacts] def list_metrics(self): run = self.client.get_run(self.id) From 89977320e373826fa867a34409e6348fff500400 Mon Sep 17 00:00:00 2001 From: Young Date: Sat, 21 Nov 2020 15:34:15 +0000 Subject: [PATCH 070/241] black format --- qlib/contrib/eva/alpha.py | 12 ++++++------ qlib/data/dataset/__init__.py | 16 +++++++++------- qlib/utils/serial.py | 4 ++-- qlib/workflow/expm.py | 2 +- qlib/workflow/record_temp.py | 4 ++-- 5 files changed, 20 insertions(+), 18 deletions(-) diff --git a/qlib/contrib/eva/alpha.py b/qlib/contrib/eva/alpha.py index 1faebbe19..3ef70091d 100644 --- a/qlib/contrib/eva/alpha.py +++ b/qlib/contrib/eva/alpha.py @@ -1,12 +1,12 @@ -''' +""" Here is a batch of evaluation functions. The interface should be redesigned carefully in the future. -''' +""" import pandas as pd -def calc_ic(pred: pd.Series, label: pd.Series, date_col='datetime', dropna=False) -> (pd.Series, pd.Series): +def calc_ic(pred: pd.Series, label: pd.Series, date_col="datetime", dropna=False) -> (pd.Series, pd.Series): """calc_ic. Parameters @@ -23,9 +23,9 @@ def calc_ic(pred: pd.Series, label: pd.Series, date_col='datetime', dropna=False (pd.Series, pd.Series) ic and rank ic """ - df = pd.DataFrame({'pred': pred, 'label': label}) - ic = df.groupby(date_col).apply(lambda df: df['pred'].corr(df['label'])) - ric = df.groupby(date_col).apply(lambda df: df['pred'].corr(df['label'], method='spearman')) + df = pd.DataFrame({"pred": pred, "label": label}) + ic = df.groupby(date_col).apply(lambda df: df["pred"].corr(df["label"])) + ric = df.groupby(date_col).apply(lambda df: df["pred"].corr(df["label"], method="spearman")) if dropna: return ic.dropna(), ric.dropna() else: diff --git a/qlib/data/dataset/__init__.py b/qlib/data/dataset/__init__.py index 7e41a7b7c..c46528944 100644 --- a/qlib/data/dataset/__init__.py +++ b/qlib/data/dataset/__init__.py @@ -100,11 +100,13 @@ class DatasetH(Dataset): self._handler = init_instance_by_config(handler, accept_types=DataHandler) self._segments = segments.copy() - def prepare(self, - segments: Union[List[str], Tuple[str], str, slice], - col_set=DataHandler.CS_ALL, - data_key=DataHandlerLP.DK_I, - **kwargs) -> Union[List[pd.DataFrame], pd.DataFrame]: + def prepare( + self, + segments: Union[List[str], Tuple[str], str, slice], + col_set=DataHandler.CS_ALL, + data_key=DataHandlerLP.DK_I, + **kwargs, + ) -> Union[List[pd.DataFrame], pd.DataFrame]: """ prepare the data for learning and inference @@ -132,8 +134,8 @@ class DatasetH(Dataset): logger = get_module_logger("DatasetH") fetch_kwargs = {"col_set": col_set} fetch_kwargs.update(kwargs) - if "data_key"in getfullargspec(self._handler.fetch).args: - fetch_kwargs['data_key'] = data_key + if "data_key" in getfullargspec(self._handler.fetch).args: + fetch_kwargs["data_key"] = data_key else: logger.info(f"data_key[{data_key}] is ignored.") diff --git a/qlib/utils/serial.py b/qlib/utils/serial.py index b5734d726..2d22434ac 100644 --- a/qlib/utils/serial.py +++ b/qlib/utils/serial.py @@ -10,14 +10,14 @@ class Serializable: Serializable behaves like pickle. But it only saves the state whose name **does not** start with `_` """ + def __init__(self): self._dump_all = False self._exclude = [] def __getstate__(self) -> dict: return { - k: v - for k, v in self.__dict__.items() if k not in self.exclude and (self.dump_all or not k.startswith("_")) + k: v for k, v in self.__dict__.items() if k not in self.exclude and (self.dump_all or not k.startswith("_")) } def __setstate__(self, state: dict): diff --git a/qlib/workflow/expm.py b/qlib/workflow/expm.py index 25c5d4661..e1469746a 100644 --- a/qlib/workflow/expm.py +++ b/qlib/workflow/expm.py @@ -251,7 +251,7 @@ class MLflowExpManager(ExpManager): self.active_experiment = None def create_exp(self, experiment_name=None): - assert(experiment_name is not None) + assert experiment_name is not None # init experiment experiment_id = self.client.create_experiment(experiment_name) experiment = MLflowExperiment(experiment_id, experiment_name, self.uri) diff --git a/qlib/workflow/record_temp.py b/qlib/workflow/record_temp.py index 08daa1dae..af4e99acb 100644 --- a/qlib/workflow/record_temp.py +++ b/qlib/workflow/record_temp.py @@ -119,7 +119,7 @@ class SignalRecord(RecordTemp): raw_label = DatasetH.prepare(**params) except TypeError: # The argument number is not right - del params['data_key'] + del params["data_key"] # The backend handler should be DataHandler raw_label = DatasetH.prepare(**params) self.recorder.save_objects(**{"label.pkl": raw_label}) @@ -147,7 +147,7 @@ class SigAnaRecord(SignalRecord): "IC": ic.mean(), "ICIR": ic.mean() / ic.std(), "Rank IC": ric.mean(), - "Rank ICIR": ric.mean() / ric.std() + "Rank ICIR": ric.mean() / ric.std(), } self.recorder.log_metrics(**metrics) self.recorder.save_objects(**{"ic.pkl": ic, "ric.pkl": ric}, artifact_path=self.artifact_path_sig) From 220208f28c49e48c00901aef6428c2a6816f525b Mon Sep 17 00:00:00 2001 From: Young Date: Sun, 22 Nov 2020 03:17:50 +0000 Subject: [PATCH 071/241] fix record_tmp bug --- qlib/workflow/record_temp.py | 37 +++++++++++++++++++++++++----------- setup.py | 2 +- tests/test_all_pipeline.py | 2 +- 3 files changed, 28 insertions(+), 13 deletions(-) diff --git a/qlib/workflow/record_temp.py b/qlib/workflow/record_temp.py index af4e99acb..87d6405b8 100644 --- a/qlib/workflow/record_temp.py +++ b/qlib/workflow/record_temp.py @@ -24,6 +24,18 @@ class RecordTemp: This is the Records Template class that enables user to generate experiment results such as IC and backtest in a certain format. """ + artifact_path = None + + @classmethod + def get_path(cls, path=None): + names = [] + if cls.artifact_path is not None: + names.append(cls.artifact_path) + + if path is not None: + names.append(path) + + return "/".join(names) def __init__(self, recorder): self.recorder = recorder @@ -79,7 +91,7 @@ class RecordTemp: artifacts = set(self.recorder.list_artifacts()) if parent: # Downcasting have to be done here instead of using `super` - flist = self.__class__.__base__.list(self) + flist = self.__class__.__base__.list(self) # pylint: disable=E1101 else: flist = self.list() for item in flist: @@ -132,10 +144,12 @@ class SignalRecord(RecordTemp): class SigAnaRecord(SignalRecord): + + artifact_path = "sig_analysis" + def __init__(self, recorder, **kwargs): super().__init__(recorder=recorder, **kwargs) # The name must be unique. Otherwise it will be overridden - self.artifact_path_sig = "sig_analysis" def generate(self): self.check(parent=True) @@ -150,11 +164,11 @@ class SigAnaRecord(SignalRecord): "Rank ICIR": ric.mean() / ric.std(), } self.recorder.log_metrics(**metrics) - self.recorder.save_objects(**{"ic.pkl": ic, "ric.pkl": ric}, artifact_path=self.artifact_path_sig) + self.recorder.save_objects(**{"ic.pkl": ic, "ric.pkl": ric}, artifact_path=self.get_path()) pprint(metrics) def list(self): - return ["{self.artifact_path_sig}/ic.pkl", "{self.artifact_path_sig}/ric.pkl"] + return [self.get_path("ic.pkl"), self.get_path("ric.pkl")] class PortAnaRecord(SignalRecord): @@ -162,6 +176,8 @@ class PortAnaRecord(SignalRecord): This is the Portfolio Analysis Record class that generates the results such as those of backtest. """ + artifact_path = "portfolio_analysis" + def __init__(self, recorder, config, **kwargs): """ config["strategy"] : dict @@ -174,7 +190,6 @@ class PortAnaRecord(SignalRecord): self.strategy_config = config["strategy"] self.backtest_config = config["backtest"] self.strategy = init_instance_by_config(self.strategy_config) - self.artifact_path_port = "portfolio_analysis" def generate(self, **kwargs): # check previously stored prediction results @@ -183,8 +198,8 @@ 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) - self.recorder.save_objects(**{"report_normal.pkl": report_normal}, artifact_path=self.artifact_path_port) - self.recorder.save_objects(**{"positions_normal.pkl": positions_normal}, artifact_path=self.artifact_path_port) + 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()) # analysis analysis = dict() @@ -197,7 +212,7 @@ class PortAnaRecord(SignalRecord): # log metrics self.recorder.log_metrics(**flatten_dict(analysis_df["risk"].unstack().T.to_dict())) # save results - self.recorder.save_objects(**{"port_analysis.pkl": analysis_df}, artifact_path=self.artifact_path_port) + self.recorder.save_objects(**{"port_analysis.pkl": analysis_df}, artifact_path=PortAnaRecord.get_path()) logger.info( f"Portfolio analysis record 'port_analysis.pkl' has been saved as the artifact of the Experiment {self.recorder.experiment_id}" ) @@ -209,7 +224,7 @@ class PortAnaRecord(SignalRecord): def list(self): return [ - f"{self.artifact_path_port}/report_normal.pkl", - f"{self.artifact_path_port}/positions_normal.pkl", - f"{self.artifact_path_port}/port_analysis.pkl", + PortAnaRecord.get_path("report_normal.pkl"), + PortAnaRecord.get_path("positions_normal.pkl"), + PortAnaRecord.get_path("port_analysis.pkl"), ] diff --git a/setup.py b/setup.py index 7c2688666..7d7ea7fdb 100644 --- a/setup.py +++ b/setup.py @@ -101,7 +101,7 @@ setup( entry_points={ # 'console_scripts': ['mycli=mymodule:cli'], "console_scripts": [ - "workflow_by_config=qlib.workflow.cli:run", + "qrun=qlib.workflow.cli:run", ], }, ext_modules=extensions, diff --git a/tests/test_all_pipeline.py b/tests/test_all_pipeline.py index 16242189a..d2fb506ee 100644 --- a/tests/test_all_pipeline.py +++ b/tests/test_all_pipeline.py @@ -147,7 +147,7 @@ def backtest_analysis(pred, rid): # backtest par = PortAnaRecord(recorder, port_analysis_config) par.generate() - analysis_df = par.load("port_analysis.pkl") + analysis_df = par.load(par.get_path("port_analysis.pkl")) print(analysis_df) return analysis_df From ae60546fe945c4fec25bd05f163a747071ef43ae Mon Sep 17 00:00:00 2001 From: Young Date: Sun, 22 Nov 2020 03:17:50 +0000 Subject: [PATCH 072/241] fix record_tmp bug --- qlib/workflow/record_temp.py | 38 +++++++++++++++++++++++++----------- setup.py | 2 +- tests/test_all_pipeline.py | 2 +- 3 files changed, 29 insertions(+), 13 deletions(-) diff --git a/qlib/workflow/record_temp.py b/qlib/workflow/record_temp.py index af4e99acb..b1fd9cc83 100644 --- a/qlib/workflow/record_temp.py +++ b/qlib/workflow/record_temp.py @@ -25,6 +25,19 @@ class RecordTemp: backtest in a certain format. """ + artifact_path = None + + @classmethod + def get_path(cls, path=None): + names = [] + if cls.artifact_path is not None: + names.append(cls.artifact_path) + + if path is not None: + names.append(path) + + return "/".join(names) + def __init__(self, recorder): self.recorder = recorder @@ -79,7 +92,7 @@ class RecordTemp: artifacts = set(self.recorder.list_artifacts()) if parent: # Downcasting have to be done here instead of using `super` - flist = self.__class__.__base__.list(self) + flist = self.__class__.__base__.list(self) # pylint: disable=E1101 else: flist = self.list() for item in flist: @@ -132,10 +145,12 @@ class SignalRecord(RecordTemp): class SigAnaRecord(SignalRecord): + + artifact_path = "sig_analysis" + def __init__(self, recorder, **kwargs): super().__init__(recorder=recorder, **kwargs) # The name must be unique. Otherwise it will be overridden - self.artifact_path_sig = "sig_analysis" def generate(self): self.check(parent=True) @@ -150,11 +165,11 @@ class SigAnaRecord(SignalRecord): "Rank ICIR": ric.mean() / ric.std(), } self.recorder.log_metrics(**metrics) - self.recorder.save_objects(**{"ic.pkl": ic, "ric.pkl": ric}, artifact_path=self.artifact_path_sig) + self.recorder.save_objects(**{"ic.pkl": ic, "ric.pkl": ric}, artifact_path=self.get_path()) pprint(metrics) def list(self): - return ["{self.artifact_path_sig}/ic.pkl", "{self.artifact_path_sig}/ric.pkl"] + return [self.get_path("ic.pkl"), self.get_path("ric.pkl")] class PortAnaRecord(SignalRecord): @@ -162,6 +177,8 @@ class PortAnaRecord(SignalRecord): This is the Portfolio Analysis Record class that generates the results such as those of backtest. """ + artifact_path = "portfolio_analysis" + def __init__(self, recorder, config, **kwargs): """ config["strategy"] : dict @@ -174,7 +191,6 @@ class PortAnaRecord(SignalRecord): self.strategy_config = config["strategy"] self.backtest_config = config["backtest"] self.strategy = init_instance_by_config(self.strategy_config) - self.artifact_path_port = "portfolio_analysis" def generate(self, **kwargs): # check previously stored prediction results @@ -183,8 +199,8 @@ 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) - self.recorder.save_objects(**{"report_normal.pkl": report_normal}, artifact_path=self.artifact_path_port) - self.recorder.save_objects(**{"positions_normal.pkl": positions_normal}, artifact_path=self.artifact_path_port) + 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()) # analysis analysis = dict() @@ -197,7 +213,7 @@ class PortAnaRecord(SignalRecord): # log metrics self.recorder.log_metrics(**flatten_dict(analysis_df["risk"].unstack().T.to_dict())) # save results - self.recorder.save_objects(**{"port_analysis.pkl": analysis_df}, artifact_path=self.artifact_path_port) + self.recorder.save_objects(**{"port_analysis.pkl": analysis_df}, artifact_path=PortAnaRecord.get_path()) logger.info( f"Portfolio analysis record 'port_analysis.pkl' has been saved as the artifact of the Experiment {self.recorder.experiment_id}" ) @@ -209,7 +225,7 @@ class PortAnaRecord(SignalRecord): def list(self): return [ - f"{self.artifact_path_port}/report_normal.pkl", - f"{self.artifact_path_port}/positions_normal.pkl", - f"{self.artifact_path_port}/port_analysis.pkl", + PortAnaRecord.get_path("report_normal.pkl"), + PortAnaRecord.get_path("positions_normal.pkl"), + PortAnaRecord.get_path("port_analysis.pkl"), ] diff --git a/setup.py b/setup.py index 7c2688666..7d7ea7fdb 100644 --- a/setup.py +++ b/setup.py @@ -101,7 +101,7 @@ setup( entry_points={ # 'console_scripts': ['mycli=mymodule:cli'], "console_scripts": [ - "workflow_by_config=qlib.workflow.cli:run", + "qrun=qlib.workflow.cli:run", ], }, ext_modules=extensions, diff --git a/tests/test_all_pipeline.py b/tests/test_all_pipeline.py index 16242189a..d2fb506ee 100644 --- a/tests/test_all_pipeline.py +++ b/tests/test_all_pipeline.py @@ -147,7 +147,7 @@ def backtest_analysis(pred, rid): # backtest par = PortAnaRecord(recorder, port_analysis_config) par.generate() - analysis_df = par.load("port_analysis.pkl") + analysis_df = par.load(par.get_path("port_analysis.pkl")) print(analysis_df) return analysis_df From c8d7d3ea2aed896b352cc07dd7d747424af0efa0 Mon Sep 17 00:00:00 2001 From: Young Date: Sun, 22 Nov 2020 03:17:50 +0000 Subject: [PATCH 073/241] fix record_tmp bug --- .github/workflows/test.yml | 2 +- qlib/workflow/record_temp.py | 38 +++++++++++++++++++++++++----------- setup.py | 2 +- tests/test_all_pipeline.py | 2 +- 4 files changed, 30 insertions(+), 14 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d1e01e46b..935d03116 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -56,4 +56,4 @@ jobs: - name: Test workflow by config run: | - workflow_by_config examples/benchmarks/GBDT/workflow_config_gbdt.yaml \ No newline at end of file + qrun examples/benchmarks/GBDT/workflow_config_gbdt.yaml diff --git a/qlib/workflow/record_temp.py b/qlib/workflow/record_temp.py index af4e99acb..b1fd9cc83 100644 --- a/qlib/workflow/record_temp.py +++ b/qlib/workflow/record_temp.py @@ -25,6 +25,19 @@ class RecordTemp: backtest in a certain format. """ + artifact_path = None + + @classmethod + def get_path(cls, path=None): + names = [] + if cls.artifact_path is not None: + names.append(cls.artifact_path) + + if path is not None: + names.append(path) + + return "/".join(names) + def __init__(self, recorder): self.recorder = recorder @@ -79,7 +92,7 @@ class RecordTemp: artifacts = set(self.recorder.list_artifacts()) if parent: # Downcasting have to be done here instead of using `super` - flist = self.__class__.__base__.list(self) + flist = self.__class__.__base__.list(self) # pylint: disable=E1101 else: flist = self.list() for item in flist: @@ -132,10 +145,12 @@ class SignalRecord(RecordTemp): class SigAnaRecord(SignalRecord): + + artifact_path = "sig_analysis" + def __init__(self, recorder, **kwargs): super().__init__(recorder=recorder, **kwargs) # The name must be unique. Otherwise it will be overridden - self.artifact_path_sig = "sig_analysis" def generate(self): self.check(parent=True) @@ -150,11 +165,11 @@ class SigAnaRecord(SignalRecord): "Rank ICIR": ric.mean() / ric.std(), } self.recorder.log_metrics(**metrics) - self.recorder.save_objects(**{"ic.pkl": ic, "ric.pkl": ric}, artifact_path=self.artifact_path_sig) + self.recorder.save_objects(**{"ic.pkl": ic, "ric.pkl": ric}, artifact_path=self.get_path()) pprint(metrics) def list(self): - return ["{self.artifact_path_sig}/ic.pkl", "{self.artifact_path_sig}/ric.pkl"] + return [self.get_path("ic.pkl"), self.get_path("ric.pkl")] class PortAnaRecord(SignalRecord): @@ -162,6 +177,8 @@ class PortAnaRecord(SignalRecord): This is the Portfolio Analysis Record class that generates the results such as those of backtest. """ + artifact_path = "portfolio_analysis" + def __init__(self, recorder, config, **kwargs): """ config["strategy"] : dict @@ -174,7 +191,6 @@ class PortAnaRecord(SignalRecord): self.strategy_config = config["strategy"] self.backtest_config = config["backtest"] self.strategy = init_instance_by_config(self.strategy_config) - self.artifact_path_port = "portfolio_analysis" def generate(self, **kwargs): # check previously stored prediction results @@ -183,8 +199,8 @@ 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) - self.recorder.save_objects(**{"report_normal.pkl": report_normal}, artifact_path=self.artifact_path_port) - self.recorder.save_objects(**{"positions_normal.pkl": positions_normal}, artifact_path=self.artifact_path_port) + 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()) # analysis analysis = dict() @@ -197,7 +213,7 @@ class PortAnaRecord(SignalRecord): # log metrics self.recorder.log_metrics(**flatten_dict(analysis_df["risk"].unstack().T.to_dict())) # save results - self.recorder.save_objects(**{"port_analysis.pkl": analysis_df}, artifact_path=self.artifact_path_port) + self.recorder.save_objects(**{"port_analysis.pkl": analysis_df}, artifact_path=PortAnaRecord.get_path()) logger.info( f"Portfolio analysis record 'port_analysis.pkl' has been saved as the artifact of the Experiment {self.recorder.experiment_id}" ) @@ -209,7 +225,7 @@ class PortAnaRecord(SignalRecord): def list(self): return [ - f"{self.artifact_path_port}/report_normal.pkl", - f"{self.artifact_path_port}/positions_normal.pkl", - f"{self.artifact_path_port}/port_analysis.pkl", + PortAnaRecord.get_path("report_normal.pkl"), + PortAnaRecord.get_path("positions_normal.pkl"), + PortAnaRecord.get_path("port_analysis.pkl"), ] diff --git a/setup.py b/setup.py index 7c2688666..7d7ea7fdb 100644 --- a/setup.py +++ b/setup.py @@ -101,7 +101,7 @@ setup( entry_points={ # 'console_scripts': ['mycli=mymodule:cli'], "console_scripts": [ - "workflow_by_config=qlib.workflow.cli:run", + "qrun=qlib.workflow.cli:run", ], }, ext_modules=extensions, diff --git a/tests/test_all_pipeline.py b/tests/test_all_pipeline.py index 16242189a..d2fb506ee 100644 --- a/tests/test_all_pipeline.py +++ b/tests/test_all_pipeline.py @@ -147,7 +147,7 @@ def backtest_analysis(pred, rid): # backtest par = PortAnaRecord(recorder, port_analysis_config) par.generate() - analysis_df = par.load("port_analysis.pkl") + analysis_df = par.load(par.get_path("port_analysis.pkl")) print(analysis_df) return analysis_df From 89586562226b1f3aaf4b38ef283af92ffe105e71 Mon Sep 17 00:00:00 2001 From: Jactus Date: Sun, 22 Nov 2020 13:38:40 +0800 Subject: [PATCH 074/241] Update test scipts --- examples/run_all_model.py | 4 ++++ qlib/workflow/record_temp.py | 25 ------------------------- tests/test_all_pipeline.py | 25 +++++++++++++++---------- 3 files changed, 19 insertions(+), 35 deletions(-) diff --git a/examples/run_all_model.py b/examples/run_all_model.py index 6efde6a52..0b7e1dbbe 100644 --- a/examples/run_all_model.py +++ b/examples/run_all_model.py @@ -198,6 +198,10 @@ def gen_and_save_md_table(results): # function to run the all the models def run(): + """ + Please be aware that this function can only work under Linux. MacOS and Windows will be supported in the future. + Any PR to enhance this method is highly welcomed. + """ # get all folders folders = get_all_folders() # set up diff --git a/qlib/workflow/record_temp.py b/qlib/workflow/record_temp.py index 6432883f2..b1fd9cc83 100644 --- a/qlib/workflow/record_temp.py +++ b/qlib/workflow/record_temp.py @@ -24,31 +24,6 @@ class RecordTemp: This is the Records Template class that enables user to generate experiment results such as IC and backtest in a certain format. """ - artifact_path = None - - @classmethod - def get_path(cls, path=None): - names = [] - if cls.artifact_path is not None: - names.append(cls.artifact_path) - - if path is not None: - names.append(path) - - return "/".join(names) - - artifact_path = None - - @classmethod - def get_path(cls, path=None): - names = [] - if cls.artifact_path is not None: - names.append(cls.artifact_path) - - if path is not None: - names.append(path) - - return "/".join(names) artifact_path = None diff --git a/tests/test_all_pipeline.py b/tests/test_all_pipeline.py index d2fb506ee..befd296b0 100644 --- a/tests/test_all_pipeline.py +++ b/tests/test_all_pipeline.py @@ -8,7 +8,6 @@ from pathlib import Path import numpy as np import pandas as pd -from scipy.stats import pearsonr import qlib from qlib.config import REG_CN, C @@ -22,7 +21,7 @@ from qlib.contrib.evaluate import ( ) 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 +from qlib.workflow.record_temp import SignalRecord, SigAnaRecord, PortAnaRecord market = "csi300" @@ -123,11 +122,13 @@ def train(): sr.generate() pred_score = sr.load() - y_test = dataset.prepare("test", col_set="label") - pred_score, y_test, __ = drop_nan_by_y_index(pred_score, y_test) - model_pearsonr = pearsonr(np.ravel(pred_score.values), np.ravel(y_test.values))[0] + # calculate ic and ric + sar = SigAnaRecord(recorder) + sar.generate() + ic = sar.load(sar.get_path("ic.pkl")) + ric = sar.load(sar.get_path("ric.pkl")) - return pred_score, {"model_pearsonr": model_pearsonr}, rid + return pred_score, {"ic": ic, "ric": ric}, rid def backtest_analysis(pred, rid): @@ -135,12 +136,15 @@ def backtest_analysis(pred, rid): Parameters ---------- - pred: pandas.DataFrame + pred : pandas.DataFrame predict scores + rid : str + the id of the recorder to be used in this function Returns ------- - analysis result : pandas.DataFrame + analysis : pandas.DataFrame + the analysis result """ recorder = R.get_recorder(experiment_name="workflow", recorder_id=rid) @@ -177,8 +181,9 @@ class TestAllFlow(unittest.TestCase): shutil.rmtree(str(Path(C["exp_manager"]["kwargs"]["uri"].strip("file:")).resolve())) def test_0_train(self): - TestAllFlow.PRED_SCORE, model_pearsonr, TestAllFlow.RID = train() - self.assertGreaterEqual(model_pearsonr["model_pearsonr"], 0, "train failed") + TestAllFlow.PRED_SCORE, ic_ric, TestAllFlow.RID = train() + self.assertGreaterEqual(ic_ric["ic"].all(), 0, "train failed") + self.assertGreaterEqual(ic_ric["ric"].all(), 0, "train failed") def test_1_backtest(self): analyze_df = backtest_analysis(TestAllFlow.PRED_SCORE, TestAllFlow.RID) From bac16060fffbb76efc39dcbeee241d914aef0c97 Mon Sep 17 00:00:00 2001 From: zhupr Date: Sun, 22 Nov 2020 15:26:27 +0800 Subject: [PATCH 075/241] fix us instruments --- qlib/config.py | 1 + qlib/data/data.py | 34 ++++++++++++-------- qlib/utils/__init__.py | 4 ++- scripts/README.md | 1 - scripts/data_collector/index.py | 7 +++- scripts/data_collector/us_index/collector.py | 4 +++ scripts/data_collector/utils.py | 11 +++++-- scripts/dump_bin.py | 31 +++++++++++++++--- scripts/get_data.py | 3 -- 9 files changed, 70 insertions(+), 26 deletions(-) diff --git a/qlib/config.py b/qlib/config.py index d05161772..ac9c3ba65 100644 --- a/qlib/config.py +++ b/qlib/config.py @@ -155,6 +155,7 @@ MODE_CONF = { # cache "expression_cache": "DiskExpressionCache", "dataset_cache": "DiskDatasetCache", + "mount_path": None, }, "client": { # data provider config diff --git a/qlib/data/data.py b/qlib/data/data.py index 8331b1802..8fac9edec 100644 --- a/qlib/data/data.py +++ b/qlib/data/data.py @@ -15,6 +15,7 @@ import importlib import traceback import numpy as np import pandas as pd +from pathlib import Path from multiprocessing import Pool from .cache import H @@ -211,6 +212,20 @@ class InstrumentProvider(abc.ABC): return cls.LIST raise ValueError(f"Unknown instrument type {inst}") + def convert_instruments(self, instrument): + _instruments_map = getattr(self, "_instruments_map", None) + if _instruments_map is None: + _df_list = [] + # FIXME: each process will read these files + for _path in Path(C.get_data_path()).joinpath("instruments").glob("*.txt"): + _df = pd.read_csv(_path, sep="\t", names=["inst", "start_datetime", "end_datetime", "save_inst"]) + _df_list.append(_df.iloc[:, [0, -1]]) + df = pd.concat(_df_list, sort=False).sort_values("save_inst") + df = df.drop_duplicates(subset=["save_inst"], keep="first").fillna(axis=1, method="ffill") + _instruments_map = df.set_index("inst").iloc[:, 0].to_dict() + setattr(self, "_instruments_map", _instruments_map) + return _instruments_map.get(instrument, instrument) + class FeatureProvider(abc.ABC): """Feature provider class @@ -570,19 +585,11 @@ class LocalInstrumentProvider(InstrumentProvider): if not os.path.exists(fname): raise ValueError("instruments not exists for market " + market) _instruments = dict() - with open(fname) as f: - for line in f: - inst_time = line.strip().split() - inst = inst_time[0] - if len(inst_time) == 3: - # `day` - begin = inst_time[1] - end = inst_time[2] - elif len(inst_time) == 5: - # `1min` - begin = inst_time[1] + " " + inst_time[2] - end = inst_time[3] + " " + inst_time[4] - _instruments.setdefault(inst, []).append((pd.Timestamp(begin), pd.Timestamp(end))) + df = pd.read_csv(fname, sep="\t", names=["inst", "start_datetime", "end_datetime", "save_inst"]) + df["start_datetime"] = pd.to_datetime(df["start_datetime"]) + df["end_datetime"] = pd.to_datetime(df["end_datetime"]) + for row in df.itertuples(index=False): + _instruments.setdefault(row[0], []).append((row[1], row[2])) return _instruments def list_instruments(self, instruments, start_time=None, end_time=None, freq="day", as_list=False): @@ -637,6 +644,7 @@ class LocalFeatureProvider(FeatureProvider): def feature(self, instrument, field, start_index, end_index, freq): # validate field = str(field).lower()[1:] + instrument = Inst.convert_instruments(instrument) uri_data = self._uri_data.format(instrument.lower(), field, freq) if not os.path.exists(uri_data): get_module_logger("data").warning("WARN: data not found for %s.%s" % (instrument, field)) diff --git a/qlib/utils/__init__.py b/qlib/utils/__init__.py index f32cceba3..79fd6fe5c 100644 --- a/qlib/utils/__init__.py +++ b/qlib/utils/__init__.py @@ -613,7 +613,9 @@ def exists_qlib_data(qlib_dir): # check instruments code_names = set(map(lambda x: x.name.lower(), features_dir.iterdir())) _instrument = instruments_dir.joinpath("all.txt") - miss_code = set(pd.read_csv(_instrument, sep="\t", header=None).loc[:, 0].apply(str.lower)) - set(code_names) + df = pd.read_csv(_instrument, sep="\t", names=["inst", "start_datetime", "end_datetime", "save_inst"]) + df = df.iloc[:, [0, -1]].fillna(axis=1, method="ffill") + miss_code = set(df.iloc[:, -1].apply(str.lower)) - set(code_names) if miss_code and any(map(lambda x: "sht" not in x, miss_code)): return False diff --git a/scripts/README.md b/scripts/README.md index 98b01e0c3..88ebdc680 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -20,7 +20,6 @@ python get_data.py qlib_data --target_dir ~/.qlib/qlib_data/cn_data --region cn ### Downlaod US Data -> The US stock code contains 'PRN', and the directory cannot be created on Windows system: https://superuser.com/questions/613313/why-cant-we-make-con-prn-null-folder-in-windows ```bash python get_data.py qlib_data --target_dir ~/.qlib/qlib_data/us_data --region us diff --git a/scripts/data_collector/index.py b/scripts/data_collector/index.py index c5f3854fd..300e6b625 100644 --- a/scripts/data_collector/index.py +++ b/scripts/data_collector/index.py @@ -24,6 +24,7 @@ class IndexBase: INSTRUMENTS_COLUMNS = [SYMBOL_FIELD_NAME, START_DATE_FIELD, END_DATE_FIELD] REMOVE = "remove" ADD = "add" + INST_PREFIX = "" def __init__(self, index_name: str, qlib_dir: [str, Path] = None, request_retry: int = 5, retry_sleep: int = 3): """ @@ -196,7 +197,11 @@ class IndexBase: _tmp_df = pd.DataFrame([[_row.symbol, self.bench_start_date, _row.date]], columns=instruments_columns) new_df = new_df.append(_tmp_df, sort=False) - new_df.loc[:, instruments_columns].to_csv( + inst_df = new_df.loc[:, instruments_columns] + _inst_prefix = self.INST_PREFIX.strip() + if _inst_prefix: + inst_df["save_inst"] = inst_df[self.SYMBOL_FIELD_NAME].apply(lambda x: f"{_inst_prefix}{x}") + inst_df.to_csv( self.instruments_dir.joinpath(f"{self.index_name.lower()}.txt"), sep="\t", index=False, header=None ) logger.info(f"parse {self.index_name.lower()} companies finished.") diff --git a/scripts/data_collector/us_index/collector.py b/scripts/data_collector/us_index/collector.py index ea1e974a0..0641437e0 100644 --- a/scripts/data_collector/us_index/collector.py +++ b/scripts/data_collector/us_index/collector.py @@ -33,6 +33,10 @@ WIKI_INDEX_NAME_MAP = { class WIKIIndex(IndexBase): + # NOTE: The US stock code contains "PRN", and the directory cannot be created on Windows system, use the "_" prefix + # https://superuser.com/questions/613313/why-cant-we-make-con-prn-null-folder-in-windows + INST_PREFIX = "_" + def __init__(self, index_name: str, qlib_dir: [str, Path] = None, request_retry: int = 5, retry_sleep: int = 3): super(WIKIIndex, self).__init__( index_name=index_name, qlib_dir=qlib_dir, request_retry=request_retry, retry_sleep=retry_sleep diff --git a/scripts/data_collector/utils.py b/scripts/data_collector/utils.py index 855569642..2cf9f4c6a 100644 --- a/scripts/data_collector/utils.py +++ b/scripts/data_collector/utils.py @@ -184,9 +184,14 @@ def get_us_stock_symbols(qlib_data_path: [str, Path] = None) -> list: names=["symbol", "start_date", "end_date"], ) _all_symbols += ins_df["symbol"].unique().tolist() - _US_SYMBOLS = sorted( - set(map(lambda x: x.replace(".", "-"), filter(lambda x: len(x) < 8 and not x.endswith("WS"), _all_symbols))) - ) + + def _format(s_): + s_ = s_.replace(".", "-") + s_ = s_.strip("$") + s_ = s_.strip("*") + return s_ + + _US_SYMBOLS = sorted(set(map(_format, filter(lambda x: len(x) < 8 and not x.endswith("WS"), _all_symbols)))) return _US_SYMBOLS diff --git a/scripts/dump_bin.py b/scripts/dump_bin.py index 2e44c454e..2bca4f037 100644 --- a/scripts/dump_bin.py +++ b/scripts/dump_bin.py @@ -27,6 +27,7 @@ class DumpDataBase: HIGH_FREQ_FORMAT = "%Y-%m-%d %H:%M:%S" INSTRUMENTS_SEP = "\t" INSTRUMENTS_FILE_NAME = "all.txt" + SAVE_INST_FIELD = "save_inst" UPDATE_MODE = "update" ALL_MODE = "all" @@ -44,6 +45,7 @@ class DumpDataBase: exclude_fields: str = "", include_fields: str = "", limit_nums: int = None, + inst_prefix: str = "", ): """ @@ -71,6 +73,9 @@ class DumpDataBase: fields not dumped limit_nums: int Use when debugging, default None + inst_prefix: str + add a column to the instruments file and record the saved instrument name, + the US stock code contains "PRN", and the directory cannot be created on Windows system, use the "_" prefix. """ csv_path = Path(csv_path).expanduser() if isinstance(exclude_fields, str): @@ -79,6 +84,7 @@ class DumpDataBase: include_fields = include_fields.split(",") self._exclude_fields = tuple(filter(lambda x: len(x) > 0, map(str.strip, exclude_fields))) self._include_fields = tuple(filter(lambda x: len(x) > 0, map(str.strip, include_fields))) + self._inst_prefix = inst_prefix.strip() self.file_suffix = file_suffix self.symbol_field_name = symbol_field_name self.csv_files = sorted(csv_path.glob(f"*{self.file_suffix}") if csv_path.is_dir() else [csv_path]) @@ -160,12 +166,19 @@ class DumpDataBase: ) def _read_instruments(self, instrument_path: Path) -> pd.DataFrame: - return pd.read_csv( + df = pd.read_csv( instrument_path, sep=self.INSTRUMENTS_SEP, - names=[self.symbol_field_name, self.INSTRUMENTS_START_FIELD, self.INSTRUMENTS_END_FIELD], + names=[ + self.symbol_field_name, + self.INSTRUMENTS_START_FIELD, + self.INSTRUMENTS_END_FIELD, + self.SAVE_INST_FIELD, + ], ) + return df + def save_calendars(self, calendars_data: list): self._calendars_dir.mkdir(parents=True, exist_ok=True) calendars_path = str(self._calendars_dir.joinpath(f"{self.freq}.txt").expanduser().resolve()) @@ -176,7 +189,13 @@ class DumpDataBase: self._instruments_dir.mkdir(parents=True, exist_ok=True) instruments_path = str(self._instruments_dir.joinpath(self.INSTRUMENTS_FILE_NAME).resolve()) if isinstance(instruments_data, pd.DataFrame): - instruments_data = instruments_data.loc[:, [self.INSTRUMENTS_START_FIELD, self.INSTRUMENTS_END_FIELD]] + _df_fields = [self.symbol_field_name, self.INSTRUMENTS_START_FIELD, self.INSTRUMENTS_END_FIELD] + if self._inst_prefix: + _df_fields.append(self.SAVE_INST_FIELD) + instruments_data[self.SAVE_INST_FIELD] = instruments_data[self.symbol_field_name].apply( + lambda x: f"{self._inst_prefix}{x}" + ) + instruments_data = instruments_data.loc[:, _df_fields] instruments_data.to_csv(instruments_path, header=False, sep=self.INSTRUMENTS_SEP) else: np.savetxt(instruments_path, instruments_data, fmt="%s", encoding="utf-8") @@ -234,6 +253,7 @@ class DumpDataBase: logger.warning(f"{code} data is None or empty") return # features save dir + code = self._inst_prefix + code if self._inst_prefix else code features_dir = self._features_dir.joinpath(code) features_dir.mkdir(parents=True, exist_ok=True) self._data_to_bin(df, calendar_list, features_dir) @@ -262,7 +282,10 @@ class DumpDataAll(DumpDataBase): _begin_time = self._format_datetime(_begin_time) _end_time = self._format_datetime(_end_time) symbol = self.get_symbol_from_file(file_path) - date_range_list.append(f"{self.INSTRUMENTS_SEP.join((symbol.upper(), _begin_time, _end_time))}") + _inst_fields = [symbol.upper(), _begin_time, _end_time] + if self._inst_prefix: + _inst_fields.append(self._inst_prefix + symbol.upper()) + date_range_list.append(f"{self.INSTRUMENTS_SEP.join(_inst_fields)}") p_bar.update() self._kwargs["all_datetime_set"] = all_datetime self._kwargs["date_range_list"] = date_range_list diff --git a/scripts/get_data.py b/scripts/get_data.py index 4c0595238..f4dba1474 100644 --- a/scripts/get_data.py +++ b/scripts/get_data.py @@ -79,9 +79,6 @@ class GetData: ------- """ - # TODO: The US stock code contains "PRN", and the directory cannot be created on Windows system - if region.lower() == "us": - logger.warning(f"The US stock code contains 'PRN', and the directory cannot be created on Windows system") file_name = f"{name}_{region.lower()}_{interval.lower()}_{version}.zip" self._download_data(file_name.lower(), target_dir) From aed566fa0097566a3179c36c9e3870be7724d809 Mon Sep 17 00:00:00 2001 From: Jactus Date: Mon, 23 Nov 2020 10:53:26 +0800 Subject: [PATCH 076/241] Fix get_data bug in workflows --- examples/workflow_by_code.ipynb | 2 +- examples/workflow_by_code.py | 2 +- examples/workflow_by_code_finetune.py | 2 +- examples/workflow_by_code_gats.py | 2 +- examples/workflow_by_code_gru.py | 2 +- examples/workflow_by_code_lstm.py | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/examples/workflow_by_code.ipynb b/examples/workflow_by_code.ipynb index 246d2b0c2..1ac9ab17c 100644 --- a/examples/workflow_by_code.ipynb +++ b/examples/workflow_by_code.ipynb @@ -49,7 +49,7 @@ " print(f\"Qlib data is not found in {provider_uri}\")\n", " sys.path.append(str(Path.cwd().parent.joinpath(\"scripts\")))\n", " from get_data import GetData\n", - " GetData().qlib_data_cn(target_dir=provider_uri)\n", + " GetData().qlib_data(target_dir=provider_uri, region=\"cn\")\n", "qlib.init(provider_uri=provider_uri, region=REG_CN)" ] }, diff --git a/examples/workflow_by_code.py b/examples/workflow_by_code.py index b62ace155..8d495e05e 100644 --- a/examples/workflow_by_code.py +++ b/examples/workflow_by_code.py @@ -28,7 +28,7 @@ if __name__ == "__main__": sys.path.append(str(Path(__file__).resolve().parent.parent.joinpath("scripts"))) from get_data import GetData - GetData().qlib_data_cn(target_dir=provider_uri) + GetData().qlib_data(target_dir=provider_uri, region="cn") qlib.init(provider_uri=provider_uri, region=REG_CN) diff --git a/examples/workflow_by_code_finetune.py b/examples/workflow_by_code_finetune.py index c69ecc350..209cb4a1e 100644 --- a/examples/workflow_by_code_finetune.py +++ b/examples/workflow_by_code_finetune.py @@ -28,7 +28,7 @@ if __name__ == "__main__": sys.path.append(str(Path(__file__).resolve().parent.parent.joinpath("scripts"))) from get_data import GetData - GetData().qlib_data_cn(target_dir=provider_uri) + GetData().qlib_data(target_dir=provider_uri, region="cn") qlib.init(provider_uri=provider_uri, region=REG_CN) diff --git a/examples/workflow_by_code_gats.py b/examples/workflow_by_code_gats.py index 222eb126f..6d44bd1b6 100644 --- a/examples/workflow_by_code_gats.py +++ b/examples/workflow_by_code_gats.py @@ -30,7 +30,7 @@ if __name__ == "__main__": sys.path.append(str(Path(__file__).resolve().parent.parent.joinpath("scripts"))) from get_data import GetData - GetData().qlib_data_cn(target_dir=provider_uri) + GetData().qlib_data(target_dir=provider_uri, region="cn") qlib.init(provider_uri=provider_uri, region=REG_CN) diff --git a/examples/workflow_by_code_gru.py b/examples/workflow_by_code_gru.py index e55f0ae45..96e461ba8 100644 --- a/examples/workflow_by_code_gru.py +++ b/examples/workflow_by_code_gru.py @@ -30,7 +30,7 @@ if __name__ == "__main__": sys.path.append(str(Path(__file__).resolve().parent.parent.joinpath("scripts"))) from get_data import GetData - GetData().qlib_data_cn(target_dir=provider_uri) + GetData().qlib_data(target_dir=provider_uri, region="cn") qlib.init(provider_uri=provider_uri, region=REG_CN) diff --git a/examples/workflow_by_code_lstm.py b/examples/workflow_by_code_lstm.py index 1815d2fec..2b07f6925 100644 --- a/examples/workflow_by_code_lstm.py +++ b/examples/workflow_by_code_lstm.py @@ -30,7 +30,7 @@ if __name__ == "__main__": sys.path.append(str(Path(__file__).resolve().parent.parent.joinpath("scripts"))) from get_data import GetData - GetData().qlib_data_cn(target_dir=provider_uri) + GetData().qlib_data(target_dir=provider_uri, region="cn") qlib.init(provider_uri=provider_uri, region=REG_CN) From 27b573c7d690c2c628c4d8ad3ae7a7bd53791004 Mon Sep 17 00:00:00 2001 From: Jactus Date: Mon, 23 Nov 2020 15:10:14 +0800 Subject: [PATCH 077/241] Update run_all_model script --- .github/workflows/test.yml | 2 +- .../{GBDT => LightGBM}/requirements.txt | 0 .../workflow_config_lightgbm.yaml} | 0 examples/run_all_model.py | 66 ++++++++++++++----- examples/workflow_by_code.ipynb | 2 +- examples/workflow_by_code.py | 2 +- examples/workflow_by_code_finetune.py | 2 +- examples/workflow_by_code_gats.py | 2 +- examples/workflow_by_code_gru.py | 2 +- examples/workflow_by_code_lstm.py | 2 +- qlib/workflow/cli.py | 1 - 11 files changed, 57 insertions(+), 24 deletions(-) rename examples/benchmarks/{GBDT => LightGBM}/requirements.txt (100%) rename examples/benchmarks/{GBDT/workflow_config_gbdt.yaml => LightGBM/workflow_config_lightgbm.yaml} (100%) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 935d03116..033d31536 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -56,4 +56,4 @@ jobs: - name: Test workflow by config run: | - qrun examples/benchmarks/GBDT/workflow_config_gbdt.yaml + qrun examples/benchmarks/LightGBM/workflow_config_lightgbm.yaml diff --git a/examples/benchmarks/GBDT/requirements.txt b/examples/benchmarks/LightGBM/requirements.txt similarity index 100% rename from examples/benchmarks/GBDT/requirements.txt rename to examples/benchmarks/LightGBM/requirements.txt diff --git a/examples/benchmarks/GBDT/workflow_config_gbdt.yaml b/examples/benchmarks/LightGBM/workflow_config_lightgbm.yaml similarity index 100% rename from examples/benchmarks/GBDT/workflow_config_gbdt.yaml rename to examples/benchmarks/LightGBM/workflow_config_lightgbm.yaml diff --git a/examples/run_all_model.py b/examples/run_all_model.py index 0b7e1dbbe..f8894afd3 100644 --- a/examples/run_all_model.py +++ b/examples/run_all_model.py @@ -3,10 +3,12 @@ import os import sys +import fire import venv import glob import shutil import tempfile +import statistics from pathlib import Path from subprocess import Popen, PIPE from threading import Thread @@ -18,9 +20,16 @@ import qlib from qlib.config import REG_CN from qlib.workflow import R from qlib.workflow.cli import workflow +from qlib.utils import exists_qlib_data # init qlib provider_uri = "~/.qlib/qlib_data/cn_data" +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) @@ -152,6 +161,18 @@ class ExtendedEnvBuilder(venv.EnvBuilder): self.install_script(context, "pip", url) +# function to calculate the mean and std of a list in the results dictionary +def cal_mean_std(results) -> dict: + mean_std = dict() + for fn in results: + mean_std[fn] = dict() + for metric in results[fn]: + mean = statistics.mean(results[fn][metric]) if len(results[fn][metric]) > 1 else results[fn][metric][0] + std = statistics.stdev(results[fn][metric]) if len(results[fn][metric]) > 1 else 0 + mean_std[fn][metric] = [mean, std] + return mean_std + + # function to get all the folders benchmark folder def get_all_folders() -> dict: folders = dict() @@ -175,21 +196,29 @@ def get_all_results(folders) -> dict: for fn in folders: exp = R.get_exp(experiment_name=fn, create=False) recorders = exp.list_recorders() - recorder = R.get_recorder(recorder_id=next(iter(recorders)), experiment_name=fn) - metrics = recorder.list_metrics() - results[fn] = {key: metrics[key] for key in metrics if "with_cost" in key} + result = dict() + result["annualized_return_with_cost"] = list() + result["information_ratio_with_cost"] = list() + result["max_drawdown_with_cost"] = list() + for recorder_id in recorders: + recorder = R.get_recorder(recorder_id=recorder_id, experiment_name=fn) + metrics = recorder.list_metrics() + result["annualized_return_with_cost"].append(metrics["excess_return_with_cost.annualized_return"]) + result["information_ratio_with_cost"].append(metrics["excess_return_with_cost.information_ratio"]) + result["max_drawdown_with_cost"].append(metrics["excess_return_with_cost.max_drawdown"]) + results[fn] = result return results -# function to generate and save markdown tables -def gen_and_save_md_table(results): +# function to generate and save markdown table +def gen_and_save_md_table(metrics): table = "| Model Name | Annualized Return | Information Ratio | Max Drawdown |\n" table += "|---|---|---|---|\n" - for fn in results: - ar = metrics[fn]["excess_return_with_cost.annualized_return"] - ir = metrics[fn]["excess_return_with_cost.information_ratio"] - md = metrics[fn]["excess_return_with_cost.max_drawdown"] - table += f"| {fn} | {ar:9.5f} | {ir:9.5f} | {md:9.5f} |\n" + for fn in metrics: + ar = metrics[fn]["annualized_return_with_cost"] + ir = metrics[fn]["information_ratio_with_cost"] + md = metrics[fn]["max_drawdown_with_cost"] + table += f"| {fn} | {ar[0]:9.4f}±{ar[1]:9.2f} | {ir[0]:9.4f}±{ir[1]:9.2f}| {md[0]:9.4f}±{md[1]:9.2f} |\n" pprint(table) with open("table.md", "w") as f: f.write(table) @@ -197,7 +226,7 @@ def gen_and_save_md_table(results): # function to run the all the models -def run(): +def run(times=1): """ Please be aware that this function can only work under Linux. MacOS and Windows will be supported in the future. Any PR to enhance this method is highly welcomed. @@ -225,6 +254,7 @@ def run(): nopip=False, verbose=False, ) + # run all the model for iterations for fn in folders: # create env temp_dir = tempfile.mkdtemp() @@ -246,16 +276,20 @@ def run(): os.system(f"{python_path} -m pip install --upgrade cython") # TODO: FIX ME! os.system(f"{python_path} -m pip install -e git+https://github.com/you-n-g/qlib#egg=pyqlib") # TODO: FIX ME! sys.stderr.write("\n") - # run workflow_by_config - sys.stderr.write(f"Running the model: {fn}...\n") - os.system(f"{python_path} {env_path / 'src/pyqlib/qlib/workflow/cli.py'} {yaml_path} {fn}") - sys.stderr.write("\n") + # run workflow_by_config for multiple times + for i in range(times): + sys.stderr.write(f"Running the model: {fn} for iteration {i+1}...\n") + os.system(f"{python_path} {env_path / 'src/pyqlib/qlib/workflow/cli.py'} {yaml_path} {fn}") + sys.stderr.write("\n") # remove env sys.stderr.write(f"Deleting the environment: {env_path}...\n") shutil.rmtree(env_path) # getting all results sys.stderr.write(f"Retrieving results...\n") results = get_all_results(folders) + # calculating the mean and std + sys.stderr.write(f"Calculating the mean and std of results...\n") + results = cal_mean_std(results) # generating md table sys.stderr.write(f"Generating markdown table...\n") gen_and_save_md_table(results) @@ -264,7 +298,7 @@ def run(): if __name__ == "__main__": rc = 1 try: - run() # run all the model + fire.Fire(run) # run all the model rc = 0 except Exception as e: print("Error: %s" % e, file=sys.stderr) diff --git a/examples/workflow_by_code.ipynb b/examples/workflow_by_code.ipynb index 1ac9ab17c..1b4183b29 100644 --- a/examples/workflow_by_code.ipynb +++ b/examples/workflow_by_code.ipynb @@ -49,7 +49,7 @@ " print(f\"Qlib data is not found in {provider_uri}\")\n", " sys.path.append(str(Path.cwd().parent.joinpath(\"scripts\")))\n", " from get_data import GetData\n", - " GetData().qlib_data(target_dir=provider_uri, region=\"cn\")\n", + " GetData().qlib_data(target_dir=provider_uri, region=REG_CN)\n", "qlib.init(provider_uri=provider_uri, region=REG_CN)" ] }, diff --git a/examples/workflow_by_code.py b/examples/workflow_by_code.py index 8d495e05e..8fdb4332f 100644 --- a/examples/workflow_by_code.py +++ b/examples/workflow_by_code.py @@ -28,7 +28,7 @@ if __name__ == "__main__": sys.path.append(str(Path(__file__).resolve().parent.parent.joinpath("scripts"))) from get_data import GetData - GetData().qlib_data(target_dir=provider_uri, region="cn") + GetData().qlib_data(target_dir=provider_uri, region=REG_CN) qlib.init(provider_uri=provider_uri, region=REG_CN) diff --git a/examples/workflow_by_code_finetune.py b/examples/workflow_by_code_finetune.py index 209cb4a1e..5e7c179ae 100644 --- a/examples/workflow_by_code_finetune.py +++ b/examples/workflow_by_code_finetune.py @@ -28,7 +28,7 @@ if __name__ == "__main__": sys.path.append(str(Path(__file__).resolve().parent.parent.joinpath("scripts"))) from get_data import GetData - GetData().qlib_data(target_dir=provider_uri, region="cn") + GetData().qlib_data(target_dir=provider_uri, region=REG_CN) qlib.init(provider_uri=provider_uri, region=REG_CN) diff --git a/examples/workflow_by_code_gats.py b/examples/workflow_by_code_gats.py index 6d44bd1b6..6b15b77b4 100644 --- a/examples/workflow_by_code_gats.py +++ b/examples/workflow_by_code_gats.py @@ -30,7 +30,7 @@ if __name__ == "__main__": sys.path.append(str(Path(__file__).resolve().parent.parent.joinpath("scripts"))) from get_data import GetData - GetData().qlib_data(target_dir=provider_uri, region="cn") + GetData().qlib_data(target_dir=provider_uri, region=REG_CN) qlib.init(provider_uri=provider_uri, region=REG_CN) diff --git a/examples/workflow_by_code_gru.py b/examples/workflow_by_code_gru.py index 96e461ba8..fdd0d9220 100644 --- a/examples/workflow_by_code_gru.py +++ b/examples/workflow_by_code_gru.py @@ -30,7 +30,7 @@ if __name__ == "__main__": sys.path.append(str(Path(__file__).resolve().parent.parent.joinpath("scripts"))) from get_data import GetData - GetData().qlib_data(target_dir=provider_uri, region="cn") + GetData().qlib_data(target_dir=provider_uri, region=REG_CN) qlib.init(provider_uri=provider_uri, region=REG_CN) diff --git a/examples/workflow_by_code_lstm.py b/examples/workflow_by_code_lstm.py index 2b07f6925..ee50c9aff 100644 --- a/examples/workflow_by_code_lstm.py +++ b/examples/workflow_by_code_lstm.py @@ -30,7 +30,7 @@ if __name__ == "__main__": sys.path.append(str(Path(__file__).resolve().parent.parent.joinpath("scripts"))) from get_data import GetData - GetData().qlib_data(target_dir=provider_uri, region="cn") + GetData().qlib_data(target_dir=provider_uri, region=REG_CN) qlib.init(provider_uri=provider_uri, region=REG_CN) diff --git a/qlib/workflow/cli.py b/qlib/workflow/cli.py index 307a466a1..a946af9a7 100644 --- a/qlib/workflow/cli.py +++ b/qlib/workflow/cli.py @@ -8,7 +8,6 @@ import qlib import fire import pandas as pd import ruamel.yaml as yaml -from qlib.config import REG_CN from qlib.utils import init_instance_by_config, flatten_dict from qlib.workflow import R from qlib.workflow.record_temp import SignalRecord From 71ba15c0978b7dfb36cb7799f0dd53e7263973c1 Mon Sep 17 00:00:00 2001 From: Wendi Li Date: Mon, 2 Nov 2020 19:24:42 +0800 Subject: [PATCH 078/241] Update cache.py --- qlib/data/cache.py | 1 + 1 file changed, 1 insertion(+) diff --git a/qlib/data/cache.py b/qlib/data/cache.py index 3cfb8dae9..2b9783fe3 100644 --- a/qlib/data/cache.py +++ b/qlib/data/cache.py @@ -180,6 +180,7 @@ class CacheUtils(object): > select {C.redis_task_db} > del "lock:{repr(lock_name)[1:-1]}-wlock" > quit + If the issue is not resolved, use "keys *" to find if multiple keys exist. If so, try using "flushall" to clear all the keys. """ ) From e3a5d1fc50725b462c19d2ae0750b31198f9cf52 Mon Sep 17 00:00:00 2001 From: Wendi Li Date: Mon, 2 Nov 2020 19:19:49 +0800 Subject: [PATCH 079/241] Update FAQ.rst --- docs/FAQ/FAQ.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/FAQ/FAQ.rst b/docs/FAQ/FAQ.rst index 8ea5e9f82..1cf895123 100644 --- a/docs/FAQ/FAQ.rst +++ b/docs/FAQ/FAQ.rst @@ -62,6 +62,7 @@ It sees the key of the redis lock has existed in your redis db now. You can use > select 1 > flushdb +If the issue is not resolved, use ``key *`` to find if multiple keys exist. If so, try using ``flushall`` to clear all the keys. .. note:: From f536241a5f6f82111fea5c19ccaa5585cdb60f22 Mon Sep 17 00:00:00 2001 From: Wendi Li Date: Mon, 2 Nov 2020 19:26:13 +0800 Subject: [PATCH 080/241] Update FAQ.rst --- docs/FAQ/FAQ.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/FAQ/FAQ.rst b/docs/FAQ/FAQ.rst index 1cf895123..ba6f77b47 100644 --- a/docs/FAQ/FAQ.rst +++ b/docs/FAQ/FAQ.rst @@ -62,7 +62,7 @@ It sees the key of the redis lock has existed in your redis db now. You can use > select 1 > flushdb -If the issue is not resolved, use ``key *`` to find if multiple keys exist. If so, try using ``flushall`` to clear all the keys. +If the issue is not resolved, use ``keys *`` to find if multiple keys exist. If so, try using ``flushall`` to clear all the keys. .. note:: From 6ded0d50c728b99cd3c0c15992efd8ce4179bc59 Mon Sep 17 00:00:00 2001 From: Dong Zhou Date: Tue, 24 Nov 2020 10:04:13 +0800 Subject: [PATCH 081/241] support long-short analysis --- qlib/contrib/eva/alpha.py | 52 ++++++++++++++++++++++++++++++++++-- qlib/workflow/record_temp.py | 29 +++++++++++++++++--- 2 files changed, 75 insertions(+), 6 deletions(-) diff --git a/qlib/contrib/eva/alpha.py b/qlib/contrib/eva/alpha.py index 3ef70091d..e00fbfe25 100644 --- a/qlib/contrib/eva/alpha.py +++ b/qlib/contrib/eva/alpha.py @@ -5,8 +5,12 @@ The interface should be redesigned carefully in the future. """ import pandas as pd +from typing import Tuple -def calc_ic(pred: pd.Series, label: pd.Series, date_col="datetime", dropna=False) -> (pd.Series, pd.Series): + +def calc_ic( + pred: pd.Series, label: pd.Series, date_col="datetime", dropna=False +) -> Tuple[pd.Series, pd.Series]: """calc_ic. Parameters @@ -25,8 +29,52 @@ def calc_ic(pred: pd.Series, label: pd.Series, date_col="datetime", dropna=False """ df = pd.DataFrame({"pred": pred, "label": label}) ic = df.groupby(date_col).apply(lambda df: df["pred"].corr(df["label"])) - ric = df.groupby(date_col).apply(lambda df: df["pred"].corr(df["label"], method="spearman")) + ric = df.groupby(date_col).apply( + lambda df: df["pred"].corr(df["label"], method="spearman") + ) if dropna: return ic.dropna(), ric.dropna() else: return ic, ric + + +def calc_long_short_return( + pred: pd.Series, + label: pd.Series, + date_col: str = "datetime", + quantile: float = 0.2, + dropna: bool = False, +) -> Tuple[pd.Series, pd.Series]: + """ + calculate long-short return + + Note: + `label` must be raw stock returns. + + Parameters + ---------- + pred : pd.Series + stock predictions + label : pd.Series + stock returns + date_col : str + datetime index name + quantile : float + long-short quantile + + Returns + ---------- + long_short_r : pd.Series + daily long-short returns + long_avg_r : pd.Series + daily long-average returns + """ + df = pd.DataFrame({"pred": pred, "label": label}) + if dropna: + df.dropna(inplace=True) + group = df.groupby(level=date_col) + N = lambda x: int(len(x) * quantile) + r_long = group.apply(lambda x: x.nlargest(N(x), columns="pred").label.mean()) + r_short = group.apply(lambda x: x.nsmallest(N(x), columns="pred").label.mean()) + r_avg = group.label.mean() + return (r_long - r_short) / 2, r_avg diff --git a/qlib/workflow/record_temp.py b/qlib/workflow/record_temp.py index b1fd9cc83..ffb339278 100644 --- a/qlib/workflow/record_temp.py +++ b/qlib/workflow/record_temp.py @@ -14,7 +14,7 @@ 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 ..contrib.eva.alpha import calc_ic +from ..contrib.eva.alpha import calc_ic, calc_long_short_return logger = get_module_logger("workflow", "INFO") @@ -148,7 +148,9 @@ class SigAnaRecord(SignalRecord): artifact_path = "sig_analysis" - def __init__(self, recorder, **kwargs): + def __init__(self, recorder, ana_long_short=False, ann_scaler=252, **kwargs): + self.ana_long_short = ana_long_short + self.ann_scaler = ann_scaler super().__init__(recorder=recorder, **kwargs) # The name must be unique. Otherwise it will be overridden @@ -164,12 +166,31 @@ class SigAnaRecord(SignalRecord): "Rank IC": ric.mean(), "Rank ICIR": ric.mean() / ric.std(), } + objects = { + 'ic.pkl': ic, + 'ric.pkl': ric + } + if self.ana_long_short: + long_short_r, long_avg_r = calc_long_short_return(pred.iloc[:, 0], label.iloc[:, 0]) + metrics.update({ + 'Long-Short Ann Return': long_short_r.mean() * self.ann_scaler, + 'Long-Short Ann Sharpe': long_short_r.mean() / long_short_r.std() * self.ann_scaler ** 0.5, + 'Long-Avg Ann Return': long_avg_r.mean() * self.ann_scaler, + 'Long-Avg Ann Sharpe': long_avg_r.mean() / long_avg_r.std() * self.ann_scaler ** 0.5, + }) + objects.update({ + 'long_short_r.pkl': long_short_r, + 'long_avg_r.pkl': long_avg_r, + }) self.recorder.log_metrics(**metrics) - self.recorder.save_objects(**{"ic.pkl": ic, "ric.pkl": ric}, artifact_path=self.get_path()) + self.recorder.save_objects(**objects, artifact_path=self.get_path()) pprint(metrics) def list(self): - return [self.get_path("ic.pkl"), self.get_path("ric.pkl")] + paths = [self.get_path("ic.pkl"), self.get_path("ric.pkl")] + if self.ana_long_short: + paths.extend([self.get_path('long_short_r.pkl'), self.get_path('long_avg_r.pkl')]) + return paths class PortAnaRecord(SignalRecord): From 0112d06ed5de7a09c08bf9467b4c0847b4487366 Mon Sep 17 00:00:00 2001 From: Young Date: Tue, 24 Nov 2020 02:05:05 +0000 Subject: [PATCH 082/241] rename group -> fields_group --- qlib/__init__.py | 3 ++- qlib/contrib/data/handler.py | 4 ++-- qlib/data/dataset/processor.py | 10 +++++----- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/qlib/__init__.py b/qlib/__init__.py index ed6ae1543..3fecc85c3 100644 --- a/qlib/__init__.py +++ b/qlib/__init__.py @@ -182,7 +182,7 @@ def _mount_nfs_uri(C): LOG.warning(f"{_remote_uri} on {_mount_path} is already mounted") -def init_from_yaml_conf(conf_path): +def init_from_yaml_conf(conf_path, **kwargs): """init_from_yaml_conf :param conf_path: A path to the qlib config in yml format @@ -190,5 +190,6 @@ def init_from_yaml_conf(conf_path): with open(conf_path) as f: config = yaml.load(f, Loader=yaml.FullLoader) + config.update(kwargs) default_conf = config.pop("default_conf", "client") init(default_conf, **config) diff --git a/qlib/contrib/data/handler.py b/qlib/contrib/data/handler.py index 99a601b9e..8cce92907 100644 --- a/qlib/contrib/data/handler.py +++ b/qlib/contrib/data/handler.py @@ -23,7 +23,7 @@ class ALPHA360_Denoise(DataHandlerLP): } learn_processors = [ - {"class": "DropnaLabel", "kwargs": {"group": "label"}}, + {"class": "DropnaLabel", "kwargs": {"fields_group": "label"}}, {"class": "CSZScoreNorm", "kwargs": {"fields_group": "label"}}, ] infer_processors = [ @@ -96,7 +96,7 @@ class ALPHA360(DataHandlerLP): } learn_processors = [ - {"class": "DropnaLabel", "kwargs": {"group": "label"}}, + {"class": "DropnaLabel", "kwargs": {"fields_group": "label"}}, {"class": "CSZScoreNorm", "kwargs": {"fields_group": "label"}}, ] infer_processors = [ diff --git a/qlib/data/dataset/processor.py b/qlib/data/dataset/processor.py index 3970c8a0a..5944db40b 100755 --- a/qlib/data/dataset/processor.py +++ b/qlib/data/dataset/processor.py @@ -74,16 +74,16 @@ class Processor(Serializable): class DropnaProcessor(Processor): - def __init__(self, group=None): - self.group = group + def __init__(self, fields_group=None): + self.fields_group = fields_group def __call__(self, df): - return df.dropna(subset=get_group_columns(df, self.group)) + return df.dropna(subset=get_group_columns(df, self.fields_group)) class DropnaLabel(DropnaProcessor): - def __init__(self, group="label"): - super().__init__(group=group) + def __init__(self, fields_group="label"): + super().__init__(fields_group=fields_group) def is_for_infer(self) -> bool: """The samples are dropped according to label. So it is not usable for inference""" From 5729b2242e6fa71072b1945bd30f27acc588bbd4 Mon Sep 17 00:00:00 2001 From: Dong Zhou Date: Tue, 24 Nov 2020 10:08:52 +0800 Subject: [PATCH 083/241] dataloader support static file or dataframe --- qlib/data/dataset/handler.py | 4 +-- qlib/data/dataset/loader.py | 65 +++++++++++++++++++++++++++++++++++- qlib/utils/__init__.py | 14 ++++++++ 3 files changed, 80 insertions(+), 3 deletions(-) diff --git a/qlib/data/dataset/handler.py b/qlib/data/dataset/handler.py index d1dbe1777..0a2fed637 100644 --- a/qlib/data/dataset/handler.py +++ b/qlib/data/dataset/handler.py @@ -51,7 +51,7 @@ class DataHandler(Serializable): def __init__( self, - instruments, + instruments=None, start_time=None, end_time=None, data_loader: Tuple[dict, str, DataLoader] = None, @@ -242,7 +242,7 @@ class DataHandlerLP(DataHandler): def __init__( self, - instruments, + instruments=None, start_time=None, end_time=None, data_loader: Tuple[dict, str, DataLoader] = None, diff --git a/qlib/data/dataset/loader.py b/qlib/data/dataset/loader.py index 564a7e5d5..7e8dd507c 100644 --- a/qlib/data/dataset/loader.py +++ b/qlib/data/dataset/loader.py @@ -1,13 +1,16 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. +import os import abc import warnings +import numpy as np import pandas as pd -from typing import Tuple +from typing import Tuple, Union from qlib.data import D +from qlib.utils import load_dataset class DataLoader(abc.ABC): @@ -139,6 +142,9 @@ class QlibDataLoader(DLWParser): super().__init__(config) def load_group_df(self, instruments, exprs: list, names: list, start_time=None, end_time=None) -> pd.DataFrame: + if instruments is None: + warnings.warn('`instruments` is not set, will load all stocks') + instruments = 'all' if isinstance(instruments, str): instruments = D.instruments(instruments, filter_pipe=self.filter_pipe) elif self.filter_pipe is not None: @@ -148,3 +154,60 @@ class QlibDataLoader(DLWParser): df.columns = names df = df.swaplevel().sort_index() # NOTE: always return return df + + +class StaticDataLoader(DataLoader): + """ + DataLoader that supports loading data from file or as provided. + """ + + def __init__(self, feature_path_or_obj: Union[str, pd.DataFrame], label_path_or_obj: Union[str, pd.DataFrame] = None): + """ + Parameters + ---------- + feature_path_or_obj : str or pd.DataFrame + file path or pandas object for feature + label_path_or_obj : str or pd.DataFrame + file path or pandas object for label + """ + if isinstance(feature_path_or_obj, str): + assert os.path.exists(feature_path_or_obj), f"cannot find feature `{feature_path_or_obj}" + else: + assert isinstance(feature_path_or_obj, pd.DataFrame), f"need to be dataframe" + self._feature_path_or_obj = feature_path_or_obj + + if isinstance(label_path_or_obj, str): + assert os.path.exists(label_path_or_obj), f"cannot find label `{label_path_or_obj}" + elif label_path_or_obj is not None: + assert isinstance(label_path_or_obj, pd.DataFrame), f"need to be dataframe" + self._label_path_or_obj = label_path_or_obj + + self._data = None + + def load(self, instruments=None, start_time=None, end_time=None) -> pd.DataFrame: + self._maybe_load_raw_data() + if instruments is None: + df = self._data + else: + df = self._data.loc(axis=0)[:, instruments] + if start_time is None and end_time is None: + return df # NOTE: avoid copy by loc + return df.loc[pd.Timestamp(start_time):pd.Timestamp(end_time)] + + def _maybe_load_raw_data(self): + if self._data is not None: + return + self._data = load_dataset(self._feature_path_or_obj) + if self._label_path_or_obj is not None: + self._data = pd.concat( + {"feature": self._data, "label": load_dataset(self._label_path_or_obj)}, axis=1 + ) + if not isinstance(self._data.columns, pd.MultiIndex): + self._data.columns = pd.MultiIndex.from_arrays( + [ + np.array(["feature", "label"])[ + self._data.columns.str.contains("^LABEL").astype(int) + ], + self._data.columns, + ] + ) diff --git a/qlib/utils/__init__.py b/qlib/utils/__init__.py index 79fd6fe5c..c77c67fa2 100644 --- a/qlib/utils/__init__.py +++ b/qlib/utils/__init__.py @@ -695,3 +695,17 @@ def register_wrapper(wrapper, cls_or_obj, module_path=None): cls_or_obj = getattr(module, cls_or_obj) obj = cls_or_obj() if isinstance(cls_or_obj, type) else cls_or_obj wrapper.register(obj) + + +def load_dataset(path_or_obj): + """load dataset from multiple file formats""" + if isinstance(path_or_obj, pd.DataFrame): + return path_or_obj + _, extension = os.path.splitext(path_or_obj) + if extension == '.h5': + return pd.read_hdf(path_or_obj) + elif extension == '.pkl': + return pd.read_pickle(path_or_obj) + elif extension == '.csv': + return pd.read_csv(path_or_obj, parse_dates=True, index_col=[0, 1]) + raise ValueError(f'unsupported file type `{extension}`') From 7f9f54faf40d3d270876220d2caadbb1dc4bab20 Mon Sep 17 00:00:00 2001 From: Dong Zhou Date: Tue, 24 Nov 2020 10:09:27 +0800 Subject: [PATCH 084/241] add CSRankNorm processor --- qlib/data/dataset/processor.py | 34 ++++++++++++++++++++++++---------- 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/qlib/data/dataset/processor.py b/qlib/data/dataset/processor.py index 3970c8a0a..1e8442866 100755 --- a/qlib/data/dataset/processor.py +++ b/qlib/data/dataset/processor.py @@ -124,18 +124,19 @@ class ProcessInf(Processor): class Fillna(Processor): - """Process infinity """ + """Process NaN""" + + def __init__(self, fields_group=None, fill_value=0): + self.fields_group = fields_group + self.fill_value = fill_value def __call__(self, df): - def fill_na(df, columns=None, fill=0): - - if columns == None: - columns = df.columns - df[columns] = df[columns].fillna(fill) - - return df - - return fill_na(df) + if self.fields_group is None: + df.fillna(self.fill_value, inplace=True) + else: + cols = get_group_columns(df, self.fields_group) + df.fillna({col: self.fill_value for col in cols}, inplace=True) + return df class MinMaxNorm(Processor): @@ -203,3 +204,16 @@ class CSZScoreNorm(Processor): cols = get_group_columns(df, self.fields_group) df[cols] = df[cols].groupby("datetime").apply(lambda df: (df - df.mean()).div(df.std())) return df + + +class CSRankNorm(Processor): + """Cross Sectional Rank Normalization""" + + def __init__(self, fields_group=None): + self.fields_group = fields_group + + def __call__(self, df): + # try not modify original dataframe + cols = get_group_columns(df, self.fields_group) + df[cols] = df[cols].groupby("datetime").apply(lambda df: (df - df.mean()).div(df.std())) + return df From e2485bcf2b5edf71e301bac13766f329f048ec08 Mon Sep 17 00:00:00 2001 From: Dong Zhou Date: Tue, 24 Nov 2020 10:20:29 +0800 Subject: [PATCH 085/241] add CSRankNorm impl --- qlib/data/dataset/processor.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/qlib/data/dataset/processor.py b/qlib/data/dataset/processor.py index 9c45b8bcd..2201c0891 100755 --- a/qlib/data/dataset/processor.py +++ b/qlib/data/dataset/processor.py @@ -215,5 +215,8 @@ class CSRankNorm(Processor): def __call__(self, df): # try not modify original dataframe cols = get_group_columns(df, self.fields_group) - df[cols] = df[cols].groupby("datetime").apply(lambda df: (df - df.mean()).div(df.std())) + t = df[cols].groupby("datetime").rank(pct=True) + t -= 0.5 + t *= 3.46 # NOTE: towards unit std + df[cols] = t return df From acae9087e9609781a5d80071b8511dd7d50d8d09 Mon Sep 17 00:00:00 2001 From: Jactus Date: Tue, 24 Nov 2020 16:41:10 +0800 Subject: [PATCH 086/241] Update model configs --- docs/component/estimator.rst | 706 ------------------ .../CatBoost/workflow_config_catboost.yaml | 5 + .../benchmarks/DNN/workflow_config_dnn.yaml | 5 + .../benchmarks/GATs/worflow_config_gats.yaml | 5 + .../benchmarks/GRU/workflow_config_gru.yaml | 5 + .../benchmarks/LSTM/workflow_config_lstm.yaml | 5 + .../LightGBM/workflow_config_lightgbm.yaml | 5 + .../XGBoost/workflow_config_xgboost.yaml | 5 + 8 files changed, 35 insertions(+), 706 deletions(-) delete mode 100644 docs/component/estimator.rst diff --git a/docs/component/estimator.rst b/docs/component/estimator.rst deleted file mode 100644 index df59b75b9..000000000 --- a/docs/component/estimator.rst +++ /dev/null @@ -1,706 +0,0 @@ -.. _estimator: -================================= -Estimator: Workflow Management -================================= -.. currentmodule:: qlib - -Introduction -=================== - -The components in `Qlib Framework <../introduction/introduction.html#framework>`_ are designed in a loosely-coupled way. Users could build their own Quant research workflow with these components like `Example `_ - - -Besides, ``Qlib`` provides more user-friendly interfaces named ``Estimator`` to automatically run the whole workflow defined by configuration. A concrete execution of the whole workflow is called an `experiment`. -With ``Estimator``, user can easily run an `experiment`, which includes the following steps: - -- Data - - Loading - - Processing - - Slicing -- Model - - Training and inference(static or rolling) - - Saving & loading -- Evaluation(Back-testing) - -For each `experiment`, ``Qlib`` will capture the model training details, performance evaluation results and basic information (e.g. names, ids). The captured data will be stored in backend-storage (disk or database). - -Complete Example -=================== - -Before getting into details, here is a complete example of ``Estimator``, which defines the workflow in typical Quant research. -Below is a typical config file of ``Estimator``. - -.. code-block:: YAML - - experiment: - name: estimator_example - observer_type: file_storage - mode: train - model: - class: LGBModel - module_path: qlib.contrib.model.gbdt - args: - 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 - data: - class: Alpha158 - args: - dropna_label: True - filter: - market: csi500 - trainer: - class: StaticTrainer - args: - rolling_period: 360 - train_start_date: 2007-01-01 - train_end_date: 2014-12-31 - validate_start_date: 2015-01-01 - validate_end_date: 2016-12-31 - test_start_date: 2017-01-01 - test_end_date: 2020-08-01 - strategy: - class: TopkDropoutStrategy - args: - topk: 50 - n_drop: 5 - backtest: - normal_backtest_args: - verbose: False - limit_threshold: 0.095 - account: 100000000 - benchmark: SH000905 - deal_price: close - open_cost: 0.0005 - close_cost: 0.0015 - min_cost: 5 - qlib_data: - # when testing, please modify the following parameters according to the specific environment - provider_uri: "~/.qlib/qlib_data/cn_data" - region: "cn" - -After saving the config into `configuration.yaml`, users could start the workflow and test their ideas with a single command below. - -.. code-block:: bash - - estimator -c configuration.yaml - -.. note:: `estimator` will be placed in your $PATH directory when installing ``Qlib``. - - - -Configuration File -=================== - -Let's get into details of ``Estimator`` in this section. - -Before using ``estimator``, users need to prepare a configuration file. The following content shows how to prepare each part of the configuration file. - -Experiment Section --------------------- - -At first, the configuration file needs to contain a section named `experiment` about the basic information. This section describes how `estimator` tracks and persists current `experiment`. ``Qlib`` used `sacred`, a lightweight open-source tool, to configure, organize, generate logs, and manage experiment results. Partial behaviors of `sacred` will base on the `experiment` section. - -Following files will be saved by `sacred` after `estimator` finish an `experiment`: - -- `model.bin`, model binary file -- `pred.pkl`, model prediction result file -- `analysis.pkl`, backtest performance analysis file -- `positions.pkl`, backtest position records file -- `run`, the experiment information object, usually contains some meta information such as the experiment name, experiment date, etc. - -Here is the typical configuration of `experiment section` - -.. code-block:: YAML - - experiment: - name: test_experiment - observer_type: mongo - mongo_url: mongodb://MONGO_URL - db_name: public - finetune: false - exp_info_path: /home/test_user/exp_info.json - mode: test - loader: - id: 677 - - -The meaning of each field is as follows: - -- `name` - The experiment name, str type, `sacred _` will use this experiment name as an identifier for some important internal processes. Users can find this field in `run` object of `sacred`. The default value is `test_experiment`. - -- `observer_type` - Observer type, str type, there are two choices which include `file_storage` and `mongo` respectively. If `file_storage` is selected, all the above-mentioned managed contents will be stored in the `dir` directory, separated by the number of times of experiments as a subfolder. If it is `mongo`, the content will be stored in the database. The default is `file_storage`. - - - For `file_storage` observer. - - `dir` - Directory URL, str type, directory for `file_storage` observer type, files captured and managed by sacred with `file_storage` observer will be saved to this directory, which is the same directory as `config.json` by default. - - - For `mongo` observer. - - `mongo_url` - Database URL, str type, required if the observer type is `mongo`. - - - `db_name` - Database name, str type, required if the observer type is `mongo`. - -- `finetune` - ``Estimator``'s behaviors to train models will base on this flag. - If you just want to train models from scratch each time instead of based on existing models, please leave `finetune=false`. Otherwise please read the - details below. - - The following table is the processing logic for different situations. - - ========== =========================================== ==================================== =========================================== ========================================== - . Static Rolling - . finetune:true finetune:false finetune:true finetune:false - ========== =========================================== ==================================== =========================================== ========================================== - Train - Need to provide model (Static or Rolling) - No need to provide model - Need to provide model (Static or Rolling) - Need to provide model (Static or Rolling) - - The args in model section will be - The args in model section will be - The args in model section will be - The args in model section will be - used for finetuning used for training used for finetuning used for finetuning - - Update based on the provided model - Train model from scratch - Update based on the provided model - Based on the provided model update - and parameters and parameters - Train model from scratch - - **Each rolling time slice is based on** - **Train each rolling time slice** - **a model updated from the previous** **separately** - **time** - Test - Model must exist, otherwise an exception will be raised. - - For `StaticTrainer`, users need to train a model and record 'exp_info' for 'Test'. - - For `RollingTrainer`, users need to train a set of models until the latest time, and record 'exp_info' for 'Test'. - ========== ============================================================================================================================================================================= - - .. note:: - - 1. finetune parameters: share model.args parameters. - - 2. provide model: from `loader.model_index`, load the index of the model(starting from 0). - - 3. If `loader.model_index` is None: - - In 'Static Finetune=True', if provide 'Rolling', use the last model to update. - - - For `RollingTrainer` with Finetune=True. - - - If `StaticTrainer` is used in loader, the model will be used for initialization for finetuning. - - - If `RollingTrainer` is used in loader, the existing models will be used without any modification and the new models will be initialized with the model in the last period and finetune one by one. - - -- `exp_info_path` - save path of experiment info, str type, save the experiment info and model `prediction score` after the experiment is finished. Optional parameter, the default value is `/ex_name/exp_info.json`. - -- `mode` - `train` or `test`, str type. - - `test mode` is designed for inference. Under `test mode`, it will load the model according to the parameters of `loader` and skip model training. - - `train model` is the default value. It will train new models by default and - Please note that when it fails to load model, it will fall back to `fit` model. - - .. note:: - - if users choose ` test mode`, they need to make sure: - - The loader of `test_start_date` must be less than or equal to the current `test_start_date`. - - If other parameters of the `loader` model args are different, a warning will appear. - - -- `loader` - If you just want to train models from scratch each time instead of based on existing models, please ignore `loader` section. Otherwise please read the - details below. - - The `loader` section only works when the `mode` is `test` or `finetune` is `true`. - - - `model_index` - Model index, int type. The index of the loaded model in loader_models (starting at 0) for the first `finetune`. The default value is None. - - - `exp_info_path` - Loader model experiment info path, str type. If the field exists, the following parameters will be parsed from `exp_info_path`, and the following parameters will not work. One of this field and `id` must exist at least . - - - `id` - The experiment id of the model that needs to be loaded, int type. If the `mode` is `test`, this value is required. This field and `exp_info_path` must exist one. - - - `name` - The experiment name of the model that needs to be loaded, str type. The default value is the current experiment `name`. - - - `observer_type` - The experiment observer type of the model that needs to be loaded, str type. The default value is the current experiment `observer_type`. - - .. note:: The observer type is a concept of the `sacred` module, which determines how files, standard input, and output which are managed by sacred are stored. - - - - `file_storage` - If `observer_type` is `file_storage`, the config may be as follows. - - .. code-block:: YAML - - experiment: - name: test_experiment - dir: # default is dir of `config.yml` - observer_type: file_storage - - `mongo` - If `observer_type` is `mongo`, the config may be as follows. - - .. code-block:: YAML - - experiment: - name: test_experiment - observer_type: mongo - mongo_url: mongodb://MONGO_URL - db_name: public - - Users need to indicate `mongo_url` and `db_name` for a mongo observer. - - .. note:: - - If users choose the mongo observer, they need to make sure: - - Have an environment with the mongodb installed and a mongo database dedicated to storing the results of the experiments. - - The python environment (the version of python and package) to run the experiments and the one to fetch the results are consistent. - -Model Section ------------------ - -Users can use a specified model by configuration with hyper-parameters. - -Custom Models -~~~~~~~~~~~~~~~~~ - -Qlib supports custom models, but it must be a subclass of the `qlib.model.Model`, the config for a custom model may be as following. - -.. code-block:: YAML - - model: - class: SomeModel - module_path: /tmp/my_experment/custom_model.py - args: - loss: binary - - -The class `SomeModel` should be in the module `custom_model`, and ``Qlib`` could parse the `module_path` to load the class. - -To know more about ``Interday Model``, please refer to `Interday Model: Training & Prediction `_. - -Data Section ------------------ - -``Data Handler`` can be used to load raw data, prepare features and label columns, preprocess data (standardization, remove NaN, etc.), split training, validation, and test sets. It is a subclass of `qlib.data.dataset.handler.DataHandlerLP`. - -Users can use the specified data handler by config as follows. - -.. code-block:: YAML - - data: - class: Alpha158 - args: - start_date: 2005-01-01 - end_date: 2018-04-30 - dropna_label: True - filter: - market: csi500 - filter_pipeline: - - - class: NameDFilter - module_path: qlib.filter - args: - name_rule_re: S(?!Z3) - fstart_time: 2018-01-01 - fend_time: 2018-12-11 - - - class: ExpressionDFilter - module_path: qlib.filter - args: - rule_expression: $open/$factor<=45 - fstart_time: 2018-01-01 - fend_time: 2018-12-11 - -- `class` - Data handler class, str type, which should be a subclass of `qlib.data.dataset.handler.DataHandlerLP`, and implements 5 important interfaces for loading features, loading raw data, preprocessing raw data, slicing train, validation, and test data. The default value is `ALPHA360`. If users want to write a data handler to retrieve the data in ``Qlib``, `QlibDataHandler` is suggested. - -- `module_path` - The module path, str type, absolute url is also supported, indicates the path of the `class` implementation of the data processor class. The default value is `qlib.data.dataset.handler`. - -- `args` - Parameters used for ``Data Handler`` initialization. - - - `train_start_date` - Training start time, str type, the default value is `2005-01-01`. - - - `start_date` - Data start date, str type. - - - `end_date` - Data end date, str type. the data from start_date to end_date decides which part of data will be loaded in `datahandler`, users can only use these data in the following parts. - - - `dropna_feature` (Optional in args) - Drop Nan feature, bool type, the default value is False. - - - `dropna_label` (Optional in args) - Drop Nan label, bool type, the default value is True. Some multi-label tasks will use this. - - - `normalize_method` (Optional in args) - Normalize data by a given method. str type. ``Qlib`` gives two normalizing methods, `MinMax` and `Std`. - If users want to build their own method, please override `_process_normalize_feature`. - -- `filter` - Dynamically filtering the stocks based on the filter pipeline. - - - `market` - index name, str type, the default value is `csi500`. - - - `filter_pipeline` - Filter rule list, list type, the default value is []. Can be customized according to users' needs. - - - `class` - Filter class name, str type. - - - `module_path` - The module path, str type. - - - `args` - The filter class parameters, these parameters are set according to the `class`, and all the parameters as kwargs to `class`. - -Custom Data Handler -~~~~~~~~~~~~~~~~~~~~~~ - -Qlib support custom data handler, but it must be a subclass of the ``qlib.data.dataset.handler.DataHandlerLP``, the config for custom data handler may be as follows. - -.. code-block:: YAML - - data: - class: SomeDataHandler - module_path: /tmp/my_experment/custom_data_handler.py - args: - start_date: 2005-01-01 - end_date: 2018-04-30 - -The class `SomeDataHandler` should be in the module `custom_data_handler`, and ``Qlib`` could parse the `module_path` to load the class. - -If users want to load features and labels by config, they can inherit ``qlib.data.dataset.handler.ConfigDataHandler``, ``Qlib`` also has provided some preprocess methods in this subclass. -If users want to use qlib data, `QLibDataHandler` is recommended, from which users can inherit the custom class. `QLibDataHandler` is also a subclass of `ConfigDataHandler`. - -To know more about ``Data Handler``, please refer to `Data Framework&Usage `_. - -Trainer Section ------------------ - -Users can specify the trainer ``Trainer`` by the config file, which is a subclass of ``qlib.contrib.estimator.trainer.BaseTrainer`` and implement three important interfaces for training the model, restoring the model, and getting model predictions as follows. - -- `train` - Implement this interface to train the model. - -- `load` - Implement this interface to recover the model from disk. - -- `get_pred` - Implement this interface to get model prediction results. - -Qlib have provided two implemented trainer, - -- `StaticTrainer` - The static trainer will be trained using the training, validation, and test data of the data processor static slicing. - -- `RollingTrainer` - The rolling trainer will use the rolling iterator of the data processor to split data for rolling training. - - -Users can specify `trainer` with the configuration file: - -.. code-block:: YAML - - trainer: - class: StaticTrainer # or RollingTrainer - args: - rolling_period: 360 - train_start_date: 2005-01-01 - train_end_date: 2014-12-31 - validate_start_date: 2015-01-01 - validate_end_date: 2016-06-30 - test_start_date: 2016-07-01 - test_end_date: 2017-07-31 - -- `class` - Trainer class, which should be a subclass of `qlib.contrib.estimator.trainer.BaseTrainer`, and needs to implement three important interfaces, the default value is `StaticTrainer`. - -- `module_path` - The module path, str type, absolute url is also supported, indicates the path of the trainer class implementation. - -- `args` - Parameters used for ``Trainer`` initialization. - - - `rolling_period` - The rolling period, integer type, indicates how many time steps need rolling when rolling the data. The default value is `60`. Only used in `RollingTrainer`. - - - `train_start_date` - Training start time, str type. - - - `train_end_date` - Training end time, str type. - - - `validate_start_date` - Validation start time, str type. - - - `validate_end_date` - Validation end time, str type. - - - `test_start_date` - Test start time, str type. - - - `test_end_date` - Test end time, str type. If `test_end_date` is `-1` or greater than the last date of the data, the last date of the data will be used as `test_end_date`. - -Custom Trainer -~~~~~~~~~~~~~~~~~~ - -Qlib supports custom trainer, but it must be a subclass of the `qlib.contrib.estimator.trainer.BaseTrainer`, the config for a custom trainer may be as following: - -.. code-block:: YAML - - trainer: - class: SomeTrainer - module_path: /tmp/my_experment/custom_trainer.py - args: - train_start_date: 2005-01-01 - train_end_date: 2014-12-31 - validate_start_date: 2015-01-01 - validate_end_date: 2016-06-30 - test_start_date: 2016-07-01 - test_end_date: 2017-07-31 - - -The class `SomeTrainer` should be in the module `custom_trainer`, and ``Qlib`` could parse the `module_path` to load the class. - -Strategy Section ------------------ - -Users can specify strategy through a config file, for example: - -.. code-block:: YAML - - strategy : - class: TopkDropoutStrategy - args: - topk: 50 - n_drop: 5 - -- `class` - The strategy class, str type, should be a subclass of `qlib.contrib.strategy.strategy.BaseStrategy`. The default value is `TopkDropoutStrategy`. - -- `module_path` - The module location, str type, absolute url is also supported, and absolute path is also supported, indicates the location of the policy class implementation. - -- `args` - Parameters used for ``Trainer`` initialization. - - - `topk` - The number of stocks in the portfolio - - - `n_drop` - Number of stocks to be replaced in each trading date - -Custom Strategy -^^^^^^^^^^^^^^^^^^^ - -Qlib supports custom strategy, but it must be a subclass of the ``qlib.contrib.strategy.strategy.BaseStrategy``, the config for custom strategy may be as following: - - -.. code-block:: YAML - - strategy : - class: SomeStrategy - module_path: /tmp/my_experment/custom_strategy.py - -The class `SomeStrategy` should be in the module `custom_strategy`, and ``Qlib`` could parse the `module_path` to load the class. - -To know more about ``Strategy``, please refer to `Strategy `_. - -Backtest Section ------------------ - -Users can specify `backtest` through a config file, for example: - -.. code-block:: YAML - - backtest : - normal_backtest_args: - topk: 50 - benchmark: SH000905 - account: 500000 - deal_price: close - min_cost: 5 - subscribe_fields: - - $close - - $change - - $factor - -- `normal_backtest_args` - Normal backtest parameters. All the parameters in this section will be passed to the ``qlib.contrib.evaluate.backtest`` function in the form of `**kwargs`. - - - `benchmark` - Stock index symbol, str, or list type, the default value is `None`. - - .. note:: - - * If `benchmark` is None, it will use the average change of the day of all stocks in 'pred' as the 'bench'. - - * If `benchmark` is list, it will use the daily average change of the stock pool in the list as the 'bench'. - - * If `benchmark` is str, it will use the daily change as the 'bench'. - - - - `account` - Backtest initial cash, integer type. The `account` in `strategy` section is deprecated. It only works when `account` is not set in `backtest` section. It will be overridden by `account` in the `backtest` section. The default value is 1e9. - - - `deal_price` - Order transaction price field, str type, the default value is close. - - - `min_cost` - Min transaction cost, float type, the default value is 5. - - - `subscribe_fields` - Subscribe quote fields, array type, the default value is [`deal_price`, $close, $change, $factor]. - - -Qlib Data Section --------------------- - -The `qlib_data` field describes the parameters of qlib initialization. - -.. code-block:: YAML - - qlib_data: - # when testing, please modify the following parameters according to the specific environment - provider_uri: "~/.qlib/qlib_data/cn_data" - region: "cn" - -- `provider_uri` - Type: str. The URI of the Qlib data. For example, it could be the location where the data loaded by ``get_data.py`` are stored. -- `region` - - If `region` == "us", ``Qlib`` will be initialized in US-stock mode. - - If `region` == "cn", ``Qlib`` will be initialized in china-stock mode. -- `redis_host` - Type: str, optional parameter(default: "127.0.0.1"), host of `redis` - The lock and cache mechanism relies on redis. -- `redis_port` - Type: int, optional parameter(default: 6379), port of `redis` - - .. note:: - - The value of `region` should be aligned with the data stored in `provider_uri`. Currently, ``scripts/get_data.py`` only provides China stock market data. If users want to use the US stock market data, they should prepare their own US-stock data in `provider_uri` and switch to US-stock mode. - - .. note:: - - If Qlib fails to connect redis via `redis_host` and `redis_port`, cache mechanism will not be used! Please refer to `Cache `_ for details. - - -Please refer to `Initialization <../start/initialization.html>`_. - -Experiment Result -=================== - -Form of Experimental Result ----------------------------- -The result of the experiment is also the result of the ``Intraday Trading(Backtest)``, please refer to `Intraday Trading: Model&Strategy Testing `_. - - -Get Experiment Result ----------------------------- - -Base Class & Interface -~~~~~~~~~~~~~~~~~~~~~~~ - -Users can check the experiment results from file storage directly, or check the experiment results from the database, or get the experiment results through two interfaces of a base class `Fetcher` provided by ``Qlib``. - -The `Fetcher` provides the following interface - - `get_experiments(self, exp_name=None):` - The interface takes one parameters. The `exp_name` is the experiment name, the default is all experiments. Users can get the returned dictionary with a list of ids and test end date as follows. - - .. code-block:: JSON - - { - "ex_a": [ - { - "id": 1, - "test_end_date": "2017-01-01" - } - ], - "ex_b": [ - ... - ] - } - - - - `get_experiment(exp_name, exp_id, fields=None)` - The interface takes three parameters. The first parameter is the experiment name, the second parameter is the experiment id, and the third parameter is a list of fields. The default value of `fields` is None, which means all fields. - - - .. note:: - Currently supported fields: - ['model', 'analysis', 'positions', 'report_normal', 'pred', 'task_config', 'label'] - - Users can get the returned dictionary as follows. - - .. code-block:: JSON - - { - 'analysis': analysis_df, - 'pred': pred_df, - 'positions': positions_dic, - 'report_normal': report_normal_df, - } - -Implemented `Fetcher` s & Examples -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -``Qlib`` provides two implemented `Fetcher` s as follows. - -`FileFetcher` -^^^^^^^^^^^^^^^ - -The `FileFetcher` is a subclass of `Fetcher`, which could fetch files from `file_storage` observer. The following is an example: -.. code-block:: python - - >>> from qlib.contrib.estimator.fetcher import FileFetcher - >>> f = FileFetcher(experiments_dir=r'./') - >>> print(f.get_experiments()) - { - 'test_experiment': [ - { - 'id': '1', - 'config': ... - }, - { - 'id': '2', - 'config': ... - }, - { - 'id': '3', - 'config': ... - } - ] - } - >>> print(f.get_experiment('test_experiment', '1')) - risk - excess_return_without_cost mean 0.000605 - std 0.005481 - annualized_return 0.152373 - information_ratio 1.751319 - max_drawdown -0.059055 - excess_return_with_cost mean 0.000410 - std 0.005478 - annualized_return 0.103265 - information_ratio 1.187411 - max_drawdown -0.075024 - - - -`MongoFetcher` -^^^^^^^^^^^^^^^ - -The `FileFetcher` is a subclass of `Fetcher`, which could fetch files from `mongo` observer. Users should initialize the fetcher with `mongo_url`. The following is an example: - -.. code-block:: python - - >>> from qlib.contrib.estimator.fetcher import MongoFetcher - >>> f = MongoFetcher(mongo_url=..., db_name=...) - diff --git a/examples/benchmarks/CatBoost/workflow_config_catboost.yaml b/examples/benchmarks/CatBoost/workflow_config_catboost.yaml index d66418544..80229e22b 100644 --- a/examples/benchmarks/CatBoost/workflow_config_catboost.yaml +++ b/examples/benchmarks/CatBoost/workflow_config_catboost.yaml @@ -48,6 +48,11 @@ task: - class: SignalRecord module_path: qlib.workflow.record_temp kwargs: {} + - class: SigAnaRecord + module_path: qlib.workflow.record_temp + kwargs: + ana_long_short: False + ann_scaler: 252 - class: PortAnaRecord module_path: qlib.workflow.record_temp kwargs: diff --git a/examples/benchmarks/DNN/workflow_config_dnn.yaml b/examples/benchmarks/DNN/workflow_config_dnn.yaml index 8f785aa76..6dbd345dd 100644 --- a/examples/benchmarks/DNN/workflow_config_dnn.yaml +++ b/examples/benchmarks/DNN/workflow_config_dnn.yaml @@ -55,6 +55,11 @@ task: - class: SignalRecord module_path: qlib.workflow.record_temp kwargs: {} + - class: SigAnaRecord + module_path: qlib.workflow.record_temp + kwargs: + ana_long_short: False + ann_scaler: 252 - class: PortAnaRecord module_path: qlib.workflow.record_temp kwargs: diff --git a/examples/benchmarks/GATs/worflow_config_gats.yaml b/examples/benchmarks/GATs/worflow_config_gats.yaml index 382c14f01..84eeff4db 100644 --- a/examples/benchmarks/GATs/worflow_config_gats.yaml +++ b/examples/benchmarks/GATs/worflow_config_gats.yaml @@ -58,6 +58,11 @@ task: - class: SignalRecord module_path: qlib.workflow.record_temp kwargs: {} + - class: SigAnaRecord + module_path: qlib.workflow.record_temp + kwargs: + ana_long_short: False + ann_scaler: 252 - class: PortAnaRecord module_path: qlib.workflow.record_temp kwargs: diff --git a/examples/benchmarks/GRU/workflow_config_gru.yaml b/examples/benchmarks/GRU/workflow_config_gru.yaml index 4e9ebc670..e9e6224e6 100644 --- a/examples/benchmarks/GRU/workflow_config_gru.yaml +++ b/examples/benchmarks/GRU/workflow_config_gru.yaml @@ -57,6 +57,11 @@ task: - class: SignalRecord module_path: qlib.workflow.record_temp kwargs: {} + - class: SigAnaRecord + module_path: qlib.workflow.record_temp + kwargs: + ana_long_short: False + ann_scaler: 252 - class: PortAnaRecord module_path: qlib.workflow.record_temp kwargs: diff --git a/examples/benchmarks/LSTM/workflow_config_lstm.yaml b/examples/benchmarks/LSTM/workflow_config_lstm.yaml index b45f94569..354149dae 100644 --- a/examples/benchmarks/LSTM/workflow_config_lstm.yaml +++ b/examples/benchmarks/LSTM/workflow_config_lstm.yaml @@ -57,6 +57,11 @@ task: - class: SignalRecord module_path: qlib.workflow.record_temp kwargs: {} + - class: SigAnaRecord + module_path: qlib.workflow.record_temp + kwargs: + ana_long_short: False + ann_scaler: 252 - class: PortAnaRecord module_path: qlib.workflow.record_temp kwargs: diff --git a/examples/benchmarks/LightGBM/workflow_config_lightgbm.yaml b/examples/benchmarks/LightGBM/workflow_config_lightgbm.yaml index eeb9db7bd..790fc3ae5 100644 --- a/examples/benchmarks/LightGBM/workflow_config_lightgbm.yaml +++ b/examples/benchmarks/LightGBM/workflow_config_lightgbm.yaml @@ -54,6 +54,11 @@ task: - class: SignalRecord module_path: qlib.workflow.record_temp kwargs: {} + - class: SigAnaRecord + module_path: qlib.workflow.record_temp + kwargs: + ana_long_short: False + ann_scaler: 252 - class: PortAnaRecord module_path: qlib.workflow.record_temp kwargs: diff --git a/examples/benchmarks/XGBoost/workflow_config_xgboost.yaml b/examples/benchmarks/XGBoost/workflow_config_xgboost.yaml index fb88f1058..407d56fb7 100644 --- a/examples/benchmarks/XGBoost/workflow_config_xgboost.yaml +++ b/examples/benchmarks/XGBoost/workflow_config_xgboost.yaml @@ -57,6 +57,11 @@ task: - class: SignalRecord module_path: qlib.workflow.record_temp kwargs: {} + - class: SigAnaRecord + module_path: qlib.workflow.record_temp + kwargs: + ana_long_short: False + ann_scaler: 252 - class: PortAnaRecord module_path: qlib.workflow.record_temp kwargs: From 93ce9a4cb2fc805d311b4583aaf8ba6f7a68f006 Mon Sep 17 00:00:00 2001 From: Jactus Date: Tue, 24 Nov 2020 16:42:32 +0800 Subject: [PATCH 087/241] Format --- qlib/contrib/eva/alpha.py | 8 ++------ qlib/data/dataset/loader.py | 18 ++++++++---------- qlib/utils/__init__.py | 8 ++++---- qlib/workflow/record_temp.py | 31 ++++++++++++++++--------------- 4 files changed, 30 insertions(+), 35 deletions(-) diff --git a/qlib/contrib/eva/alpha.py b/qlib/contrib/eva/alpha.py index e00fbfe25..c68571853 100644 --- a/qlib/contrib/eva/alpha.py +++ b/qlib/contrib/eva/alpha.py @@ -8,9 +8,7 @@ import pandas as pd from typing import Tuple -def calc_ic( - pred: pd.Series, label: pd.Series, date_col="datetime", dropna=False -) -> Tuple[pd.Series, pd.Series]: +def calc_ic(pred: pd.Series, label: pd.Series, date_col="datetime", dropna=False) -> Tuple[pd.Series, pd.Series]: """calc_ic. Parameters @@ -29,9 +27,7 @@ def calc_ic( """ df = pd.DataFrame({"pred": pred, "label": label}) ic = df.groupby(date_col).apply(lambda df: df["pred"].corr(df["label"])) - ric = df.groupby(date_col).apply( - lambda df: df["pred"].corr(df["label"], method="spearman") - ) + ric = df.groupby(date_col).apply(lambda df: df["pred"].corr(df["label"], method="spearman")) if dropna: return ic.dropna(), ric.dropna() else: diff --git a/qlib/data/dataset/loader.py b/qlib/data/dataset/loader.py index 7e8dd507c..eddbca044 100644 --- a/qlib/data/dataset/loader.py +++ b/qlib/data/dataset/loader.py @@ -143,8 +143,8 @@ class QlibDataLoader(DLWParser): def load_group_df(self, instruments, exprs: list, names: list, start_time=None, end_time=None) -> pd.DataFrame: if instruments is None: - warnings.warn('`instruments` is not set, will load all stocks') - instruments = 'all' + warnings.warn("`instruments` is not set, will load all stocks") + instruments = "all" if isinstance(instruments, str): instruments = D.instruments(instruments, filter_pipe=self.filter_pipe) elif self.filter_pipe is not None: @@ -161,7 +161,9 @@ class StaticDataLoader(DataLoader): DataLoader that supports loading data from file or as provided. """ - def __init__(self, feature_path_or_obj: Union[str, pd.DataFrame], label_path_or_obj: Union[str, pd.DataFrame] = None): + def __init__( + self, feature_path_or_obj: Union[str, pd.DataFrame], label_path_or_obj: Union[str, pd.DataFrame] = None + ): """ Parameters ---------- @@ -192,22 +194,18 @@ class StaticDataLoader(DataLoader): df = self._data.loc(axis=0)[:, instruments] if start_time is None and end_time is None: return df # NOTE: avoid copy by loc - return df.loc[pd.Timestamp(start_time):pd.Timestamp(end_time)] + return df.loc[pd.Timestamp(start_time) : pd.Timestamp(end_time)] def _maybe_load_raw_data(self): if self._data is not None: return self._data = load_dataset(self._feature_path_or_obj) if self._label_path_or_obj is not None: - self._data = pd.concat( - {"feature": self._data, "label": load_dataset(self._label_path_or_obj)}, axis=1 - ) + self._data = pd.concat({"feature": self._data, "label": load_dataset(self._label_path_or_obj)}, axis=1) if not isinstance(self._data.columns, pd.MultiIndex): self._data.columns = pd.MultiIndex.from_arrays( [ - np.array(["feature", "label"])[ - self._data.columns.str.contains("^LABEL").astype(int) - ], + np.array(["feature", "label"])[self._data.columns.str.contains("^LABEL").astype(int)], self._data.columns, ] ) diff --git a/qlib/utils/__init__.py b/qlib/utils/__init__.py index c77c67fa2..8a1436799 100644 --- a/qlib/utils/__init__.py +++ b/qlib/utils/__init__.py @@ -702,10 +702,10 @@ def load_dataset(path_or_obj): if isinstance(path_or_obj, pd.DataFrame): return path_or_obj _, extension = os.path.splitext(path_or_obj) - if extension == '.h5': + if extension == ".h5": return pd.read_hdf(path_or_obj) - elif extension == '.pkl': + elif extension == ".pkl": return pd.read_pickle(path_or_obj) - elif extension == '.csv': + elif extension == ".csv": return pd.read_csv(path_or_obj, parse_dates=True, index_col=[0, 1]) - raise ValueError(f'unsupported file type `{extension}`') + raise ValueError(f"unsupported file type `{extension}`") diff --git a/qlib/workflow/record_temp.py b/qlib/workflow/record_temp.py index ffb339278..81b0022c5 100644 --- a/qlib/workflow/record_temp.py +++ b/qlib/workflow/record_temp.py @@ -166,22 +166,23 @@ class SigAnaRecord(SignalRecord): "Rank IC": ric.mean(), "Rank ICIR": ric.mean() / ric.std(), } - objects = { - 'ic.pkl': ic, - 'ric.pkl': ric - } + objects = {"ic.pkl": ic, "ric.pkl": ric} if self.ana_long_short: long_short_r, long_avg_r = calc_long_short_return(pred.iloc[:, 0], label.iloc[:, 0]) - metrics.update({ - 'Long-Short Ann Return': long_short_r.mean() * self.ann_scaler, - 'Long-Short Ann Sharpe': long_short_r.mean() / long_short_r.std() * self.ann_scaler ** 0.5, - 'Long-Avg Ann Return': long_avg_r.mean() * self.ann_scaler, - 'Long-Avg Ann Sharpe': long_avg_r.mean() / long_avg_r.std() * self.ann_scaler ** 0.5, - }) - objects.update({ - 'long_short_r.pkl': long_short_r, - 'long_avg_r.pkl': long_avg_r, - }) + metrics.update( + { + "Long-Short Ann Return": long_short_r.mean() * self.ann_scaler, + "Long-Short Ann Sharpe": long_short_r.mean() / long_short_r.std() * self.ann_scaler ** 0.5, + "Long-Avg Ann Return": long_avg_r.mean() * self.ann_scaler, + "Long-Avg Ann Sharpe": long_avg_r.mean() / long_avg_r.std() * self.ann_scaler ** 0.5, + } + ) + objects.update( + { + "long_short_r.pkl": long_short_r, + "long_avg_r.pkl": long_avg_r, + } + ) self.recorder.log_metrics(**metrics) self.recorder.save_objects(**objects, artifact_path=self.get_path()) pprint(metrics) @@ -189,7 +190,7 @@ class SigAnaRecord(SignalRecord): def list(self): paths = [self.get_path("ic.pkl"), self.get_path("ric.pkl")] if self.ana_long_short: - paths.extend([self.get_path('long_short_r.pkl'), self.get_path('long_avg_r.pkl')]) + paths.extend([self.get_path("long_short_r.pkl"), self.get_path("long_avg_r.pkl")]) return paths From e5e402197a423ebc058bcd5a4329999475b553ed Mon Sep 17 00:00:00 2001 From: Young Date: Tue, 24 Nov 2020 09:18:30 +0000 Subject: [PATCH 088/241] delay the creating of `mlruns` folder --- qlib/workflow/expm.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/qlib/workflow/expm.py b/qlib/workflow/expm.py index e1469746a..7af661a64 100644 --- a/qlib/workflow/expm.py +++ b/qlib/workflow/expm.py @@ -225,7 +225,13 @@ class MLflowExpManager(ExpManager): def __init__(self, uri, default_exp_name): super(MLflowExpManager, self).__init__(uri, default_exp_name) - self.client = mlflow.tracking.MlflowClient(tracking_uri=self.uri) + + @property + def client(self): + # Delay the creation of mlflow client in case of creating `mlruns` folder when importing qlib + if not hasattr(self, "_client"): + self._client = mlflow.tracking.MlflowClient(tracking_uri=self.uri) + return self._client def start_exp(self, experiment_name=None, recorder_name=None, uri=None): # create experiment From 73b280754df0d243224926ee838778d1218b0d09 Mon Sep 17 00:00:00 2001 From: Dong Zhou Date: Tue, 24 Nov 2020 20:50:42 +0800 Subject: [PATCH 089/241] auto infer cpu count --- qlib/config.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/qlib/config.py b/qlib/config.py index ac9c3ba65..640701ee5 100644 --- a/qlib/config.py +++ b/qlib/config.py @@ -15,6 +15,7 @@ import copy from pathlib import Path import re import os +import multiprocessing class Config: @@ -63,6 +64,8 @@ class Config: REG_CN = "cn" REG_US = "us" +NUM_USABLE_CPU = multiprocessing.cpu_count() - 2 + _default_config = { # data provider config "calendar_provider": "LocalCalendarProvider", @@ -79,7 +82,7 @@ _default_config = { "calendar_cache": None, # for simple dataset cache "local_cache_path": None, - "kernels": 16, + "kernels": NUM_USABLE_CPU, # How many tasks belong to one process. Recommend 1 for high-frequency data and None for daily data. "maxtasksperchild": None, "default_disk_cache": 1, # 0:skip/1:use @@ -151,7 +154,7 @@ MODE_CONF = { "redis_host": "127.0.0.1", "redis_port": 6379, "redis_task_db": 1, - "kernels": 64, + "kernels": NUM_USABLE_CPU, # cache "expression_cache": "DiskExpressionCache", "dataset_cache": "DiskDatasetCache", @@ -173,7 +176,7 @@ MODE_CONF = { "dataset_cache": "DiskDatasetCache", "calendar_cache": None, # client config - "kernels": 16, + "kernels": NUM_USABLE_CPU, "mount_path": None, "auto_mount": False, # The nfs is already mounted on our server[auto_mount: False]. # The nfs should be auto-mounted by qlib on other From e819879232cf02ec33638632fc3644ab621d2b81 Mon Sep 17 00:00:00 2001 From: Dong Zhou Date: Tue, 24 Nov 2020 21:03:52 +0800 Subject: [PATCH 090/241] fix model when using single feature --- qlib/contrib/model/catboost_model.py | 2 +- qlib/contrib/model/gbdt.py | 4 ++-- qlib/contrib/model/xgboost.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/qlib/contrib/model/catboost_model.py b/qlib/contrib/model/catboost_model.py index d53a6db41..e487a6d1e 100644 --- a/qlib/contrib/model/catboost_model.py +++ b/qlib/contrib/model/catboost_model.py @@ -61,7 +61,7 @@ class CatBoostModel(Model): if self.model is None: raise ValueError("model is not fitted yet!") x_test = dataset.prepare("test", col_set="feature") - return pd.Series(self.model.predict(np.squeeze(x_test.values)), index=x_test.index) + return pd.Series(self.model.predict(x_test.values), index=x_test.index) if __name__ == "__main__": diff --git a/qlib/contrib/model/gbdt.py b/qlib/contrib/model/gbdt.py index 58b76c355..995a02696 100644 --- a/qlib/contrib/model/gbdt.py +++ b/qlib/contrib/model/gbdt.py @@ -16,7 +16,7 @@ class LGBModel(ModelFT): def __init__(self, loss="mse", **kwargs): if loss not in {"mse", "binary"}: raise NotImplementedError - self.params = {"objective": loss} + self.params = {"objective": loss, 'verbosity': -1} self.params.update(kwargs) self.model = None @@ -65,7 +65,7 @@ class LGBModel(ModelFT): if self.model is None: raise ValueError("model is not fitted yet!") x_test = dataset.prepare("test", col_set="feature", data_key=DataHandlerLP.DK_I) - return pd.Series(self.model.predict(np.squeeze(x_test.values)), index=x_test.index) + return pd.Series(self.model.predict(x_test.values), index=x_test.index) def finetune(self, dataset: DatasetH, num_boost_round=10, verbose_eval=20): """ diff --git a/qlib/contrib/model/xgboost.py b/qlib/contrib/model/xgboost.py index f1208eb93..e0691ba16 100755 --- a/qlib/contrib/model/xgboost.py +++ b/qlib/contrib/model/xgboost.py @@ -61,4 +61,4 @@ class XGBModel(Model): if self.model is None: raise ValueError("model is not fitted yet!") x_test = dataset.prepare("test", col_set="feature") - return pd.Series(self.model.predict(xgb.DMatrix(np.squeeze(x_test.values))), index=x_test.index) + return pd.Series(self.model.predict(xgb.DMatrix(x_test.values)), index=x_test.index) From dfa8bc10a5b60d634a3c41124b3bf631ad2c7f08 Mon Sep 17 00:00:00 2001 From: Young Date: Tue, 24 Nov 2020 13:37:08 +0000 Subject: [PATCH 091/241] update mlflow version mlflow 1.11.0 will create a folder name `mlruns` when exist --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 7d7ea7fdb..2c9cfea95 100644 --- a/setup.py +++ b/setup.py @@ -50,7 +50,7 @@ REQUIRED = [ "matplotlib==3.1.3", "tables>=3.6.1", "pyyaml>=5.3.1", - "mlflow>=1.10.0", + "mlflow>=1.12.1", "tqdm", "loguru", "lightgbm", From db9758575b2c938f6e63220be9f518632afb4d28 Mon Sep 17 00:00:00 2001 From: Dong Zhou Date: Tue, 24 Nov 2020 22:43:34 +0800 Subject: [PATCH 092/241] static data loader supports fields_group --- qlib/data/dataset/loader.py | 40 ++++++++++--------------------------- qlib/utils/__init__.py | 2 ++ 2 files changed, 13 insertions(+), 29 deletions(-) diff --git a/qlib/data/dataset/loader.py b/qlib/data/dataset/loader.py index eddbca044..6d90907f4 100644 --- a/qlib/data/dataset/loader.py +++ b/qlib/data/dataset/loader.py @@ -161,29 +161,17 @@ class StaticDataLoader(DataLoader): DataLoader that supports loading data from file or as provided. """ - def __init__( - self, feature_path_or_obj: Union[str, pd.DataFrame], label_path_or_obj: Union[str, pd.DataFrame] = None - ): + def __init__(self, config: dict, join='outer'): """ Parameters ---------- - feature_path_or_obj : str or pd.DataFrame - file path or pandas object for feature - label_path_or_obj : str or pd.DataFrame - file path or pandas object for label + config : dict + {fields_group: } + join : str + How to align different dataframes """ - if isinstance(feature_path_or_obj, str): - assert os.path.exists(feature_path_or_obj), f"cannot find feature `{feature_path_or_obj}" - else: - assert isinstance(feature_path_or_obj, pd.DataFrame), f"need to be dataframe" - self._feature_path_or_obj = feature_path_or_obj - - if isinstance(label_path_or_obj, str): - assert os.path.exists(label_path_or_obj), f"cannot find label `{label_path_or_obj}" - elif label_path_or_obj is not None: - assert isinstance(label_path_or_obj, pd.DataFrame), f"need to be dataframe" - self._label_path_or_obj = label_path_or_obj - + self.config = config + self.join = join self._data = None def load(self, instruments=None, start_time=None, end_time=None) -> pd.DataFrame: @@ -199,13 +187,7 @@ class StaticDataLoader(DataLoader): def _maybe_load_raw_data(self): if self._data is not None: return - self._data = load_dataset(self._feature_path_or_obj) - if self._label_path_or_obj is not None: - self._data = pd.concat({"feature": self._data, "label": load_dataset(self._label_path_or_obj)}, axis=1) - if not isinstance(self._data.columns, pd.MultiIndex): - self._data.columns = pd.MultiIndex.from_arrays( - [ - np.array(["feature", "label"])[self._data.columns.str.contains("^LABEL").astype(int)], - self._data.columns, - ] - ) + self._data = pd.concat({ + fields_group: load_dataset(path_or_obj) + for fields_group, path_or_obj in self.config.items() + }, axis=1, join=self.join) diff --git a/qlib/utils/__init__.py b/qlib/utils/__init__.py index 8a1436799..14480b7b5 100644 --- a/qlib/utils/__init__.py +++ b/qlib/utils/__init__.py @@ -701,6 +701,8 @@ def load_dataset(path_or_obj): """load dataset from multiple file formats""" if isinstance(path_or_obj, pd.DataFrame): return path_or_obj + if not os.path.exists(path_or_obj): + raise ValueError(f'file {path_or_obj} doesn\'t exist') _, extension = os.path.splitext(path_or_obj) if extension == ".h5": return pd.read_hdf(path_or_obj) From 5059bba51ea7977d85887bb97f3b4cb10aa04cc5 Mon Sep 17 00:00:00 2001 From: Dong Zhou Date: Tue, 24 Nov 2020 22:52:19 +0800 Subject: [PATCH 093/241] fix static dataloader --- qlib/data/dataset/loader.py | 1 + 1 file changed, 1 insertion(+) diff --git a/qlib/data/dataset/loader.py b/qlib/data/dataset/loader.py index 6d90907f4..d7e262eb7 100644 --- a/qlib/data/dataset/loader.py +++ b/qlib/data/dataset/loader.py @@ -191,3 +191,4 @@ class StaticDataLoader(DataLoader): fields_group: load_dataset(path_or_obj) for fields_group, path_or_obj in self.config.items() }, axis=1, join=self.join) + self._data.sort_index(inplace=True) From b4671746c2e4cb7e469a3a7cbbd974d0e7bd2f73 Mon Sep 17 00:00:00 2001 From: Jactus Date: Tue, 24 Nov 2020 23:56:16 +0800 Subject: [PATCH 094/241] Update part of the docs --- README.md | 22 +- docs/advanced/alpha.rst | 1 + docs/advanced/server.rst | 1 + docs/component/backtest.rst | 1 + docs/component/data.rst | 1 + docs/component/model.rst | 1 + docs/component/recorder.rst | 409 ++++++++++++++++++++++++++++++ docs/component/report.rst | 1 + docs/component/strategy.rst | 1 + docs/component/workflow.rst | 279 ++++++++++++++++++++ docs/index.rst | 4 +- docs/introduction/quick.rst | 19 +- docs/reference/api.rst | 23 ++ docs/start/getdata.rst | 1 + docs/start/initialization.rst | 3 +- docs/start/installation.rst | 1 + qlib/contrib/evaluate.py | 16 +- qlib/contrib/model/gbdt.py | 2 +- qlib/contrib/strategy/strategy.py | 40 +-- qlib/data/cache.py | 5 +- qlib/data/data.py | 25 +- qlib/data/dataset/handler.py | 29 ++- qlib/data/dataset/loader.py | 11 +- qlib/model/base.py | 6 +- qlib/utils/__init__.py | 2 +- qlib/workflow/__init__.py | 6 +- qlib/workflow/exp.py | 46 ++-- qlib/workflow/expm.py | 43 ++-- qlib/workflow/record_temp.py | 14 +- requirements.txt | 2 +- 30 files changed, 902 insertions(+), 113 deletions(-) create mode 100644 docs/component/recorder.rst create mode 100644 docs/component/workflow.rst diff --git a/README.md b/README.md index 5ff9b624b..b06afd975 100644 --- a/README.md +++ b/README.md @@ -128,14 +128,14 @@ Users could create the same dataset with it. --> ## Auto Quant Research Workflow -Qlib provides a tool named `Estimator` to run the whole workflow automatically (including building dataset, training models, backtest and evaluation). You can start an auto quant research workflow and have a graphical reports analysis according to the following steps: +Qlib provides a tool named `qrun` to run the whole workflow automatically (including building dataset, training models, backtest and evaluation). You can start an auto quant research workflow and have a graphical reports analysis according to the following steps: -1. Quant Research Workflow: Run `Estimator` with [estimator_config.yaml](examples/estimator/estimator_config.yaml) as following. (*Please note that this may **not work** under MacOS with Python 3.8 due to the incompatibility of the `sacred` package we use with Python 3.8. We will fix this bug in the future.*) +1. Quant Research Workflow: Run `qrun` with lightgbm workflow config ([workflow_config_lightgbm.yaml](examples/benchmarks/LightGBM/workflow_config_lightgbm.yaml)) as following. ```bash cd examples # Avoid running program under the directory contains `qlib` - estimator -c estimator/estimator_config.yaml + qrun benchmarks/LightGBM/workflow_config_lightgbm.yaml ``` - The result of `Estimator` is as follows, please refer to please refer to [Intraday Trading](https://qlib.readthedocs.io/en/latest/component/backtest.html) for more details about the result. + The result of `qrun` is as follows, please refer to please refer to [Intraday Trading](https://qlib.readthedocs.io/en/latest/component/backtest.html) for more details about the result. ```bash @@ -154,9 +154,9 @@ Qlib provides a tool named `Estimator` to run the whole workflow automatically ( ``` - Here are detailed documents for [Estimator](https://qlib.readthedocs.io/en/latest/component/estimator.html). + Here are detailed documents for `qrun` and [workflow](https://qlib.readthedocs.io/en/latest/component/workflow.html). -2. Graphical Reports Analysis: Run `examples/estimator/analyze_from_estimator.ipynb` with `jupyter notebook` to get graphical reports +2. Graphical Reports Analysis: Run `examples/workflow_by_code.ipynb` with `jupyter notebook` to get graphical reports - Forecasting signal (model prediction) analysis - Cumulative Return of groups ![Cumulative Return](http://fintech.msra.cn/images/analysis/analysis_model_cumulative_return.png?v=0.1) @@ -184,14 +184,20 @@ Qlib provides a tool named `Estimator` to run the whole workflow automatically ( --> ## Building Customized Quant Research Workflow by Code -The automatic workflow may not suite the research workflow of all Quant researchers. To support a flexible Quant research workflow, Qlib also provides a modularized interface to allow researchers to build their own workflow by code. [Here](examples/train_backtest_analyze.ipynb) is a demo for customized Quant research workflow by code +The automatic workflow may not suite the research workflow of all Quant researchers. To support a flexible Quant research workflow, Qlib also provides a modularized interface to allow researchers to build their own workflow by code. [Here](examples/workflow_by_code.ipynb) is a demo for customized Quant research workflow by code. # Quant Model Zoo Here is a list of models built on `Qlib`. -- [GBDT based on lightgbm](qlib/contrib/model/gbdt.py) +- [GBDT based on LightGBM](qlib/contrib/model/gbdt.py) +- [GBDT based on Catboost](qlib/contrib/model/catboost_model.py) +- [GBDT based on XGBoost](qlib/contrib/model/xgboost.py) - [MLP based on pytorch](qlib/contrib/model/pytorch_nn.py) +- [GRU based on pytorch](qlib/contrib/model/pytorch_gru.py) +- [LSTM based on pytorcn](qlib/contrib/model/pytorch_lstm.py) +- [GATs based on pytorch](qlib/contrib/model/pytorch_gats.py) +- [TFT based on tensorflow-1.15.0](examples/benchmarks/TFT/tft.py) Your PR of new Quant models is highly welcomed. diff --git a/docs/advanced/alpha.rst b/docs/advanced/alpha.rst index bba6c3980..e6146dd0c 100644 --- a/docs/advanced/alpha.rst +++ b/docs/advanced/alpha.rst @@ -1,4 +1,5 @@ .. _alpha: + =========================== Building Formulaic Alphas =========================== diff --git a/docs/advanced/server.rst b/docs/advanced/server.rst index 230c4f04b..a8a764b91 100644 --- a/docs/advanced/server.rst +++ b/docs/advanced/server.rst @@ -1,4 +1,5 @@ .. _server: + ================================= ``Online`` & ``Offline`` mode ================================= diff --git a/docs/component/backtest.rst b/docs/component/backtest.rst index fd4ac19fa..d36dba316 100644 --- a/docs/component/backtest.rst +++ b/docs/component/backtest.rst @@ -1,4 +1,5 @@ .. _backtest: + ============================================ Intraday Trading: Model&Strategy Testing ============================================ diff --git a/docs/component/data.rst b/docs/component/data.rst index ba4cc0053..9ef71a6cb 100644 --- a/docs/component/data.rst +++ b/docs/component/data.rst @@ -1,4 +1,5 @@ .. _data: + ================================ Data Layer: Data Framework&Usage ================================ diff --git a/docs/component/model.rst b/docs/component/model.rst index 52cda79e7..6a6b02f86 100644 --- a/docs/component/model.rst +++ b/docs/component/model.rst @@ -1,4 +1,5 @@ .. _model: + ============================================ Interday Model: Model Training & Prediction ============================================ diff --git a/docs/component/recorder.rst b/docs/component/recorder.rst new file mode 100644 index 000000000..efd67e859 --- /dev/null +++ b/docs/component/recorder.rst @@ -0,0 +1,409 @@ +.. _recorder: + +==================================== +Qlib Recorder: Experiment Management +==================================== +.. currentmodule:: qlib + +Introduction +=================== +``Qlib`` contains an experiment management system named ``QlibRecorder``, which is designed to help users handle experiment and analysis results in an efficient way. + +There are three components of the system: + +- `ExperimentManager` + a class that manages experiments. + +- `Experiment` + a class of experiment, and each instance of it is responsible for a single experiment. + +- `Recorder` + a class of recorder, and each instance of it is responsible for a single run. + +Here is a general view of the structure of the system: + +.. code-block:: + + ExperimentManager + - Experiment 1 + - Recorder 1 + - Recorder 2 + - ... + - Experiment 2 + - Recorder 1 + - Recorder 2 + - ... + - ... + +Currently, the components of this experiment management system are implemented using the machine learning platform: ``MLFlow`` (`link `_). + + +Qlib Recorder +=================== +``QlibRecorder`` provides a high level API for users to use the experiment management system. The interfaces are wrapped in the variable ``R`` in ``Qlib``, and users can directly use ``R`` to interact with the system. The following command shows how to import ``R`` in Python: + +.. code-block:: Python + + from qlib.workflow import R + +``QlibRecorder`` includes several common API for managing `experiments` and `recorders` within a workflow. For more available APIs, please refer to the following section about `Experiment Manager`, `Experiment` and `Recorder`. + +Here are the available interfaces of ``QlibRecorder``: + +- `__init__(exp_manager)` + - Initialization. + - It takes in an input: `exp_manager`, which is an `ExperimentManager` instance. The instance will be created during ``qlib.init``. + +- `start(experiment_name=None, recorder_name=None)` + - High level API to start an experiment. This method can only be called within a Python's '`with`' statement. + - Parameters: + - `experiment_name` : str + name of the experiment one wants to start. + - `recorder_name` : str + name of the recorder under the experiment one wants to start. + - Use case: + + .. code-block:: Python + + with R.start('test', 'recorder_1'): + model.fit(dataset) + R.log... + ... # further operations + +- `start_exp(experiment_name=None, recorder_name=None, uri=None)` + - Lower level method for starting an experiment. When use this method, one should end the experiment manually and the status of the recorder may not be handled properly. + - Parameters: + - `experiment_name` : str + the name of the experiment to be started + - `recorder_name` : str + name of the recorder under the experiment one wants to start. + - `uri` : str + the tracking uri of the experiment, where all the artifacts/metrics etc. will be stored. + The default uri are set in the qlib.config. + - Returns: + - an experiment instance being started. + - Use case: + + .. code-block:: Python + + R.start_exp(experiment_name='test', recorder_name='recorder_1') + ... # further operations + R.end_exp('FINISHED') or R.end_exp(Recorder.STATUS_S) + +- `end_exp(recorder_status=Recorder.STATUS_FI)` + - Method for ending an experiment manually. It will end the current active experiment, as well as its active recorder with the specified `status` type. + - Parameters: + - `status` : str + The status of a recorder, which can be '`SCHEDULED`', '`RUNNING`', '`FINISHED`', '`FAILED`'. + - Use case: + + .. code-block:: Python + + R.start_exp(experiment_name='test') + ... # further operations + R.end_exp('FINISHED') or R.end_exp(Recorder.STATUS_S) + +- `search_records(experiment_ids, **kwargs)` + - Get a pandas DataFrame of all the records that have been stored with the given search criteria. This method is highly correlated with MLFlow's ``search_runs`` method (`link `_). + - Parameters: + - `experiment_ids` : list + list of experiment IDs. + - `filter_string` : str + filter query string, defaults to searching all runs. + - `run_view_type` : int + one of enum values ACTIVE_ONLY (1), DELETED_ONLY (2), or ALL (3). + - `max_results` : int + the maximum number of runs to put in the dataframe. + - `order_by` : list + list of columns to order by (e.g., “metrics.rmse”). + - Returns: + - A pandas.DataFrame of records, where each metric, parameter, and tag are expanded into their own columns named metrics.*, params.*, and tags.* respectively. For records that don't have a particular metric, parameter, or tag, their value will be (NumPy) Nan, None, or None respectively. + - Use case: + + .. code-block:: Python + + R.log_metrics(m=2.50, step=0) + records = R.search_runs([experiment_id], order_by=["metrics.m DESC"]) + +- `list_experiments()` + - Method for listing all the existing experiments (except for those being deleted.) + - Returns: + - A dictionary (name -> experiment) of experiments information that being stored. + - Use case: + + .. code-block:: Python + + exps = R.list_experiments() + +- `list_recorders(experiment_id=None, experiment_name=None)` + - Method for listing all the recorders of experiment with given id or name. If user doesn't provide the id or name of the experiment, this method will try to retrieve the default experiment and list all the recorders of the default experiment. If the default experiment doesn't exist, the method will first create the default experiment, and then create a new recorder under it. + - Parameters: + - `experiment_id` : str + id of the experiment. + - `experiment_name` : str + name of the experiment. + - Returns: + - A dictionary (id -> recorder) of recorder information that being stored. + - Use case: + + .. code-block:: Python + + recorders = R.list_recorders(experiment_name='test') + +- `get_exp(experiment_id=None, experiment_name=None, create: bool = True)` + - Method for retrieving an experiment with given id or name. Once the '`create`' argument is set to True, if no valid experiment is found, this method will create one for the user. Otherwise, it will only retrieve a specific experiment or raise an Error. + + - If '`create`' is True: + - If ``R``'s running: + - no id or name specified, return the active experiment. + - if id or name is specified, return the specified experiment. If no such exp found, create a new experiment with given id or name, and the experiment is set to be running. + - If ``R``'s not running: + - no id or name specified, create a default experiment, and the experiment is set to be running. + - if id or name is specified, return the specified experiment. If no such exp found, create a new experiment with given name or the default experiment, and the experiment is set to be running. + - Else If '`create`' is False: + - If ``R``'s running: + - no id or name specified, return the active experiment. + - if id or name is specified, return the specified experiment. If no such exp found, raise Error. + - If ``R``'s not running: + - no id or name specified. If the default experiment exists, return it, otherwise, raise Error. + - if id or name is specified, return the specified experiment. If no such exp found, raise Error. + - Parameters: + - `experiment_id` : str + id of the experiment. + - `experiment_name` : str + name of the experiment. + - `create` : boolean + an argument determines whether the method will automatically create a new experiment according to user's specification if the experiment hasn't been created before. + - Returns: + - An experiment instance with given id or name. + - Use case: + + .. code-block:: Python + + # Case 1 + with R.start('test'): + exp = R.get_exp() + recorders = exp.list_recorders() + + # Case 2 + with R.start('test'): + exp = R.get_exp('test1') + + # Case 3 + exp = R.get_exp() -> a default experiment. + + # Case 4 + exp = R.get_exp(experiment_name='test') + + # Case 5 + exp = R.get_exp(create=False) -> the default experiment if exists. + +- `delete_exp(experiment_id=None, experiment_name=None)` + - Method for deleting the experiment with given id or name. At least one of id or name must be given, otherwise, error will occur. + - Parameters: + - `experiment_id` : str + id of the experiment. + - `experiment_name` : str + name of the experiment. + - Use case: + + .. code-block:: Python + + R.delete_exp(experiment_name='test') + +- `get_uri()` + - Method for retrieving the uri of current experiment manager. + - Returns: + - The uri of current experiment manager. + - Use case: + + .. code-block:: Python + + uri = R.get_uri() + +- `get_recorder(recorder_id=None, recorder_name=None, experiment_name=None)` + - Method for retrieving a recorder. The recorder can be used for further process such as ``save_objects``, ``load_object``, ``log_params``, ``log_metrics``, etc. + + - If ``R``'s running: + - no id or name specified, return the active recorder. + - if id or name is specified, return the specified recorder. + - If ``R``'s not running: + - no id or name specified, raise Error. + - if id or name is specified, and the corresponding experiment_name must be given, return the specified recorder. Otherwise, raise Error. + - Parameters: + - `recorder_id` : str + id of the recorder. + - `recorder_name` : str + name of the recorder. + - `experiment_name` : str + name of the experiment. + - Returns: + - A recorder instance. + - Use case: + + .. code-block:: Python + + # Case 1 + with R.start('test'): + recorder = R.get_recorder() + + # Case 2 + with R.start('test'): + recorder = R.get_recorder(recorder_id='2e7a4efd66574fa49039e00ffaefa99d') + + # Case 3 + recorder = R.get_recorder() -> Error + + # Case 4 + recorder = R.get_recorder(recorder_id='2e7a4efd66574fa49039e00ffaefa99d') -> Error + + # Case 5 + recorder = R.get_recorder(recorder_id='2e7a4efd66574fa49039e00ffaefa99d', experiment_name='test') + +- `delete_recorder(recorder_id=None, recorder_name=None)` + - Method for deleting the recorders with given id or name. At least one of id or name must be given, otherwise, error will occur. + - Parameters: + - `recorder_id` : str + id of the experiment. + - `recorder_name` : str + name of the experiment. + - Use case: + + .. code-block:: Python + + R.delete_recorder(recorder_id='2e7a4efd66574fa49039e00ffaefa99d') + +- `save_objects(local_path=None, artifact_path=None, **kwargs)` + - Method for saving objects as artifacts in the experiment to the uri. It supports either saving from a local file/directory, or directly saving objects. User can use valid python's keywords arguments to specify the object to be saved as well as its name (name: value). + + - If R's running: it will save the objects through the running recorder. + - If R's not running: the system will create a default experiment, and a new recorder and save objects under it. + + .. note:: + + If one wants to save objects with a specific recorder. It is recommended to first get the specific recorder through `get_recorder` API and use the recorder the save objects. The supported arguments are the same as this method. + + - Parameters: + - `local_path` : str + if provided, them save the file or directory to the artifact URI. + - `artifact_path` : str + the relative path for the artifact to be stored in the URI. + - Use case: + + .. code-block:: Python + + # Case 1 + with R.start('test'): + pred = model.predict(dataset) + R.save_objects(**{"pred.pkl": pred}, artifact_path='prediction') + + # Case 2 + with R.start('test'): + R.save_objects(local_path='results/pred.pkl') + +- `log_params(**kwargs)` + - Method for logging parameters during an experiment. In addition to using ``R``, one can also log to a specific recorder after getting it with `get_recorder` API. + + - If R's running: it will log parameters through the running recorder. + - If R's not running: the system will create a default experiment as well as a new recorder, and log parameters under it. + - Parameters: + - `keyword argument`: + name1=value1, name2=value2, ... + - Use case: + + .. code-block:: Python + + # Case 1 + with R.start('test'): + R.log_params(learning_rate=0.01) + + # Case 2 + R.log_params(learning_rate=0.01) + +- `log_metrics(step=None, **kwargs)` + - Method for logging metrics during an experiment. In addition to using ``R``, one can also log to a specific recorder after getting it with `get_recorder` API. + + - If R's running: it will log metrics through the running recorder. + - If R's not running: the system will create a default experiment as well as a new recorder, and log metrics under it. + - Parameters: + - `step`: int + a single integer step at which to log the specified Metrics. If unspecified, each metric is logged at step zero. + - `keyword argument`: + name1=value1, name2=value2, ... + +- `set_tags(**kwargs)` + - Method for setting tags for a recorder. In addition to using ``R``, one can also set the tag to a specific recorder after getting it with `get_recorder` API. + + - If R's running: it will set tags through the running recorder. + - If R's not running: the system will create a default experiment as well as a new recorder, and set the tags under it. + - Parameters: + - `keyword argument`: + name1=value1, name2=value2, ... + - Use case: + + .. code-block:: Python + + # Case 1 + with R.start('test'): + R.set_tags(release_version="2.2.0") + + # Case 2 + R.set_tags(release_version="2.2.0") + + +Experiment Manager +=================== + +The ``ExpManager`` module in ``Qlib`` is responsible for managing different experiments. Most of the APIs of ``ExpManager`` are similar to ``QlibRecorder``, and the most important API will be the ``get_exp`` method. User can directly refer to the documents above for some detailed information about how to use the ``get_exp`` method. + +For other interfaces such as `create_exp`, `delete_exp`, please refer to `Experiment Manager API <../reference/api.html#experiment-manager>`_. + +Experiment +=================== + +The ``Experiment`` class is solely responsible for a single experiment, and it will handle any operations that are related to an experiment. Basic methods such as `start`, `end` an experiment are included. Besides, methods related to `recorders` are also available: such methods include `get_recorder` and `list_recorders`. + +For other interfaces such as `search_records`, `delete_recorder`, please refer to `Experiment API <../reference/api.html#experiment>`_. + +Recorder +=================== + +The ``Recorder`` class is responsible for a single recorder. It will handle some detailed operations such as ``log_metrics``, ``log_params`` of a single run. It is designed to help user to easily track results and things being generated during a run. + +Here are some important APIs that are not included in the ``QlibRecorder``: + +- `list_artifacts(artifact_path: str = None)` + - List all the artifacts of a recorder. + - Parameters: + - `artifact_path` : str + the relative path for the artifact to be stored in the URI. + - Returns: + - A list of artifacts information (name, path, etc.) that being stored. + +- `list_metrics()` + - List all the metrics of a recorder. + - Returns: + - A dictionary of metrics that being stored. + +- `list_params()` + - List all the params of a recorder. + - Returns: + - A dictionary of params that being stored. + +- `list_tags()` + - List all the tags of a recorder. + - Returns: + - A dictionary of tags that being stored. + +For other interfaces such as `save_objects`, `load_object`, please refer to `Recorder API <../reference/api.html#recorder>`_. + +Record Template +=================== + +The ``RecordTemp`` class is a class that enables generate experiment results such as IC and backtest in a certain format. We have provided three different `Record Template` class: + +- ``SignalRecord``: This class generates the `preidction` of the model. +- ``SigAnaRecord``: This class generates the `IC`, `ICIR`, `Rank IC` and `Rank ICIR`. +- ``PortAnaRecord``: This class generates the results of `backtest`. The detailed information about `backtest` as well as the available `strategy`, users can refer to `Strategy <../component/strategy.html>`_ and `Backtest <../component/backtest.html>`_. + +For more information, please refer to `Record Template API <../reference/api.html#module-qlib.workflow.record_temp>`_. \ No newline at end of file diff --git a/docs/component/report.rst b/docs/component/report.rst index 81ebbd1f3..8ea3d7abe 100644 --- a/docs/component/report.rst +++ b/docs/component/report.rst @@ -1,4 +1,5 @@ .. _report: + ========================================== Aanalysis: Evaluation & Results Analysis ========================================== diff --git a/docs/component/strategy.rst b/docs/component/strategy.rst index c0ee687ce..0bdf453fe 100644 --- a/docs/component/strategy.rst +++ b/docs/component/strategy.rst @@ -1,4 +1,5 @@ .. _strategy: + ======================================== Interday Strategy: Portfolio Management ======================================== diff --git a/docs/component/workflow.rst b/docs/component/workflow.rst new file mode 100644 index 000000000..4ca010851 --- /dev/null +++ b/docs/component/workflow.rst @@ -0,0 +1,279 @@ +.. _workflow: + +================================= +Workflow: Workflow Management +================================= +.. currentmodule:: qlib + +Introduction +=================== + +The components in `Qlib Framework <../introduction/introduction.html#framework>`_ are designed in a loosely-coupled way. Users could build their own Quant research workflow with these components like `Example `_. + + +Besides, ``Qlib`` provides more user-friendly interfaces named ``qrun`` to automatically run the whole workflow defined by configuration. A concrete execution of the whole workflow is called an `experiment`. +With ``qrun``, user can easily run an `experiment`, which includes the following steps: + +- Data + - Loading + - Processing + - Slicing +- Model + - Training and inference (static or rolling) + - Saving & loading +- Evaluation + - Backtest + +For each `experiment`, ``Qlib`` has a complete system to tracking all the information as well as artifacts generated during training, inference and evaluation phase. For more information about how Qlib handles `experiment`, please refer to the related document: `Recorder: Experiment Management <../component/recorder.html>`_. + +Complete Example +=================== + +Before getting into details, here is a complete example of ``qrun``, which defines the workflow in typical Quant research. +Below is a typical config file of ``qrun``. + +.. code-block:: YAML + + provider_uri: "~/.qlib/qlib_data/cn_data" + region: cn + market: &market csi300 + benchmark: &benchmark SH000300 + data_handler_config: &data_handler_config + start_time: 2008-01-01 + end_time: 2020-08-01 + fit_start_time: 2008-01-01 + fit_end_time: 2014-12-31 + instruments: *market + port_analysis_config: &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 + 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: [2008-01-01, 2014-12-31] + valid: [2015-01-01, 2016-12-31] + test: [2017-01-01, 2020-08-01] + record: + - class: SignalRecord + module_path: qlib.workflow.record_temp + kwargs: {} + - class: PortAnaRecord + module_path: qlib.workflow.record_temp + kwargs: + config: *port_analysis_config + +After saving the config into `configuration.yaml`, users could start the workflow and test their ideas with a single command below. + +.. code-block:: bash + + qrun -c configuration.yaml + +.. note:: + + `qrun` will be placed in your $PATH directory when installing ``Qlib``. + + +Configuration File +=================== + +Let's get into details of ``qrun`` in this section. + +Before using ``qrun``, users need to prepare a configuration file. The following content shows how to prepare each part of the configuration file. + +Qlib Data Section +-------------------- + +At first, the configuration file needs to contain several basic parameters about the data, which will be used for qlib initialization, data handling and backtest. + +.. code-block:: YAML + + provider_uri: "~/.qlib/qlib_data/cn_data" + region: cn + market: &market csi300 + benchmark: &benchmark SH000300 + +The meaning of each field is as follows: + +- `provider_uri` + Type: str. The URI of the Qlib data. For example, it could be the location where the data loaded by ``get_data.py`` are stored. + +- `region` + - If `region` == "us", ``Qlib`` will be initialized in US-stock mode. + - If `region` == "cn", ``Qlib`` will be initialized in china-stock mode. + + .. note:: + + The value of `region` should be aligned with the data stored in `provider_uri`. + +- `market` + Type: str. Index name, the default value is `csi500`. + +- `benchmark` + Type: str, list or pandas.Series. Stock index symbol, the default value is `SH000905`. + + .. note:: + + * If `benchmark` is str, it will use the daily change as the 'bench'. + + * If `benchmark` is list, it will use the daily average change of the stock pool in the list as the 'bench'. + + * If `benchmark` is pandas.Series, whose `index` is trading date and the value T is the change from T-1 to T, it will be directly used as the 'bench'. An example is as following: + + .. code-block:: python + + print(D.features(D.instruments('csi500'), ['$close/Ref($close, 1)-1'])['$close/Ref($close, 1)-1'].head()) + 2017-01-04 0.011693 + 2017-01-05 0.000721 + 2017-01-06 -0.004322 + 2017-01-09 0.006874 + 2017-01-10 -0.003350 +.. note:: + + The symbol `&` in `yaml` file stands for an anchor of a field, which is useful when another fields include this parameter as part of the value. Taking the configuration file above as an example, users can directly change the value of `market` and `benchmark` without traversing the entire configuration file. + +Model Section +-------------------- + +In the `task` field, the `model` section describes the parameters of the model to be used for training and inference. For more information about the base ``Model`` class, please refer to `Qlib Model <../component/model.html>`_. + +.. code-block:: YAML + + 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 + +The meaning of each field is as follows: + +- `class` + Type: str. The name for the model class. + +- `module_path` + Type: str. The path for the model in qlib. + +- `kwargs` + The keywords arguments for the model. Please refer to the specific model implementation for more information: `models `_. + +.. note:: + + ``Qlib`` provides a util named: ``init_instance_by_config`` to initialize any class inside ``Qlib`` with the configuration includes the fields: `class`, `module_path` and `kwargs`. + +Dataset Section +-------------------- + +The `dataset` field describes the parameters for the ``Dataset`` module in ``Qlib`` as well those for the module ``DataHandler``. For more information about the ``Dataset`` module, please refer to `Qlib Model <../component/data.html#dataset>`_. + +The keywords arguments configuration of the ``DataHandler`` is as follows: + +.. code-block:: YAML + + data_handler_config: &data_handler_config + start_time: 2008-01-01 + end_time: 2020-08-01 + fit_start_time: 2008-01-01 + fit_end_time: 2014-12-31 + instruments: *market + +Users can refer to the document of `DataHandler <../component/data.html#datahandler>`_ for more information about the meaning of each field in the configuration. + +Here is the configuration for the ``Dataset`` module which will take care of data preprossing and slicing during the training and testing phase. + +.. code-block:: YAML + + dataset: + class: DatasetH + module_path: qlib.data.dataset + kwargs: + handler: + class: Alpha158 + module_path: qlib.contrib.data.handler + kwargs: *data_handler_config + segments: + train: [2008-01-01, 2014-12-31] + valid: [2015-01-01, 2016-12-31] + test: [2017-01-01, 2020-08-01] + +Record Section +-------------------- + +The `record` field is about the parameters the ``Record`` module in ``Qlib``. ``Record`` is responsible for generating certain analysis and evaluation results such as `prediction`, `information Coefficient (IC)` and `backtest`. + +The following script is the configuration of `backtest` and the `strategy` used in `backtest`: + +.. code-block:: YAML + + port_analysis_config: &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 + +For more information about the meaning of each field in configuration of `strategy` and `backtest`, users can look up the documents: `Strategy <../component/strategy.html>`_ and `Backtest <../component/backtest.html>`_. + +Here is the configuration details of different `Record Template` such as ``SignalRecord`` and ``PortAnaRecord``: + +.. code-block:: YAML + + record: + - class: SignalRecord + module_path: qlib.workflow.record_temp + kwargs: {} + - class: PortAnaRecord + module_path: qlib.workflow.record_temp + kwargs: + config: *port_analysis_config + +For more information about the ``Record`` module in ``Qlib``, user can refer to the related document: `Record <../component/recorder.html#record-template>`_. \ No newline at end of file diff --git a/docs/index.rst b/docs/index.rst index 5bcbbf19b..3a7358288 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -35,11 +35,12 @@ Document Structure :maxdepth: 3 :caption: COMPONENTS: - Estimator: Workflow Management + Workflow: Workflow Management Data Layer: Data Framework&Usage Interday Model: Model Training & Prediction Interday Strategy: Portfolio Management Intraday Trading: Model&Strategy Testing + Qlib Recorder: Experiment Management Aanalysis: Evaluation & Results Analysis .. toctree:: @@ -48,6 +49,7 @@ Document Structure Building Formulaic Alphas Online & Offline mode + .. toctree:: :maxdepth: 3 :caption: REFERENCE: diff --git a/docs/introduction/quick.rst b/docs/introduction/quick.rst index 9fff8cb3f..a367e2dde 100644 --- a/docs/introduction/quick.rst +++ b/docs/introduction/quick.rst @@ -49,18 +49,19 @@ To kown more about `prepare data`, please refer to `Data Preparation <../compone Auto Quant Research Workflow ==================================== -``Qlib`` provides a tool named ``Estimator`` to run the whole workflow automatically (including building dataset, training models, backtest and evaluation). Users can start an auto quant research workflow and have a graphical reports analysis according to the following steps: +``Qlib`` provides a tool named ``qrun`` to run the whole workflow automatically (including building dataset, training models, backtest and evaluation). Users can start an auto quant research workflow and have a graphical reports analysis according to the following steps: - Quant Research Workflow: - - Run ``Estimator`` with `estimator_config.yaml` as following. + - Run ``qrun`` with a config file of the LightGBM model `workflow_config_lightgbm.yaml` as following. + .. code-block:: cd examples # Avoid running program under the directory contains `qlib` - estimator -c estimator/estimator_config.yaml + qrun benchmarks/LightGBM/workflow_config_lightgbm.yaml - - Estimator result - The result of ``Estimator`` is as follows, which is also the result of ``Intraday Trading``. Please refer to `Intraday Trading <../component/backtest.html>`_. for more details about the result. + - Workflow result + The result of ``qrun`` is as follows, which is also the result of ``Intraday Trading``. Please refer to `Intraday Trading <../component/backtest.html>`_. for more details about the result. .. code-block:: python @@ -77,11 +78,11 @@ Auto Quant Research Workflow max_drawdown -0.075024 - To know more about `Estimator`, please refer to `Estimator: Workflow Management <../component/estimator.html>`_. + To know more about `workflow` and `qrun`, please refer to `Workflow: Workflow Management <../component/workflow.html>`_. - Graphical Reports Analysis: - - Run ``examples/estimator/analyze_from_estimator.ipynb`` with jupyter notebook - Users can have portfolio analysis or prediction score (model prediction) analysis by run ``examples/estimator/analyze_from_estimator.ipynb``. + - Run ``examples/workflow_by_code.ipynb`` with jupyter notebook + Users can have portfolio analysis or prediction score (model prediction) analysis by run ``examples/workflow_by_code.ipynb``. - Graphical Reports Users can get graphical reports about the analysis, please refer to `Aanalysis: Evaluation & Results Analysis <../component/report.html>`_ for more details. @@ -90,4 +91,4 @@ Auto Quant Research Workflow Custom Model Integration =============================================== -``Qlib`` provides ``lightGBM`` and ``Dnn`` model as the baseline of ``Interday Model``. In addition to the default model, users can integrate their own custom models into ``Qlib``. If users are interested in the custom model, please refer to `Custom Model Integration <../start/integration.html>`_. +``Qlib`` provides several models such as ``lightGBM`` and ``DNN`` model as the baseline of ``Interday Model``. In addition to the default model, users can integrate their own custom models into ``Qlib``. If users are interested in the custom model, please refer to `Custom Model Integration <../start/integration.html>`_. diff --git a/docs/reference/api.rst b/docs/reference/api.rst index 637a32053..76d2a74a5 100644 --- a/docs/reference/api.rst +++ b/docs/reference/api.rst @@ -116,3 +116,26 @@ Report :members: +Workflow +==================== + + +Experiment Manager +-------------------- +.. autoclass:: qlib.workflow.expm.ExpManager + :members: + +Experiment +-------------------- +.. autoclass:: qlib.workflow.exp.Experiment + :members: + +Recorder +-------------------- +.. autoclass:: qlib.workflow.recorder.Recorder + :members: + +Record Template +-------------------- +.. automodule:: qlib.workflow.record_temp + :members: \ No newline at end of file diff --git a/docs/start/getdata.rst b/docs/start/getdata.rst index b352082cb..8e1695c14 100644 --- a/docs/start/getdata.rst +++ b/docs/start/getdata.rst @@ -1,4 +1,5 @@ .. _getdata: + ============================= Data Retrieval ============================= diff --git a/docs/start/initialization.rst b/docs/start/initialization.rst index b975a18c5..af89a098e 100644 --- a/docs/start/initialization.rst +++ b/docs/start/initialization.rst @@ -1,4 +1,5 @@ .. _initialization: + ==================== Qlib Initialization ==================== @@ -59,7 +60,7 @@ Besides `provider_uri` and `region`, `qlib.init` has other parameters. The follo If Qlib fails to connect redis via `redis_host` and `redis_port`, cache mechanism will not be used! Please refer to `Cache <../component/data.html#cache>`_ for details. - `exp_manager` - Type: dict, optional parameter, the setting of experiment manager to be used in qlib. Users can specify an experiment manager class, as well as the tracking URI for all the experiments. However, please be aware that we only support input of a dictionary in the following style for `exp_manager`. + Type: dict, optional parameter, the setting of `experiment manager` to be used in qlib. Users can specify an experiment manager class, as well as the tracking URI for all the experiments. However, please be aware that we only support input of a dictionary in the following style for `exp_manager`. For more information about `exp_manager`, users can refer to `Recorder: Experiment Management <../component/recorder.html>`_. :: { diff --git a/docs/start/installation.rst b/docs/start/installation.rst index 2ac3dda77..af0b37372 100644 --- a/docs/start/installation.rst +++ b/docs/start/installation.rst @@ -1,4 +1,5 @@ .. _installation: + ==================== Installation ==================== diff --git a/qlib/contrib/evaluate.py b/qlib/contrib/evaluate.py index a4b6d87dc..a9b08719a 100644 --- a/qlib/contrib/evaluate.py +++ b/qlib/contrib/evaluate.py @@ -65,10 +65,14 @@ def get_strategy( topk : int (Default value: 50) top-N stocks to buy. margin : int or float(Default value: 0.5) - if isinstance(margin, int): + - if isinstance(margin, int): + sell_limit = margin - else: + + - else: + sell_limit = pred_in_a_day.count() * margin + buffer margin, in single score_mode, continue holding stock if it is in nlargest(sell_limit) sell_limit should be no less than topk n_drop : int @@ -204,10 +208,14 @@ def backtest(pred, account=1e9, shift=1, benchmark="SH000905", verbose=True, **k topk : int (Default value: 50) top-N stocks to buy. margin : int or float(Default value: 0.5) - if isinstance(margin, int): + - if isinstance(margin, int): + sell_limit = margin - else: + + - else: + sell_limit = pred_in_a_day.count() * margin + buffer margin, in single score_mode, continue holding stock if it is in nlargest(sell_limit) sell_limit should be no less than topk n_drop : int diff --git a/qlib/contrib/model/gbdt.py b/qlib/contrib/model/gbdt.py index 995a02696..e52c05906 100644 --- a/qlib/contrib/model/gbdt.py +++ b/qlib/contrib/model/gbdt.py @@ -16,7 +16,7 @@ class LGBModel(ModelFT): def __init__(self, loss="mse", **kwargs): if loss not in {"mse", "binary"}: raise NotImplementedError - self.params = {"objective": loss, 'verbosity': -1} + self.params = {"objective": loss, "verbosity": -1} self.params.update(kwargs) self.model = None diff --git a/qlib/contrib/strategy/strategy.py b/qlib/contrib/strategy/strategy.py index 5c34e707b..084737445 100644 --- a/qlib/contrib/strategy/strategy.py +++ b/qlib/contrib/strategy/strategy.py @@ -137,7 +137,9 @@ class WeightStrategyBase(BaseStrategy, AdjustTimer): self.order_generator = order_generator_cls_or_obj def generate_target_weight_position(self, score, current, trade_date): - """Parameter: + """ + Parameters: + --------- score : pred score for this trade date, pd.Series, index is stock_id, contain 'score' column current : current position, use Position() class trade_exchange : Exchange() @@ -148,7 +150,9 @@ class WeightStrategyBase(BaseStrategy, AdjustTimer): raise NotImplementedError() def generate_order_list(self, score_series, current, trade_exchange, pred_date, trade_date): - """Parameter + """ + Parameters: + ---------- score_series : pd.Seires stock_id , score current : Position() @@ -181,7 +185,9 @@ class WeightStrategyBase(BaseStrategy, AdjustTimer): class TopkDropoutStrategy(BaseStrategy, ListAdjustTimer): def __init__(self, topk, n_drop, method="bottom", risk_degree=0.95, thresh=1, hold_thresh=1, **kwargs): - """Parameter + """ + Parameters: + ----------- topk : int The number of stocks in the portfolio n_drop : int @@ -218,19 +224,21 @@ class TopkDropoutStrategy(BaseStrategy, ListAdjustTimer): return self.risk_degree def generate_order_list(self, score_series, current, trade_exchange, pred_date, trade_date): - """Gnererate order list according to score_series at trade_date. - will not change current. - Parameter - score_series : pd.Seires - stock_id , score - current : Position() - current of account - trade_exchange : Exchange() - exchange - pred_date : pd.Timestamp - predict date - trade_date : pd.Timestamp - trade date + """ + Gnererate order list according to score_series at trade_date, will not change current. + + Parameters: + ---------- + score_series : pd.Series + stock_id , score + current : Position() + current of account + trade_exchange : Exchange() + exchange + pred_date : pd.Timestamp + predict date + trade_date : pd.Timestamp + trade date """ if not self.is_adjust(trade_date): return [] diff --git a/qlib/data/cache.py b/qlib/data/cache.py index 2b9783fe3..bf8baab31 100644 --- a/qlib/data/cache.py +++ b/qlib/data/cache.py @@ -748,7 +748,8 @@ class DiskDatasetCache(DatasetCache): The format the cache contains 3 parts(followed by typical filename). - - index : cache/d41366901e25de3ec47297f12e2ba11d.index + - index : cache/d41366901e25de3ec47297f12e2ba11d.index + - The content of the file may be in following format(pandas.Series) .. code-block:: python @@ -765,7 +766,9 @@ class DiskDatasetCache(DatasetCache): - It indicates the `end_index` of the data for `timestamp` - meta data: cache/d41366901e25de3ec47297f12e2ba11d.meta + - data : cache/d41366901e25de3ec47297f12e2ba11d + - This is a hdf file sorted by datetime :param cache_path: The path to store the cache diff --git a/qlib/data/data.py b/qlib/data/data.py index 8fac9edec..ef5e7fe8a 100644 --- a/qlib/data/data.py +++ b/qlib/data/data.py @@ -152,16 +152,19 @@ class InstrumentProvider(abc.ABC): {`market`=>base market name, `filter_pipe`=>list of filters} example : - {'market': 'csi500', - 'filter_pipe': [{'filter_type': 'ExpressionDFilter', - 'rule_expression': '$open<40', - 'filter_start_time': None, - 'filter_end_time': None, - 'keep': False}, - {'filter_type': 'NameDFilter', - 'name_rule_re': 'SH[0-9]{4}55', - 'filter_start_time': None, - 'filter_end_time': None}]} + + .. code-block:: + + {'market': 'csi500', + 'filter_pipe': [{'filter_type': 'ExpressionDFilter', + 'rule_expression': '$open<40', + 'filter_start_time': None, + 'filter_end_time': None, + 'keep': False}, + {'filter_type': 'NameDFilter', + 'name_rule_re': 'SH[0-9]{4}55', + 'filter_start_time': None, + 'filter_end_time': None}]} """ if filter_pipe is None: filter_pipe = [] @@ -956,6 +959,8 @@ class BaseProvider: disk_cache=None, ): """ + Parameters: + ----------- disk_cache : int whether to skip(0)/use(1)/replace(2) disk_cache diff --git a/qlib/data/dataset/handler.py b/qlib/data/dataset/handler.py index 0a2fed637..e0a4d809a 100644 --- a/qlib/data/dataset/handler.py +++ b/qlib/data/dataset/handler.py @@ -40,12 +40,15 @@ class DataHandler(Serializable): Example of the data: The multi-index of the columns is optional. - feature label - $close $volume Ref($close, 1) Mean($close, 3) $high-$low LABEL0 - datetime instrument - 2010-01-04 SH600000 81.807068 17145150.0 83.737389 83.016739 2.741058 0.0032 - SH600004 13.313329 11800983.0 13.313329 13.317701 0.183632 0.0042 - SH600005 37.796539 12231662.0 38.258602 37.919757 0.970325 0.0289 + + .. code-block:: + + feature label + $close $volume Ref($close, 1) Mean($close, 3) $high-$low LABEL0 + datetime instrument + 2010-01-04 SH600000 81.807068 17145150.0 83.737389 83.016739 2.741058 0.0032 + SH600004 13.313329 11800983.0 13.313329 13.317701 0.183632 0.0042 + SH600005 37.796539 12231662.0 38.258602 37.919757 0.970325 0.0289 """ @@ -107,7 +110,8 @@ class DataHandler(Serializable): ---------- enable_cache : bool default value is false - if `enable_cache` == True + - if `enable_cache` == True: + the processed data will be saved on disk, and handler will load the cached data from the disk directly when we call `init` next time """ @@ -145,16 +149,21 @@ class DataHandler(Serializable): level : Union[str, int] which index level to select the data col_set : Union[str, List[str]] - if isinstance(col_set, str): + + - if isinstance(col_set, str): + select a set of meaningful columns.(e.g. features, columns) - if isinstance(col_set, List[str]): + + - if isinstance(col_set, List[str]): + select several sets of meaningful columns, the returned data has multiple levels + squeeze : bool whether squeeze columns and index Returns ------- - pd.DataFrame: + pd.DataFrame. """ # Fetch column first will be more friendly to SepDataFrame df = self._fetch_df_by_col(self._data, col_set) diff --git a/qlib/data/dataset/loader.py b/qlib/data/dataset/loader.py index d7e262eb7..e95dc4479 100644 --- a/qlib/data/dataset/loader.py +++ b/qlib/data/dataset/loader.py @@ -161,7 +161,7 @@ class StaticDataLoader(DataLoader): DataLoader that supports loading data from file or as provided. """ - def __init__(self, config: dict, join='outer'): + def __init__(self, config: dict, join="outer"): """ Parameters ---------- @@ -187,8 +187,9 @@ class StaticDataLoader(DataLoader): def _maybe_load_raw_data(self): if self._data is not None: return - self._data = pd.concat({ - fields_group: load_dataset(path_or_obj) - for fields_group, path_or_obj in self.config.items() - }, axis=1, join=self.join) + self._data = pd.concat( + {fields_group: load_dataset(path_or_obj) for fields_group, path_or_obj in self.config.items()}, + axis=1, + join=self.join, + ) self._data.sort_index(inplace=True) diff --git a/qlib/model/base.py b/qlib/model/base.py index 3fe83445c..d6ee50e33 100644 --- a/qlib/model/base.py +++ b/qlib/model/base.py @@ -25,8 +25,10 @@ class Model(BaseModel): """ Learn model from the base model - ** NOTE **: The the attribute names of learned model should **not** start with '_'. So that the model could be - dumped to disk. + .. note:: + + The the attribute names of learned model should `not` start with '_'. So that the model could be + dumped to disk. Parameters ---------- diff --git a/qlib/utils/__init__.py b/qlib/utils/__init__.py index 14480b7b5..5b313a0ef 100644 --- a/qlib/utils/__init__.py +++ b/qlib/utils/__init__.py @@ -702,7 +702,7 @@ def load_dataset(path_or_obj): if isinstance(path_or_obj, pd.DataFrame): return path_or_obj if not os.path.exists(path_or_obj): - raise ValueError(f'file {path_or_obj} doesn\'t exist') + raise ValueError(f"file {path_or_obj} doesn't exist") _, extension = os.path.splitext(path_or_obj) if extension == ".h5": return pd.read_hdf(path_or_obj) diff --git a/qlib/workflow/__init__.py b/qlib/workflow/__init__.py index 9da65480f..8944ecbe6 100644 --- a/qlib/workflow/__init__.py +++ b/qlib/workflow/__init__.py @@ -162,6 +162,10 @@ class QlibRecorder: """ Method for listing all the recorders of experiment with given id or name. + If user doesn't provide the id or name of the experiment, this method will try to retrieve the default experiment and + list all the recorders of the default experiment. If the default experiment doesn't exist, the method will first + create the default experiment, and then create a new recorder under it. + Use case: --------- ``` @@ -382,7 +386,7 @@ class QlibRecorder: ---------- local_path : str if provided, them save the file or directory to the artifact URI. - artifact_path=None : str + artifact_path : str the relative path for the artifact to be stored in the URI. """ self.get_exp().get_recorder().save_objects(local_path, artifact_path, **kwargs) diff --git a/qlib/workflow/exp.py b/qlib/workflow/exp.py index 5a74ab28a..c23f27f09 100644 --- a/qlib/workflow/exp.py +++ b/qlib/workflow/exp.py @@ -12,7 +12,7 @@ logger = get_module_logger("workflow", "INFO") class Experiment: """ - Thie is the `Experiment` class for each experiment being run. The API is designed similar to mlflow. + This is the `Experiment` class for each experiment being run. The API is designed similar to mlflow. (The link: https://mlflow.org/docs/latest/python_api/mlflow.html) """ @@ -111,24 +111,29 @@ class Experiment: active recorder. The `create` argument determines whether the method will automatically create a new recorder according to user's specification if the recorder hasn't been created before - If `create` is True: - If R's running: - 1) no id or name specified, return the active recorder. - 2) if id or name is specified, return the specified recorder. If no such exp found, - create a new recorder with given id or name, and the recorder shoud be running. - If R's not running: - 1) no id or name specified, create a new recorder. - 2) if id or name is specified, return the specified experiment. If no such exp found, - create a new recorder with given id or name, and the recorder shoud be running. - Else If `create` is False: - If R's running: - 1) no id or name specified, return the active recorder. - 2) if id or name is specified, return the specified recorder. If no such exp found, - raise Error. - If R's not running: - 1) no id or name specified, raise Error. - 2) if id or name is specified, return the specified recorder. If no such exp found, - raise Error. + * If `create` is True: + + * If R's running: + + * no id or name specified, return the active recorder. + * if id or name is specified, return the specified recorder. If no such exp found, create a new recorder with given id or name, and the recorder shoud be running. + + * If R's not running: + + * no id or name specified, create a new recorder. + * if id or name is specified, return the specified experiment. If no such exp found, create a new recorder with given id or name, and the recorder shoud be running. + + * Else If `create` is False: + + * If R's running: + + * no id or name specified, return the active recorder. + * if id or name is specified, return the specified recorder. If no such exp found, raise Error. + + * If R's not running: + + * no id or name specified, raise Error. + * if id or name is specified, return the specified recorder. If no such exp found, raise Error. Parameters ---------- @@ -147,7 +152,8 @@ class Experiment: def list_recorders(self): """ - List all the existing recorders of this experiment. + List all the existing recorders of this experiment. Please first get the experiment instance before calling this method. + If user want to use the method `R.list_recorders()`, please refer to the related API document in `QlibRecorder`. Returns ------- diff --git a/qlib/workflow/expm.py b/qlib/workflow/expm.py index 7af661a64..156beb690 100644 --- a/qlib/workflow/expm.py +++ b/qlib/workflow/expm.py @@ -94,26 +94,31 @@ class ExpManager: When user specify experiment id and name, the method will try to return the specific experiment. When user does not provide recorder id or name, the method will try to return the current active experiment. The `create` argument determines whether the method will automatically create a new experiment according - to user's specification if the experiment hasn't been created before + to user's specification if the experiment hasn't been created before. - If `create` is True: - If R's running: - 1) no id or name specified, return the active experiment. - 2) if id or name is specified, return the specified experiment. If no such exp found, - create a new experiment with given id or name, and the experiment is set to be running. - If R's not running: - 1) no id or name specified, create a default experiment. - 2) if id or name is specified, return the specified experiment. If no such exp found, - create a new experiment with given id or name, and the experiment is set to be running. - Else If `create` is False: - If R's running: - 1) no id or name specified, return the active experiment. - 2) if id or name is specified, return the specified experiment. If no such exp found, - raise Error. - If R's not running: - 1) no id or name specified. If the default experiment exists, return it, otherwise, raise Error. - 2) if id or name is specified, return the specified experiment. If no such exp found, - raise Error. + * If `create` is True: + + * If R's running: + + * no id or name specified, return the active experiment. + * if id or name is specified, return the specified experiment. If no such exp found, create a new experiment with given id or name, and the experiment is set to be running. + + * If R's not running: + + * no id or name specified, create a default experiment. + * if id or name is specified, return the specified experiment. If no such exp found, create a new experiment with given id or name, and the experiment is set to be running. + + * Else If `create` is False: + + * If R's running: + + * no id or name specified, return the active experiment. + * if id or name is specified, return the specified experiment. If no such exp found, raise Error. + + * If R's not running: + + * no id or name specified. If the default experiment exists, return it, otherwise, raise Error. + * if id or name is specified, return the specified experiment. If no such exp found, raise Error. Parameters ---------- diff --git a/qlib/workflow/record_temp.py b/qlib/workflow/record_temp.py index 81b0022c5..1d0811d16 100644 --- a/qlib/workflow/record_temp.py +++ b/qlib/workflow/record_temp.py @@ -56,7 +56,12 @@ class RecordTemp: def load(self, name): """ - Load the stored records. + Load the stored records. Due to the fact that some problems occured when we tried to balancing a clean API + with the Python's inheritance. This method has to be used in a rather ugly way, and we will try to fix them + in the future:: + + sar = SigAnaRecord(recorder) + ic = sar.load(sar.get_path("ic.pkl")) Parameters ---------- @@ -102,7 +107,7 @@ class RecordTemp: class SignalRecord(RecordTemp): """ - This is the Signal Record class that generates the signal prediction. + 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, **kwargs): @@ -145,6 +150,9 @@ class SignalRecord(RecordTemp): class SigAnaRecord(SignalRecord): + """ + This is the Signal Analysis Record class that generates the analysis results such as IC and IR. This class inherits the ``RecordTemp`` class. + """ artifact_path = "sig_analysis" @@ -196,7 +204,7 @@ class SigAnaRecord(SignalRecord): class PortAnaRecord(SignalRecord): """ - This is the Portfolio Analysis Record class that generates the results such as those of backtest. + This is the Portfolio Analysis Record class that generates the analysis results such as those of backtest. This class inherits the ``RecordTemp`` class. """ artifact_path = "portfolio_analysis" diff --git a/requirements.txt b/requirements.txt index f927ce5a2..638ce22f4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -22,4 +22,4 @@ scikit_learn==0.23.2 torch==1.6.0 tqdm==4.49.0 yahooquery==2.2.7 -mlflow==1.11.0 \ No newline at end of file +mlflow==1.12.1 \ No newline at end of file From 8ed01d5c8e1c5f79ce68cea889f3458d6b0ae5c2 Mon Sep 17 00:00:00 2001 From: Young Date: Wed, 25 Nov 2020 01:11:34 +0000 Subject: [PATCH 095/241] more friendly error info when benchmark error --- qlib/contrib/backtest/backtest.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/qlib/contrib/backtest/backtest.py b/qlib/contrib/backtest/backtest.py index 52e74e14b..7ee8dceb0 100644 --- a/qlib/contrib/backtest/backtest.py +++ b/qlib/contrib/backtest/backtest.py @@ -65,6 +65,8 @@ def backtest(pred, strategy, trade_exchange, shift, verbose, account, benchmark) 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() trade_dates = np.append(predict_dates[shift:], get_date_range(predict_dates[-1], shift=shift)) From 0e2c2fcd7f7ddac2e30a0d53e6caf59989e903d5 Mon Sep 17 00:00:00 2001 From: lwwang1995 Date: Wed, 25 Nov 2020 10:44:48 +0800 Subject: [PATCH 096/241] Add Tabnet. --- examples/workflow_by_code_tabnet.py | 142 ++++++++++++++++++++++++++++ qlib/contrib/model/tabnet.py | 80 ++++++++++++++++ 2 files changed, 222 insertions(+) create mode 100644 examples/workflow_by_code_tabnet.py create mode 100644 qlib/contrib/model/tabnet.py diff --git a/examples/workflow_by_code_tabnet.py b/examples/workflow_by_code_tabnet.py new file mode 100644 index 000000000..d275a875c --- /dev/null +++ b/examples/workflow_by_code_tabnet.py @@ -0,0 +1,142 @@ +# 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.tabnet import TabNetModel +from qlib.contrib.data.handler import ALPHA360_Denoise +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 + +# from qlib.model.learner import train_model +from qlib.utils import init_instance_by_config + +import pickle + +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": "2008-01-01", + "end_time": "2020-08-01", + "fit_start_time": "2008-01-01", + "fit_end_time": "2014-12-31", + "instruments": MARKET, + } + + TRAINER_CONFIG = { + "train_start_time": "2008-01-01", + "train_end_time": "2014-12-31", + "validate_start_time": "2015-01-01", + "validate_end_time": "2016-12-31", + "test_start_time": "2017-01-01", + "test_end_time": "2020-08-01", + } + + task = { + "model": { + "class": "TabNetModel", + "module_path": "qlib.contrib.model.tabnet", + "kwargs": { + "n_d": 8, + "n_a": 8, + "n_steps": 3, + "gamma": 1.3, + "n_independent": 2, + "n_shared": 2, + "seed": 0, + "momentum": 0.02, + "lambda_sparse": 1e-3, + "optimizer_params": {'lr':2e-3} + }, + }, + "dataset": { + "class": "DatasetH", + "module_path": "qlib.data.dataset", + "kwargs": { + "handler": { + "class": "ALPHA360_Denoise", + "module_path": "qlib.contrib.data.handler", + "kwargs": DATA_HANDLER_CONFIG, + }, + "segments": { + "train": ("2008-01-01", "2014-12-31"), + "valid": ("2015-01-01", "2016-12-31"), + "test": ("2017-01-01", "2020-08-01"), + }, + }, + } + # You shoud record the data in specific sequence + # "record": ['SignalRecord', 'SigAnaRecord', 'PortAnaRecord'], + } + + # model = train_model(task) + model = init_instance_by_config(task["model"]) + dataset = init_instance_by_config(task["dataset"]) + model.fit(dataset) + + pred_score = model.predict(dataset) + + # save pred_score to file + pred_score_path = Path("~/tmp/qlib/pred_score.pkl").expanduser() + pred_score_path.parent.mkdir(exist_ok=True, parents=True) + pred_score.to_pickle(pred_score_path) + + ################################### + # backtest + ################################### + STRATEGY_CONFIG = { + "topk": 50, + "n_drop": 5, + } + BACKTEST_CONFIG = { + "verbose": False, + "limit_threshold": 0.095, + "account": 100000000, + "benchmark": BENCHMARK, + "deal_price": "close", + "open_cost": 0.0005, + "close_cost": 0.0015, + "min_cost": 5, + } + + # use default strategy + # custom Strategy, refer to: TODO: Strategy API url + strategy = TopkDropoutStrategy(**STRATEGY_CONFIG) + report_normal, positions_normal = normal_backtest(pred_score, strategy=strategy, **BACKTEST_CONFIG) + + ################################### + # analyze + # If need a more detailed analysis, refer to: examples/train_and_bakctest.ipynb + ################################### + 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 + print(analysis_df) diff --git a/qlib/contrib/model/tabnet.py b/qlib/contrib/model/tabnet.py new file mode 100644 index 000000000..63a75d26f --- /dev/null +++ b/qlib/contrib/model/tabnet.py @@ -0,0 +1,80 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +import numpy as np +import pandas as pd +from pytorch_tabnet.tab_model import TabNetRegressor + +from ...model.base import Model +from ...data.dataset import DatasetH +from ...data.dataset.handler import DataHandlerLP + +class TabNetModel(Model): + """TabNetModel Model""" + + def __init__(self, n_d, n_a, + n_steps, + gamma, + n_independent, + n_shared, + seed, + momentum, + lambda_sparse, + optimizer_params, + **kwargs): + self.model = None + + self.n_d = n_d + self.n_a = n_a + self.n_steps = n_steps + self.gamma = gamma + self.n_independent = n_independent + self.n_shared = n_shared + self.seed = seed + self.momentum = momentum + self.lambda_sparse = lambda_sparse + self.optimizer_params = optimizer_params + + def fit( + self, + dataset: DatasetH, + n_d=8, + n_a=8, + n_steps=3, + gamma=1.3, + n_independent=2, + n_shared=2, + seed=0, + momentum=0.02, + lambda_sparse=1e-3, + optimizer_params={'lr':2e-3}, + **kwargs + ): + + df_train, df_valid = dataset.prepare( + ["train", "valid"], col_set=["feature", "label"], data_key=DataHandlerLP.DK_L + ) + x_train, y_train = df_train["feature"].values, df_train["label"].values*100 + x_valid, y_valid = df_valid["feature"].values, df_valid["label"].values*100 + + self.model = TabNetRegressor( + n_d=self.n_d, + n_a=self.n_a, + n_steps=self.n_steps, + gamma=self.gamma, + n_independent=self.n_independent, + n_shared=self.n_shared, + seed=self.seed, + momentum=self.momentum, + lambda_sparse=self.lambda_sparse, + optimizer_params=self.optimizer_params, + **kwargs + ) + self.model.fit(x_train, y_train, eval_set=[(x_valid, y_valid)]) + + def predict(self, dataset): + if self.model is None: + raise ValueError("model is not fitted yet!") + x_test = dataset.prepare("test", col_set="feature") + test_pred = self.model.predict(x_test.values) + return pd.Series(test_pred.reshape([-1]), index=x_test.index) From 991c6195bd897a8834741fc011cbd25e6a59b71f Mon Sep 17 00:00:00 2001 From: Jactus Date: Wed, 25 Nov 2020 11:16:01 +0800 Subject: [PATCH 097/241] Add TabNet config --- README.md | 3 +- .../CatBoost/workflow_config_catboost.yaml | 2 +- .../benchmarks/DNN/workflow_config_dnn.yaml | 2 +- examples/benchmarks/TabNet/requirements.txt | 5 ++ .../TabNet/workflow_config_tabnet.yaml | 66 +++++++++++++++++++ examples/workflow_by_code_tabnet.py | 2 +- qlib/contrib/model/tabnet.py | 53 ++++++++------- qlib/contrib/strategy/strategy.py | 14 ++-- setup.py | 1 + 9 files changed, 114 insertions(+), 34 deletions(-) create mode 100644 examples/benchmarks/TabNet/requirements.txt create mode 100644 examples/benchmarks/TabNet/workflow_config_tabnet.yaml diff --git a/README.md b/README.md index b06afd975..7c7e58a1c 100644 --- a/README.md +++ b/README.md @@ -197,7 +197,8 @@ Here is a list of models built on `Qlib`. - [GRU based on pytorch](qlib/contrib/model/pytorch_gru.py) - [LSTM based on pytorcn](qlib/contrib/model/pytorch_lstm.py) - [GATs based on pytorch](qlib/contrib/model/pytorch_gats.py) -- [TFT based on tensorflow-1.15.0](examples/benchmarks/TFT/tft.py) +- [TabNet based on pytorch](qlib/contrib/model/tabnet.py) + Your PR of new Quant models is highly welcomed. diff --git a/examples/benchmarks/CatBoost/workflow_config_catboost.yaml b/examples/benchmarks/CatBoost/workflow_config_catboost.yaml index 80229e22b..8bf3bb72b 100644 --- a/examples/benchmarks/CatBoost/workflow_config_catboost.yaml +++ b/examples/benchmarks/CatBoost/workflow_config_catboost.yaml @@ -37,7 +37,7 @@ task: module_path: qlib.data.dataset kwargs: handler: - class: ALPHA360_Denoise + class: Alpha158 module_path: qlib.contrib.data.handler kwargs: *data_handler_config segments: diff --git a/examples/benchmarks/DNN/workflow_config_dnn.yaml b/examples/benchmarks/DNN/workflow_config_dnn.yaml index 6dbd345dd..e853726ca 100644 --- a/examples/benchmarks/DNN/workflow_config_dnn.yaml +++ b/examples/benchmarks/DNN/workflow_config_dnn.yaml @@ -44,7 +44,7 @@ task: module_path: qlib.data.dataset kwargs: handler: - class: ALPHA360_Denoise + class: Alpha158 module_path: qlib.contrib.data.handler kwargs: *data_handler_config segments: diff --git a/examples/benchmarks/TabNet/requirements.txt b/examples/benchmarks/TabNet/requirements.txt new file mode 100644 index 000000000..244b74b19 --- /dev/null +++ b/examples/benchmarks/TabNet/requirements.txt @@ -0,0 +1,5 @@ +pandas==1.1.2 +numpy==1.17.4 +scikit_learn==0.23.2 +torch==1.7.0 +pytorch-tabnet==2.0.1 \ No newline at end of file diff --git a/examples/benchmarks/TabNet/workflow_config_tabnet.yaml b/examples/benchmarks/TabNet/workflow_config_tabnet.yaml new file mode 100644 index 000000000..0ee95f238 --- /dev/null +++ b/examples/benchmarks/TabNet/workflow_config_tabnet.yaml @@ -0,0 +1,66 @@ +provider_uri: "~/.qlib/qlib_data/cn_data" +region: cn +market: &market csi300 +benchmark: &benchmark SH000300 +data_handler_config: &data_handler_config + start_time: 2008-01-01 + end_time: 2020-08-01 + fit_start_time: 2008-01-01 + fit_end_time: 2014-12-31 + instruments: *market +port_analysis_config: &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 +task: + model: + class: TabNetModel + module_path: qlib.contrib.model.tabnet + kwargs: + n_d: 8 + n_a: 8 + n_steps: 3 + gamma: 1.3 + n_independent: 2 + n_shared: 2 + seed: 0 + momentum: 0.02 + lambda_sparse: 1e-3 + optimizer_params: {lr: 2e-3} + dataset: + class: DatasetH + module_path: qlib.data.dataset + kwargs: + handler: + class: Alpha158 + module_path: qlib.contrib.data.handler + kwargs: *data_handler_config + segments: + train: [2008-01-01, 2014-12-31] + valid: [2015-01-01, 2016-12-31] + test: [2017-01-01, 2020-08-01] + record: + - class: SignalRecord + module_path: qlib.workflow.record_temp + kwargs: {} + - class: SigAnaRecord + module_path: qlib.workflow.record_temp + kwargs: + ana_long_short: False + ann_scaler: 252 + - class: PortAnaRecord + module_path: qlib.workflow.record_temp + kwargs: + config: *port_analysis_config \ No newline at end of file diff --git a/examples/workflow_by_code_tabnet.py b/examples/workflow_by_code_tabnet.py index d275a875c..3778b9d59 100644 --- a/examples/workflow_by_code_tabnet.py +++ b/examples/workflow_by_code_tabnet.py @@ -71,7 +71,7 @@ if __name__ == "__main__": "seed": 0, "momentum": 0.02, "lambda_sparse": 1e-3, - "optimizer_params": {'lr':2e-3} + "optimizer_params": {"lr": 2e-3}, }, }, "dataset": { diff --git a/qlib/contrib/model/tabnet.py b/qlib/contrib/model/tabnet.py index 63a75d26f..bc13d1f62 100644 --- a/qlib/contrib/model/tabnet.py +++ b/qlib/contrib/model/tabnet.py @@ -9,19 +9,24 @@ from ...model.base import Model from ...data.dataset import DatasetH from ...data.dataset.handler import DataHandlerLP + class TabNetModel(Model): """TabNetModel Model""" - def __init__(self, n_d, n_a, - n_steps, - gamma, - n_independent, - n_shared, - seed, - momentum, - lambda_sparse, - optimizer_params, - **kwargs): + def __init__( + self, + n_d, + n_a, + n_steps, + gamma, + n_independent, + n_shared, + seed, + momentum, + lambda_sparse, + optimizer_params, + **kwargs + ): self.model = None self.n_d = n_d @@ -47,28 +52,28 @@ class TabNetModel(Model): seed=0, momentum=0.02, lambda_sparse=1e-3, - optimizer_params={'lr':2e-3}, + optimizer_params={"lr": 2e-3}, **kwargs ): df_train, df_valid = dataset.prepare( ["train", "valid"], col_set=["feature", "label"], data_key=DataHandlerLP.DK_L ) - x_train, y_train = df_train["feature"].values, df_train["label"].values*100 - x_valid, y_valid = df_valid["feature"].values, df_valid["label"].values*100 + x_train, y_train = df_train["feature"].values, df_train["label"].values * 100 + x_valid, y_valid = df_valid["feature"].values, df_valid["label"].values * 100 self.model = TabNetRegressor( - n_d=self.n_d, - n_a=self.n_a, - n_steps=self.n_steps, - gamma=self.gamma, - n_independent=self.n_independent, - n_shared=self.n_shared, - seed=self.seed, - momentum=self.momentum, - lambda_sparse=self.lambda_sparse, - optimizer_params=self.optimizer_params, - **kwargs + n_d=self.n_d, + n_a=self.n_a, + n_steps=self.n_steps, + gamma=self.gamma, + n_independent=self.n_independent, + n_shared=self.n_shared, + seed=self.seed, + momentum=self.momentum, + lambda_sparse=self.lambda_sparse, + optimizer_params=self.optimizer_params, + **kwargs ) self.model.fit(x_train, y_train, eval_set=[(x_valid, y_valid)]) diff --git a/qlib/contrib/strategy/strategy.py b/qlib/contrib/strategy/strategy.py index 084737445..6eac9bafe 100644 --- a/qlib/contrib/strategy/strategy.py +++ b/qlib/contrib/strategy/strategy.py @@ -25,7 +25,9 @@ class BaseStrategy: return 0.95 def generate_order_list(self, score_series, current, trade_exchange, pred_date, trade_date): - """Parameter + """ + Parameters: + ----------- score_series : pd.Seires stock_id , score current : Position() @@ -44,8 +46,8 @@ class BaseStrategy: def update(self, score_series, pred_date, trade_date): """User can use this method to update strategy state each trade date. - Parameter - --------- + Parameters: + ----------- score_series : pd.Series stock_id , score pred_date : pd.Timestamp @@ -97,7 +99,7 @@ class AdjustTimer: Responsible for timing of position adjusting This is designed as multiple inheritance mechanism due to - - the is_adjust may need access to the internel state of a strategyw + - the is_adjust may need access to the internel state of a strategy - it can be reguard as a enhancement to the existing strategy """ @@ -139,7 +141,7 @@ class WeightStrategyBase(BaseStrategy, AdjustTimer): def generate_target_weight_position(self, score, current, trade_date): """ Parameters: - --------- + ----------- score : pred score for this trade date, pd.Series, index is stock_id, contain 'score' column current : current position, use Position() class trade_exchange : Exchange() @@ -228,7 +230,7 @@ class TopkDropoutStrategy(BaseStrategy, ListAdjustTimer): Gnererate order list according to score_series at trade_date, will not change current. Parameters: - ---------- + ----------- score_series : pd.Series stock_id , score current : Position() diff --git a/setup.py b/setup.py index 2c9cfea95..4fe410b9d 100644 --- a/setup.py +++ b/setup.py @@ -58,6 +58,7 @@ REQUIRED = [ "joblib>=0.17.0", "fire>=0.3.1", "ruamel.yaml>=0.16.12", + "pytorch-tabnet>=2.0.1", ] # Numpy include From 610535de513caccf3055e3bcd698ce0b64a8ff19 Mon Sep 17 00:00:00 2001 From: Alex Wang Date: Wed, 25 Nov 2020 14:02:12 +0800 Subject: [PATCH 098/241] include SFM model --- qlib/contrib/model/pytorch_sfm.py | 465 ++++++++++++++++++++++++++++++ 1 file changed, 465 insertions(+) create mode 100644 qlib/contrib/model/pytorch_sfm.py diff --git a/qlib/contrib/model/pytorch_sfm.py b/qlib/contrib/model/pytorch_sfm.py new file mode 100644 index 000000000..90c37fcdd --- /dev/null +++ b/qlib/contrib/model/pytorch_sfm.py @@ -0,0 +1,465 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +from __future__ import division +from __future__ import print_function + +import os +import numpy as np +import pandas as pd +import copy +from sklearn.metrics import roc_auc_score, mean_squared_error +import logging +from ...utils import unpack_archive_with_buffer, save_multiple_parts_file, create_save_path, drop_nan_by_y_index +from ...log import get_module_logger, TimeInspector + +import torch +import torch.nn as nn +import torch.nn.init as init +import torch.optim as optim + +from ...model.base import Model +from ...data.dataset import DatasetH +from ...data.dataset.handler import DataHandlerLP + +class SFM_Model(nn.Module): + def __init__(self, d_feat=6, output_dim = 1, freq_dim = 10, hidden_size = 64, dropout_W = 0.0, dropout_U = 0.0, device = "cpu"): + super().__init__() + + self.input_dim = d_feat + self.output_dim = output_dim + self.freq_dim = freq_dim + self.hidden_dim = hidden_size + self.device = device + + self.W_i = nn.Parameter(init.xavier_uniform_(torch.empty((self.input_dim, self.hidden_dim)))) + self.U_i = nn.Parameter(init.orthogonal_(torch.empty(self.hidden_dim, self.hidden_dim))) + self.b_i = nn.Parameter(torch.zeros(self.hidden_dim)) + + self.W_ste = nn.Parameter(init.xavier_uniform_(torch.empty(self.input_dim, self.hidden_dim))) + self.U_ste = nn.Parameter(init.orthogonal_(torch.empty(self.hidden_dim, self.hidden_dim))) + self.b_ste = nn.Parameter(torch.ones(self.hidden_dim)) + + self.W_fre = nn.Parameter(init.xavier_uniform_(torch.empty(self.input_dim, self.freq_dim))) + self.U_fre = nn.Parameter(init.orthogonal_(torch.empty(self.hidden_dim, self.freq_dim))) + self.b_fre = nn.Parameter(torch.ones(self.freq_dim)) + + self.W_c = nn.Parameter(init.xavier_uniform_(torch.empty(self.input_dim, self.hidden_dim))) + self.U_c = nn.Parameter(init.orthogonal_(torch.empty(self.hidden_dim, self.hidden_dim))) + self.b_c = nn.Parameter(torch.zeros(self.hidden_dim)) + + self.W_o = nn.Parameter(init.xavier_uniform_(torch.empty(self.input_dim, self.hidden_dim))) + self.U_o = nn.Parameter(init.orthogonal_(torch.empty(self.hidden_dim, self.hidden_dim))) + self.b_o = nn.Parameter(torch.zeros(self.hidden_dim)) + + self.U_a = nn.Parameter(init.orthogonal_(torch.empty(self.freq_dim, 1))) + self.b_a = nn.Parameter(torch.zeros(self.hidden_dim)) + + self.W_p = nn.Parameter(init.xavier_uniform_(torch.empty(self.hidden_dim, self.output_dim))) + self.b_p = nn.Parameter(torch.zeros(self.output_dim)) + + self.activation = nn.Tanh() + self.inner_activation = nn.Hardsigmoid() + self.dropout_W, self.dropout_U = (dropout_W, dropout_U) + self.fc_out = nn.Linear(self.output_dim, 1) + + self.states = [] + + def forward(self, input): + input = input.reshape(len(input), self.input_dim, -1) # [N, F, T] + input = input.permute(0, 2, 1) # [N, T, F] + time_step = input.shape[1] + + for ts in range(time_step): + x = input[:, ts,:] + if(len(self.states)==0): #hasn't initialized yet + self.init_states(x) + self.get_constants(x) + p_tm1 = self.states[0] + h_tm1 = self.states[1] + S_re_tm1 = self.states[2] + S_im_tm1 = self.states[3] + time_tm1 = self.states[4] + B_U = self.states[5] + B_W = self.states[6] + frequency = self.states[7] + + x_i = torch.matmul(x * B_W[0], self.W_i) + self.b_i + x_ste = torch.matmul(x * B_W[0], self.W_ste) + self.b_ste + x_fre = torch.matmul(x * B_W[0], self.W_fre) + self.b_fre + x_c = torch.matmul(x * B_W[0], self.W_c) + self.b_c + x_o = torch.matmul(x * B_W[0], self.W_o) + self.b_o + + i = self.inner_activation(x_i + torch.matmul(h_tm1 * B_U[0], self.U_i)) # not sure whether I am doing in the right unsquuze + + + ste = self.inner_activation(x_ste + torch.matmul(h_tm1 * B_U[0], self.U_ste)) + fre = self.inner_activation(x_fre + torch.matmul(h_tm1 * B_U[0], self.U_fre)) + + ste = torch.reshape(ste, (-1, self.hidden_dim, 1)) + fre = torch.reshape(fre, (-1, 1, self.freq_dim)) + + f = ste * fre + + c = i * self.activation(x_c + torch.matmul(h_tm1 * B_U[0], self.U_c)) + + time = time_tm1 + 1 + + omega = torch.tensor(2 * np.pi) * time * frequency + + re = torch.cos(omega) + im = torch.sin(omega) + + c = torch.reshape(c, (-1, self.hidden_dim, 1)) + + S_re = f * S_re_tm1 + c * re + S_im = f * S_im_tm1 + c * im + + A = torch.square(S_re) + torch.square(S_im) + + A = torch.reshape(A, (-1, self.freq_dim)).float() + A_a = torch.matmul(A * B_U[0], self.U_a) + A_a = torch.reshape(A_a, (-1, self.hidden_dim)) + a = self.activation(A_a + self.b_a) + + o = self.inner_activation(x_o + torch.matmul(h_tm1 * B_U[0], self.U_o)) + + h = o * a + p = torch.matmul(h, self.W_p) + self.b_p + + self.states = [p, h, S_re, S_im, time, None, None, None] + self.states = [] + return self.fc_out(p).squeeze() + + def init_states(self, x): + reducer_f = torch.zeros((self.hidden_dim, self.freq_dim)).to(self.device) + reducer_p = torch.zeros((self.hidden_dim, self.output_dim)).to(self.device) + + init_state_h = torch.zeros(self.hidden_dim).to(self.device) + init_state_p = torch.matmul(init_state_h, reducer_p) + + init_state = torch.zeros_like(init_state_h).to(self.device) + init_freq = torch.matmul(init_state_h, reducer_f) + + init_state = torch.reshape(init_state, (-1, self.hidden_dim, 1)) + init_freq = torch.reshape(init_freq, (-1, 1, self.freq_dim)) + + init_state_S_re = init_state * init_freq + init_state_S_im = init_state * init_freq + + init_state_time = torch.tensor(0).to(self.device) + + self.states = [init_state_p, init_state_h, init_state_S_re, init_state_S_im, init_state_time, None, None, None] + + def get_constants(self, x): + constants = [] + constants.append([torch.tensor(1.).to(self.device) for _ in range(6)]) + constants.append([torch.tensor(1.).to(self.device) for _ in range(7)]) + array = np.array([float(ii)/self.freq_dim for ii in range(self.freq_dim)]) + constants.append(torch.tensor(array).to(self.device)) + + self.states[5:] = constants + +class SFM(Model): + """SFM Model + + Parameters + ---------- + input_dim : int + input dimension + output_dim : int + output dimension + lr : float + learning rate + lr_decay : float + learning rate decay + lr_decay_steps : int + learning rate decay steps + optimizer : str + optimizer name + GPU : str + the GPU ID(s) used for training + """ + + def __init__( + self, + d_feat=6, + hidden_size=64, + output_dim=1, + freq_dim = 10, + dropout_W=0.0, + dropout_U=0.0, + n_epochs=200, + lr=0.001, + batch_size=2000, + early_stop=20, + eval_steps=5, + loss="mse", + lr_decay=0.96, + lr_decay_steps=100, + optimizer="gd", + GPU="0", + seed=0, + **kwargs + ): + # Set logger. + self.logger = get_module_logger("SFM") + self.logger.info("SFM pytorch version...") + + # set hyper-parameters. + self.d_feat = d_feat + self.hidden_size = hidden_size + self.output_dim = output_dim + self.freq_dim = freq_dim + self.dropout_W = dropout_W + self.dropout_U = dropout_U + self.n_epochs = n_epochs + self.lr = lr + self.batch_size = batch_size + self.early_stop = early_stop + self.eval_steps = eval_steps + self.lr_decay = lr_decay + self.lr_decay_steps = lr_decay_steps + self.optimizer = optimizer.lower() + self.loss_type = loss + self.device = 'cuda:%d'%(GPU) if torch.cuda.is_available() else 'cpu' + self.use_gpu = torch.cuda.is_available() + self.seed = seed + + self.logger.info( + "SFM parameters setting:" + "\nd_feat : {}" + "\nhidden_size : {}" + "\nfreqency_dimension : {}" + "\ndropout_W: {}" + "\ndropout_U: {}" + "\nn_epochs : {}" + "\nlr : {}" + "\nbatch_size : {}" + "\nearly_stop : {}" + "\neval_steps : {}" + "\nlr_decay : {}" + "\nlr_decay_steps : {}" + "\noptimizer : {}" + "\nloss_type : {}" + "\nvisible_GPU : {}" + "\nuse_GPU : {}" + "\nseed : {}".format( + d_feat, + hidden_size, + freq_dim, + dropout_W, + dropout_U, + n_epochs, + lr, + batch_size, + early_stop, + eval_steps, + lr_decay, + lr_decay_steps, + optimizer.lower(), + loss, + GPU, + self.use_gpu, + seed, + ) + ) + + if loss not in {"mse", "binary"}: + raise NotImplementedError("loss {} is not supported!".format(loss)) + self._scorer = mean_squared_error if loss == "mse" else roc_auc_score + + self.sfm_model = SFM_Model( + d_feat=self.d_feat, + output_dim = self.output_dim, + hidden_size = self.hidden_size, + freq_dim = self.freq_dim, + dropout_W=self.dropout_W, + dropout_U = self.dropout_U, + device = self.device + ) + if optimizer.lower() == "adam": + self.train_optimizer = optim.Adam(self.sfm_model.parameters(), lr=self.lr) + elif optimizer.lower() == "gd": + self.train_optimizer = optim.SGD(self.sfm_model.parameters(), lr=self.lr) + else: + raise NotImplementedError("optimizer {} is not supported!".format(optimizer)) + + # Reduce learning rate when loss has stopped decrease + self.scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau( + self.train_optimizer, + mode="min", + factor=0.5, + patience=10, + verbose=True, + threshold=0.0001, + threshold_mode="rel", + cooldown=0, + min_lr=0.00001, + eps=1e-08, + ) + + self._fitted = False + self.sfm_model.to(self.device) + + def fit( + self, + dataset: DatasetH, + evals_result=dict(), + verbose=True, + save_path=None, + **kwargs + ): + + df_train, df_valid = dataset.prepare( + ["train", "valid"], col_set=["feature", "label"], data_key=DataHandlerLP.DK_L + ) + x_train, y_train = df_train["feature"], df_train["label"] + x_valid, y_valid = df_valid["feature"], df_valid["label"] + + save_path = create_save_path(save_path) + stop_steps = 0 + train_loss = 0 + best_loss = np.inf + evals_result["train"] = [] + evals_result["valid"] = [] + + # train + self.logger.info("training...") + self._fitted = True + + # prepare training data + x_train_values = torch.from_numpy(x_train.values).float() + y_train_values = torch.from_numpy(np.squeeze(y_train.values)).float() + train_num = y_train_values.shape[0] + + # prepare validation data + x_val_auto = torch.from_numpy(x_valid.values).float() + y_val_auto = torch.from_numpy(np.squeeze(y_valid.values)).float() + + x_val_auto = x_val_auto.to(self.device) + y_val_auto = y_val_auto.to(self.device) + + for step in range(self.n_epochs): + if stop_steps >= self.early_stop: + if verbose: + self.logger.info("\tearly stop") + break + loss = AverageMeter() + self.sfm_model.train() + self.train_optimizer.zero_grad() + + choice = np.random.choice(train_num, self.batch_size) + x_batch_auto = x_train_values[choice] + y_batch_auto = y_train_values[choice] + + x_batch_auto = x_batch_auto.to(self.device) + y_batch_auto = y_batch_auto.to(self.device) + + # forward + preds = self.sfm_model(x_batch_auto) + cur_loss = self.get_loss(preds, y_batch_auto, self.loss_type) + cur_loss.backward() + self.train_optimizer.step() + loss.update(cur_loss.item()) + + # validation + train_loss += loss.val + # print(loss.val) + if step and step % self.eval_steps == 0: + stop_steps += 1 + train_loss /= self.eval_steps + + with torch.no_grad(): + self.sfm_model.eval() + loss_val = AverageMeter() + + # forward + preds = self.sfm_model(x_val_auto) + cur_loss_val = self.get_loss(preds, y_val_auto, self.loss_type) + loss_val.update(cur_loss_val.item()) + + if verbose: + self.logger.info( + "[Epoch {}]: train_loss {:.6f}, valid_loss {:.6f}".format(step, train_loss, loss_val.val) + ) + evals_result["train"].append(train_loss) + evals_result["valid"].append(loss_val.val) + if loss_val.val < best_loss: + if verbose: + self.logger.info( + "\tvalid loss update from {:.6f} to {:.6f}, save checkpoint.".format( + best_loss, loss_val.val + ) + ) + best_loss = loss_val.val + stop_steps = 0 + torch.save(self.sfm_model.state_dict(), save_path) + train_loss = 0 + # update learning rate + self.scheduler.step(cur_loss_val) + + if device != 'cpu': + torch.cuda.empty_cache() + + def get_loss(self, pred, target, loss_type): + if loss_type == "mse": + sqr_loss = (pred - target)**2 + loss = sqr_loss.mean() + return loss + elif loss_type == "binary": + loss = nn.BCELoss() + return loss(pred, target) + else: + raise NotImplementedError("loss {} is not supported!".format(loss_type)) + + def predict(self, dataset): + if not self._fitted: + raise ValueError("model is not fitted yet!") + + x_test = dataset.prepare("test", col_set="feature") + index = x_test.index + x_test = torch.from_numpy(x_test.values).float() + + x_test = x_test.to(device) + self.sfm_model.eval() + + with torch.no_grad(): + if device != 'cpu': + preds = self.sfm_model(x_test).detach().cpu().numpy() + else: + preds = self.sfm_model(x_test).detach().numpy() + return pd.Series(preds, index=index) + + def save(self, filename, **kwargs): + with save_multiple_parts_file(filename) as model_dir: + model_path = os.path.join(model_dir, os.path.split(model_dir)[-1]) + # Save model + torch.save(self.sfm_model.state_dict(), model_path) + + def load(self, buffer, **kwargs): + with unpack_archive_with_buffer(buffer) as model_dir: + # Get model name + _model_name = os.path.splitext(list(filter(lambda x: x.startswith("model.bin"), os.listdir(model_dir)))[0])[ + 0 + ] + _model_path = os.path.join(model_dir, _model_name) + # Load model + self.sfm_model.load_state_dict(torch.load(_model_path)) + self._fitted = True + +class AverageMeter(object): + """Computes and stores the average and current value""" + def __init__(self): + self.reset() + + def reset(self): + self.val = 0 + self.avg = 0 + self.sum = 0 + self.count = 0 + + def update(self, val, n=1): + self.val = val + self.sum += val * n + self.count += n + self.avg = self.sum / self.count From e59f9e43a863bc46fe2e74a8e4a09b8fd4a12480 Mon Sep 17 00:00:00 2001 From: Alex Wang Date: Wed, 25 Nov 2020 14:02:40 +0800 Subject: [PATCH 099/241] workcode for SFM --- examples/workflow_by_code_sfm.py | 147 +++++++++++++++++++++++++++++++ 1 file changed, 147 insertions(+) create mode 100644 examples/workflow_by_code_sfm.py diff --git a/examples/workflow_by_code_sfm.py b/examples/workflow_by_code_sfm.py new file mode 100644 index 000000000..6a72db3a1 --- /dev/null +++ b/examples/workflow_by_code_sfm.py @@ -0,0 +1,147 @@ +# 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.pytorch_gru import GRU +from qlib.contrib.data.handler import ALPHA360_Denoise +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 +from qlib.utils import init_instance_by_config + +import pickle + +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": "2008-01-01", + "end_time": "2020-08-01", + "fit_start_time": "2008-01-01", + "fit_end_time": "2014-12-31", + "instruments": MARKET, + } + + TRAINER_CONFIG = { + "train_start_time": "2008-01-01", + "train_end_time": "2014-12-31", + "validate_start_time": "2015-01-01", + "validate_end_time": "2016-12-31", + "test_start_time": "2017-01-01", + "test_end_time": "2020-08-01", + } + + task = { + "model": { + "class": "SFM", + "module_path": "qlib.contrib.model.pytorch_sfm", + "kwargs": { + "d_feat": 6, + "hidden_size": 64, + "output_dim" : 1, + "freq_dim" : 15, + "dropout_W": 0.5, + "dropout_U": 0.5, + "n_epochs": 200, + "lr": 1e-3, + "batch_size": 800, + "early_stop": 20, + "eval_steps": 5, + "loss": "mse", + "lr_decay" : 0.96, + "lr_decay_steps" : 100, + "optimizer" : "gd", + "GPU": 1, + "seed": 0, + }, + }, + "dataset": { + "class": "DatasetH", + "module_path": "qlib.data.dataset", + "kwargs": { + "handler": { + "class": "ALPHA360_Denoise", + "module_path": "qlib.contrib.data.handler", + "kwargs": DATA_HANDLER_CONFIG, + }, + "segments": { + "train": ("2008-01-01", "2014-12-31"), + "valid": ("2015-01-01", "2016-12-31"), + "test": ("2017-01-01", "2020-08-01"), + }, + }, + } + # You shoud record the data in specific sequence + # "record": ['SignalRecord', 'SigAnaRecord', 'PortAnaRecord'], + } + + # model = train_model(task) + model = init_instance_by_config(task["model"]) + dataset = init_instance_by_config(task["dataset"]) + model.fit(dataset) + + pred_score = model.predict(dataset) + + # save pred_score to file + pred_score_path = Path("~/tmp/qlib/pred_score.pkl").expanduser() + pred_score_path.parent.mkdir(exist_ok=True, parents=True) + pred_score.to_pickle(pred_score_path) + + ################################### + # backtest + ################################### + STRATEGY_CONFIG = { + "topk": 50, + "n_drop": 5, + } + BACKTEST_CONFIG = { + "verbose": False, + "limit_threshold": 0.095, + "account": 100000000, + "benchmark": BENCHMARK, + "deal_price": "close", + "open_cost": 0.0005, + "close_cost": 0.0015, + "min_cost": 5, + } + + # use default strategy + # custom Strategy, refer to: TODO: Strategy API url + strategy = TopkDropoutStrategy(**STRATEGY_CONFIG) + report_normal, positions_normal = normal_backtest(pred_score, strategy=strategy, **BACKTEST_CONFIG) + + ################################### + # analyze + # If need a more detailed analysis, refer to: examples/train_and_bakctest.ipynb + ################################### + 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 + print(analysis_df) From dad09e91cf1463e3c8fd60ed4b5a3e194f7bab5c Mon Sep 17 00:00:00 2001 From: Alex Wang Date: Wed, 25 Nov 2020 14:10:46 +0800 Subject: [PATCH 100/241] update --- qlib/contrib/model/pytorch_sfm.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qlib/contrib/model/pytorch_sfm.py b/qlib/contrib/model/pytorch_sfm.py index 90c37fcdd..cf33732b9 100644 --- a/qlib/contrib/model/pytorch_sfm.py +++ b/qlib/contrib/model/pytorch_sfm.py @@ -229,7 +229,7 @@ class SFM(Model): "SFM parameters setting:" "\nd_feat : {}" "\nhidden_size : {}" - "\nfreqency_dimension : {}" + "\nfrequency_dimension : {}" "\ndropout_W: {}" "\ndropout_U: {}" "\nn_epochs : {}" From 6b90c6d066d313f0380c3a537769e977d7d0c4e6 Mon Sep 17 00:00:00 2001 From: Alex Wang Date: Wed, 25 Nov 2020 14:23:12 +0800 Subject: [PATCH 101/241] update --- qlib/contrib/model/pytorch_sfm.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qlib/contrib/model/pytorch_sfm.py b/qlib/contrib/model/pytorch_sfm.py index cf33732b9..1d3012331 100644 --- a/qlib/contrib/model/pytorch_sfm.py +++ b/qlib/contrib/model/pytorch_sfm.py @@ -398,7 +398,7 @@ class SFM(Model): # update learning rate self.scheduler.step(cur_loss_val) - if device != 'cpu': + if self.device != 'cpu': torch.cuda.empty_cache() def get_loss(self, pred, target, loss_type): From fcbafde741f8577284f36f5e3d7141d126a9b486 Mon Sep 17 00:00:00 2001 From: Alex Wang Date: Wed, 25 Nov 2020 14:27:19 +0800 Subject: [PATCH 102/241] update --- examples/workflow_by_code_sfm.py | 2 +- qlib/contrib/model/pytorch_sfm.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/workflow_by_code_sfm.py b/examples/workflow_by_code_sfm.py index 6a72db3a1..45f34f012 100644 --- a/examples/workflow_by_code_sfm.py +++ b/examples/workflow_by_code_sfm.py @@ -66,7 +66,7 @@ if __name__ == "__main__": "freq_dim" : 15, "dropout_W": 0.5, "dropout_U": 0.5, - "n_epochs": 200, + "n_epochs": 10, "lr": 1e-3, "batch_size": 800, "early_stop": 20, diff --git a/qlib/contrib/model/pytorch_sfm.py b/qlib/contrib/model/pytorch_sfm.py index 1d3012331..04a5ad33f 100644 --- a/qlib/contrib/model/pytorch_sfm.py +++ b/qlib/contrib/model/pytorch_sfm.py @@ -420,11 +420,11 @@ class SFM(Model): index = x_test.index x_test = torch.from_numpy(x_test.values).float() - x_test = x_test.to(device) + x_test = x_test.to(self.device) self.sfm_model.eval() with torch.no_grad(): - if device != 'cpu': + if self.device != 'cpu': preds = self.sfm_model(x_test).detach().cpu().numpy() else: preds = self.sfm_model(x_test).detach().numpy() From 3520d3b108c814f4be88204dfab778832bb2ec65 Mon Sep 17 00:00:00 2001 From: Jactus Date: Wed, 25 Nov 2020 14:58:23 +0800 Subject: [PATCH 103/241] Add SFM config --- README.md | 1 + examples/benchmarks/SFM/requirements.txt | 4 + .../benchmarks/SFM/workflow_config_sfm.yaml | 73 ++++++++++++++ examples/workflow_by_code_sfm.py | 10 +- qlib/contrib/model/pytorch_sfm.py | 94 +++++++++---------- requirements.txt | 3 +- 6 files changed, 131 insertions(+), 54 deletions(-) create mode 100644 examples/benchmarks/SFM/requirements.txt create mode 100644 examples/benchmarks/SFM/workflow_config_sfm.yaml diff --git a/README.md b/README.md index 7c7e58a1c..4383dea26 100644 --- a/README.md +++ b/README.md @@ -198,6 +198,7 @@ Here is a list of models built on `Qlib`. - [LSTM based on pytorcn](qlib/contrib/model/pytorch_lstm.py) - [GATs based on pytorch](qlib/contrib/model/pytorch_gats.py) - [TabNet based on pytorch](qlib/contrib/model/tabnet.py) +- [SFM based on pytorch](qlib/contrib/model/pytorch_sfm.py) Your PR of new Quant models is highly welcomed. diff --git a/examples/benchmarks/SFM/requirements.txt b/examples/benchmarks/SFM/requirements.txt new file mode 100644 index 000000000..6a3d13097 --- /dev/null +++ b/examples/benchmarks/SFM/requirements.txt @@ -0,0 +1,4 @@ +pandas==1.1.2 +numpy==1.17.4 +scikit_learn==0.23.2 +torch==1.7.0 \ No newline at end of file diff --git a/examples/benchmarks/SFM/workflow_config_sfm.yaml b/examples/benchmarks/SFM/workflow_config_sfm.yaml new file mode 100644 index 000000000..9086bab4a --- /dev/null +++ b/examples/benchmarks/SFM/workflow_config_sfm.yaml @@ -0,0 +1,73 @@ +provider_uri: "~/.qlib/qlib_data/cn_data" +region: cn +market: &market csi300 +benchmark: &benchmark SH000300 +data_handler_config: &data_handler_config + start_time: 2008-01-01 + end_time: 2020-08-01 + fit_start_time: 2008-01-01 + fit_end_time: 2014-12-31 + instruments: *market +port_analysis_config: &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 +task: + model: + class: SFM + module_path: qlib.contrib.model.pytorch_sfm + kwargs: + d_feat: 6 + hidden_size: 64 + output_dim: 1 + freq_dim: 15 + dropout_W: 0.5 + dropout_U: 0.5 + n_epochs: 10 + lr: 1e-3 + batch_size: 800 + early_stop: 20 + eval_steps: 5 + loss: mse + lr_decay: 0.96 + lr_decay_steps: 100 + optimizer: gd + GPU: 1 + seed: 0 + dataset: + class: DatasetH + module_path: qlib.data.dataset + kwargs: + handler: + class: ALPHA360_Denoise + module_path: qlib.contrib.data.handler + kwargs: *data_handler_config + segments: + train: [2008-01-01, 2014-12-31] + valid: [2015-01-01, 2016-12-31] + test: [2017-01-01, 2020-08-01] + record: + - class: SignalRecord + module_path: qlib.workflow.record_temp + kwargs: {} + - class: SigAnaRecord + module_path: qlib.workflow.record_temp + kwargs: + ana_long_short: False + ann_scaler: 252 + - class: PortAnaRecord + module_path: qlib.workflow.record_temp + kwargs: + config: *port_analysis_config \ No newline at end of file diff --git a/examples/workflow_by_code_sfm.py b/examples/workflow_by_code_sfm.py index 45f34f012..1942bfb33 100644 --- a/examples/workflow_by_code_sfm.py +++ b/examples/workflow_by_code_sfm.py @@ -62,8 +62,8 @@ if __name__ == "__main__": "kwargs": { "d_feat": 6, "hidden_size": 64, - "output_dim" : 1, - "freq_dim" : 15, + "output_dim": 1, + "freq_dim": 15, "dropout_W": 0.5, "dropout_U": 0.5, "n_epochs": 10, @@ -72,9 +72,9 @@ if __name__ == "__main__": "early_stop": 20, "eval_steps": 5, "loss": "mse", - "lr_decay" : 0.96, - "lr_decay_steps" : 100, - "optimizer" : "gd", + "lr_decay": 0.96, + "lr_decay_steps": 100, + "optimizer": "gd", "GPU": 1, "seed": 0, }, diff --git a/qlib/contrib/model/pytorch_sfm.py b/qlib/contrib/model/pytorch_sfm.py index 04a5ad33f..8564c491c 100644 --- a/qlib/contrib/model/pytorch_sfm.py +++ b/qlib/contrib/model/pytorch_sfm.py @@ -21,11 +21,12 @@ from ...model.base import Model from ...data.dataset import DatasetH from ...data.dataset.handler import DataHandlerLP + class SFM_Model(nn.Module): - def __init__(self, d_feat=6, output_dim = 1, freq_dim = 10, hidden_size = 64, dropout_W = 0.0, dropout_U = 0.0, device = "cpu"): + def __init__(self, d_feat=6, output_dim=1, freq_dim=10, hidden_size=64, dropout_W=0.0, dropout_U=0.0, device="cpu"): super().__init__() - self.input_dim = d_feat + self.input_dim = d_feat self.output_dim = output_dim self.freq_dim = freq_dim self.hidden_dim = hidden_size @@ -56,22 +57,22 @@ class SFM_Model(nn.Module): self.W_p = nn.Parameter(init.xavier_uniform_(torch.empty(self.hidden_dim, self.output_dim))) self.b_p = nn.Parameter(torch.zeros(self.output_dim)) - + self.activation = nn.Tanh() self.inner_activation = nn.Hardsigmoid() self.dropout_W, self.dropout_U = (dropout_W, dropout_U) self.fc_out = nn.Linear(self.output_dim, 1) self.states = [] - + def forward(self, input): - input = input.reshape(len(input), self.input_dim, -1) # [N, F, T] - input = input.permute(0, 2, 1) # [N, T, F] + input = input.reshape(len(input), self.input_dim, -1) # [N, F, T] + input = input.permute(0, 2, 1) # [N, T, F] time_step = input.shape[1] - + for ts in range(time_step): - x = input[:, ts,:] - if(len(self.states)==0): #hasn't initialized yet + x = input[:, ts, :] + if len(self.states) == 0: # hasn't initialized yet self.init_states(x) self.get_constants(x) p_tm1 = self.states[0] @@ -88,77 +89,79 @@ class SFM_Model(nn.Module): x_fre = torch.matmul(x * B_W[0], self.W_fre) + self.b_fre x_c = torch.matmul(x * B_W[0], self.W_c) + self.b_c x_o = torch.matmul(x * B_W[0], self.W_o) + self.b_o - - i = self.inner_activation(x_i + torch.matmul(h_tm1 * B_U[0], self.U_i)) # not sure whether I am doing in the right unsquuze - + + i = self.inner_activation( + x_i + torch.matmul(h_tm1 * B_U[0], self.U_i) + ) # not sure whether I am doing in the right unsquuze ste = self.inner_activation(x_ste + torch.matmul(h_tm1 * B_U[0], self.U_ste)) fre = self.inner_activation(x_fre + torch.matmul(h_tm1 * B_U[0], self.U_fre)) ste = torch.reshape(ste, (-1, self.hidden_dim, 1)) fre = torch.reshape(fre, (-1, 1, self.freq_dim)) - + f = ste * fre - + c = i * self.activation(x_c + torch.matmul(h_tm1 * B_U[0], self.U_c)) time = time_tm1 + 1 omega = torch.tensor(2 * np.pi) * time * frequency - re = torch.cos(omega) + re = torch.cos(omega) im = torch.sin(omega) - + c = torch.reshape(c, (-1, self.hidden_dim, 1)) S_re = f * S_re_tm1 + c * re S_im = f * S_im_tm1 + c * im - + A = torch.square(S_re) + torch.square(S_im) A = torch.reshape(A, (-1, self.freq_dim)).float() A_a = torch.matmul(A * B_U[0], self.U_a) A_a = torch.reshape(A_a, (-1, self.hidden_dim)) a = self.activation(A_a + self.b_a) - + o = self.inner_activation(x_o + torch.matmul(h_tm1 * B_U[0], self.U_o)) h = o * a p = torch.matmul(h, self.W_p) + self.b_p self.states = [p, h, S_re, S_im, time, None, None, None] - self.states = [] + self.states = [] return self.fc_out(p).squeeze() def init_states(self, x): reducer_f = torch.zeros((self.hidden_dim, self.freq_dim)).to(self.device) reducer_p = torch.zeros((self.hidden_dim, self.output_dim)).to(self.device) - + init_state_h = torch.zeros(self.hidden_dim).to(self.device) init_state_p = torch.matmul(init_state_h, reducer_p) - + init_state = torch.zeros_like(init_state_h).to(self.device) init_freq = torch.matmul(init_state_h, reducer_f) init_state = torch.reshape(init_state, (-1, self.hidden_dim, 1)) init_freq = torch.reshape(init_freq, (-1, 1, self.freq_dim)) - + init_state_S_re = init_state * init_freq init_state_S_im = init_state * init_freq - + init_state_time = torch.tensor(0).to(self.device) self.states = [init_state_p, init_state_h, init_state_S_re, init_state_S_im, init_state_time, None, None, None] def get_constants(self, x): constants = [] - constants.append([torch.tensor(1.).to(self.device) for _ in range(6)]) - constants.append([torch.tensor(1.).to(self.device) for _ in range(7)]) - array = np.array([float(ii)/self.freq_dim for ii in range(self.freq_dim)]) + constants.append([torch.tensor(1.0).to(self.device) for _ in range(6)]) + constants.append([torch.tensor(1.0).to(self.device) for _ in range(7)]) + array = np.array([float(ii) / self.freq_dim for ii in range(self.freq_dim)]) constants.append(torch.tensor(array).to(self.device)) self.states[5:] = constants + class SFM(Model): """SFM Model @@ -185,7 +188,7 @@ class SFM(Model): d_feat=6, hidden_size=64, output_dim=1, - freq_dim = 10, + freq_dim=10, dropout_W=0.0, dropout_U=0.0, n_epochs=200, @@ -221,7 +224,7 @@ class SFM(Model): self.lr_decay_steps = lr_decay_steps self.optimizer = optimizer.lower() self.loss_type = loss - self.device = 'cuda:%d'%(GPU) if torch.cuda.is_available() else 'cpu' + self.device = "cuda:%d" % (GPU) if torch.cuda.is_available() else "cpu" self.use_gpu = torch.cuda.is_available() self.seed = seed @@ -229,7 +232,7 @@ class SFM(Model): "SFM parameters setting:" "\nd_feat : {}" "\nhidden_size : {}" - "\nfrequency_dimension : {}" + "\nfrequency_dimension : {}" "\ndropout_W: {}" "\ndropout_U: {}" "\nn_epochs : {}" @@ -269,14 +272,14 @@ class SFM(Model): self._scorer = mean_squared_error if loss == "mse" else roc_auc_score self.sfm_model = SFM_Model( - d_feat=self.d_feat, - output_dim = self.output_dim, - hidden_size = self.hidden_size, - freq_dim = self.freq_dim, - dropout_W=self.dropout_W, - dropout_U = self.dropout_U, - device = self.device - ) + d_feat=self.d_feat, + output_dim=self.output_dim, + hidden_size=self.hidden_size, + freq_dim=self.freq_dim, + dropout_W=self.dropout_W, + dropout_U=self.dropout_U, + device=self.device, + ) if optimizer.lower() == "adam": self.train_optimizer = optim.Adam(self.sfm_model.parameters(), lr=self.lr) elif optimizer.lower() == "gd": @@ -301,14 +304,7 @@ class SFM(Model): self._fitted = False self.sfm_model.to(self.device) - def fit( - self, - dataset: DatasetH, - evals_result=dict(), - verbose=True, - save_path=None, - **kwargs - ): + def fit(self, dataset: DatasetH, evals_result=dict(), verbose=True, save_path=None, **kwargs): df_train, df_valid = dataset.prepare( ["train", "valid"], col_set=["feature", "label"], data_key=DataHandlerLP.DK_L @@ -398,12 +394,12 @@ class SFM(Model): # update learning rate self.scheduler.step(cur_loss_val) - if self.device != 'cpu': + if self.device != "cpu": torch.cuda.empty_cache() def get_loss(self, pred, target, loss_type): if loss_type == "mse": - sqr_loss = (pred - target)**2 + sqr_loss = (pred - target) ** 2 loss = sqr_loss.mean() return loss elif loss_type == "binary": @@ -424,7 +420,7 @@ class SFM(Model): self.sfm_model.eval() with torch.no_grad(): - if self.device != 'cpu': + if self.device != "cpu": preds = self.sfm_model(x_test).detach().cpu().numpy() else: preds = self.sfm_model(x_test).detach().numpy() @@ -447,8 +443,10 @@ class SFM(Model): self.sfm_model.load_state_dict(torch.load(_model_path)) self._fitted = True + class AverageMeter(object): """Computes and stores the average and current value""" + def __init__(self): self.reset() diff --git a/requirements.txt b/requirements.txt index 638ce22f4..d3511d780 100644 --- a/requirements.txt +++ b/requirements.txt @@ -22,4 +22,5 @@ scikit_learn==0.23.2 torch==1.6.0 tqdm==4.49.0 yahooquery==2.2.7 -mlflow==1.12.1 \ No newline at end of file +mlflow==1.12.1 +pytorch-tabnet==2.0.1 \ No newline at end of file From 1e46ad2fbef2a8713ef559407afe176752d3a137 Mon Sep 17 00:00:00 2001 From: LewenWang Date: Wed, 25 Nov 2020 16:35:28 +0800 Subject: [PATCH 104/241] Add readme for tabnet. --- examples/benchmarks/TabNet/README.md | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 examples/benchmarks/TabNet/README.md diff --git a/examples/benchmarks/TabNet/README.md b/examples/benchmarks/TabNet/README.md new file mode 100644 index 000000000..8f37104aa --- /dev/null +++ b/examples/benchmarks/TabNet/README.md @@ -0,0 +1,4 @@ +#TabNet +* TabNet is a novel high-performance and interpretable canonical deep tabular data learning architectur. TabNet uses sequential attention to choose which features to reason from at each decision step, enabling interpretability and more effcient learning as the learning capacity is used for the most salient features. +* The code used in Qlib is a pyTorch implementation of Tabnet (Arik, S. O., & Pfister, T. (2019). [https://github.com/dreamquark-ai/tabnet](https://github.com/dreamquark-ai/tabnet) +* Paper: TabNet: Attentive Interpretable Tabular Learning. [https://arxiv.org/pdf/1908.07442.pdf](https://arxiv.org/pdf/1908.07442.pdf). \ No newline at end of file From 5ac90f25b3bd786dd43075793bd550f812b98de9 Mon Sep 17 00:00:00 2001 From: LewenWang Date: Wed, 25 Nov 2020 16:37:19 +0800 Subject: [PATCH 105/241] Add readme for tabnet. --- examples/benchmarks/TabNet/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/benchmarks/TabNet/README.md b/examples/benchmarks/TabNet/README.md index 8f37104aa..3a233df46 100644 --- a/examples/benchmarks/TabNet/README.md +++ b/examples/benchmarks/TabNet/README.md @@ -1,4 +1,4 @@ -#TabNet +# TabNet * TabNet is a novel high-performance and interpretable canonical deep tabular data learning architectur. TabNet uses sequential attention to choose which features to reason from at each decision step, enabling interpretability and more effcient learning as the learning capacity is used for the most salient features. * The code used in Qlib is a pyTorch implementation of Tabnet (Arik, S. O., & Pfister, T. (2019). [https://github.com/dreamquark-ai/tabnet](https://github.com/dreamquark-ai/tabnet) * Paper: TabNet: Attentive Interpretable Tabular Learning. [https://arxiv.org/pdf/1908.07442.pdf](https://arxiv.org/pdf/1908.07442.pdf). \ No newline at end of file From 88b6fc4818e702694832452692bfe7847eb9b8fa Mon Sep 17 00:00:00 2001 From: LewenWang Date: Wed, 25 Nov 2020 17:29:05 +0800 Subject: [PATCH 106/241] Add readme. --- examples/benchmarks/CatBoost/README.md | 3 +++ examples/benchmarks/LightGBM/README.md | 4 ++++ examples/benchmarks/XGBoost/README.md | 3 +++ 3 files changed, 10 insertions(+) create mode 100644 examples/benchmarks/CatBoost/README.md create mode 100644 examples/benchmarks/LightGBM/README.md create mode 100644 examples/benchmarks/XGBoost/README.md diff --git a/examples/benchmarks/CatBoost/README.md b/examples/benchmarks/CatBoost/README.md new file mode 100644 index 000000000..5e4f3966f --- /dev/null +++ b/examples/benchmarks/CatBoost/README.md @@ -0,0 +1,3 @@ +# CatBoost +* Code: [https://github.com/catboost/catboost](https://github.com/catboost/catboost) +* Paper: CatBoost: unbiased boosting with categorical features. [https://proceedings.neurips.cc/paper/2018/file/14491b756b3a51daac41c24863285549-Paper.pdf](https://proceedings.neurips.cc/paper/2018/file/14491b756b3a51daac41c24863285549-Paper.pdf). \ No newline at end of file diff --git a/examples/benchmarks/LightGBM/README.md b/examples/benchmarks/LightGBM/README.md new file mode 100644 index 000000000..13f408d5f --- /dev/null +++ b/examples/benchmarks/LightGBM/README.md @@ -0,0 +1,4 @@ +# LightGBM +* Code: [https://github.com/microsoft/LightGBM](https://github.com/microsoft/LightGBM) +* Paper: LightGBM: A Highly Efficient Gradient Boosting +Decision Tree. [https://proceedings.neurips.cc/paper/2017/file/6449f44a102fde848669bdd9eb6b76fa-Paper.pdf](https://proceedings.neurips.cc/paper/2017/file/6449f44a102fde848669bdd9eb6b76fa-Paper.pdf). \ No newline at end of file diff --git a/examples/benchmarks/XGBoost/README.md b/examples/benchmarks/XGBoost/README.md new file mode 100644 index 000000000..33e04b23b --- /dev/null +++ b/examples/benchmarks/XGBoost/README.md @@ -0,0 +1,3 @@ +# XGBoost +* Code: [https://github.com/dmlc/xgboost](https://github.com/dmlc/xgboost) +* Paper: XGBoost: A Scalable Tree Boosting System. [https://dl.acm.org/doi/pdf/10.1145/2939672.2939785](https://dl.acm.org/doi/pdf/10.1145/2939672.2939785). \ No newline at end of file From c14a99a735cdfffc2d4f1fbdbb687269c71f7b85 Mon Sep 17 00:00:00 2001 From: zhupr Date: Wed, 25 Nov 2020 17:35:26 +0800 Subject: [PATCH 107/241] Fix TopkDropoutStrategy && dump_bin --- qlib/contrib/strategy/strategy.py | 168 ++++++++++++++++++++++-------- scripts/README.md | 4 + scripts/dump_bin.py | 10 +- 3 files changed, 136 insertions(+), 46 deletions(-) diff --git a/qlib/contrib/strategy/strategy.py b/qlib/contrib/strategy/strategy.py index 6eac9bafe..2fc5dbc0f 100644 --- a/qlib/contrib/strategy/strategy.py +++ b/qlib/contrib/strategy/strategy.py @@ -26,7 +26,7 @@ class BaseStrategy: def generate_order_list(self, score_series, current, trade_exchange, pred_date, trade_date): """ - Parameters: + Parameters ----------- score_series : pd.Seires stock_id , score @@ -46,7 +46,7 @@ class BaseStrategy: def update(self, score_series, pred_date, trade_date): """User can use this method to update strategy state each trade date. - Parameters: + Parameters ----------- score_series : pd.Series stock_id , score @@ -140,12 +140,15 @@ class WeightStrategyBase(BaseStrategy, AdjustTimer): def generate_target_weight_position(self, score, current, trade_date): """ - Parameters: + Parameters ----------- - score : pred score for this trade date, pd.Series, index is stock_id, contain 'score' column - current : current position, use Position() class - trade_exchange : Exchange() - trade_date : trade date + score : pd.Series + pred score for this trade date, index is stock_id, contain 'score' column + current : Position + current position, use Position() class + trade_exchange : Exchange + trade_date : str, pd.Timestamp + trade date generate target position from score for this date and the current position The cash is not considered in the position """ @@ -153,7 +156,7 @@ class WeightStrategyBase(BaseStrategy, AdjustTimer): def generate_order_list(self, score_series, current, trade_exchange, pred_date, trade_date): """ - Parameters: + Parameters ---------- score_series : pd.Seires stock_id , score @@ -186,16 +189,29 @@ class WeightStrategyBase(BaseStrategy, AdjustTimer): class TopkDropoutStrategy(BaseStrategy, ListAdjustTimer): - def __init__(self, topk, n_drop, method="bottom", risk_degree=0.95, thresh=1, hold_thresh=1, **kwargs): + def __init__( + self, + topk, + n_drop, + method_sell="bottom", + method_buy="top", + risk_degree=0.95, + thresh=1, + hold_thresh=1, + only_tradable=False, + **kwargs, + ): """ - Parameters: - ----------- + Parameters + ---------- topk : int The number of stocks in the portfolio n_drop : int number of stocks to be replaced in each trading date - method : str - dropout method, random/bottom + method_sell : str + dropout method_sell, random/bottom + method_buy : str + dropout method_buy, random/top risk_degree : float position percentage of total value thresh : int @@ -203,12 +219,19 @@ class TopkDropoutStrategy(BaseStrategy, ListAdjustTimer): hold_thresh : int minimum holding days before sell stock , will check current.get_stock_count(order.stock_id) >= self.thresh + only_tradable : bool + will the strategy only consider the tradable stock when buying and selling. + if only_tradable: + strategy will make buy sell decision without checking the tradable state of the stock + else: + strategy will make decision with the tradable state of the stock info and avoid buy and sell them """ super(TopkDropoutStrategy, self).__init__() ListAdjustTimer.__init__(self, kwargs.get("adjust_dates", None)) self.topk = topk self.n_drop = n_drop - self.method = method + self.method_sell = method_sell + self.method_buy = method_buy self.risk_degree = risk_degree self.thresh = thresh # self.stock_count['code'] will be the days the stock has been hold @@ -216,6 +239,7 @@ class TopkDropoutStrategy(BaseStrategy, ListAdjustTimer): self.stock_count = {} self.hold_thresh = hold_thresh + self.only_tradable = only_tradable def get_risk_degree(self, date): """get_risk_degree @@ -226,42 +250,102 @@ class TopkDropoutStrategy(BaseStrategy, ListAdjustTimer): return self.risk_degree def generate_order_list(self, score_series, current, trade_exchange, pred_date, trade_date): - """ - Gnererate order list according to score_series at trade_date, will not change current. - - Parameters: - ----------- - score_series : pd.Series - stock_id , score - current : Position() - current of account - trade_exchange : Exchange() - exchange - pred_date : pd.Timestamp - predict date - trade_date : pd.Timestamp - trade date + """Gnererate order list according to score_series at trade_date. + will not change current. + Parameters + ---------- + score_series : pd.Seires + stock_id , score + current : Position() + current of account + trade_exchange : Exchange() + exchange + pred_date : pd.Timestamp + predict date + trade_date : pd.Timestamp + trade date """ if not self.is_adjust(trade_date): return [] + + if self.only_tradable: + # If The strategy only consider tradable stock when make decision + # It needs following actions to filter stocks + def get_first_n(l, n, reverse=False): + cur_n = 0 + res = [] + for si in reversed(l) if reverse else l: + if trade_exchange.is_stock_tradable(stock_id=si, trade_date=trade_date): + res.append(si) + cur_n += 1 + if cur_n >= n: + break + return res[::-1] if reverse else res + + def get_last_n(l, n): + return get_first_n(l, n, reverse=True) + + def filter_stock(l): + return [si for si in l if trade_exchange.is_stock_tradable(stock_id=si, trade_date=trade_date)] + + else: + # Otherwise, the stock will make decision with out the stock tradable info + def get_first_n(l, n): + return list(l)[:n] + + def get_last_n(l, n): + return list(l)[-n:] + + def filter_stock(l): + return l + current_temp = copy.deepcopy(current) # generate order list for this adjust date sell_order_list = [] buy_order_list = [] # load score + cash = current_temp.get_cash() current_stock_list = current_temp.get_stock_list() + # last position (sorted by score) last = score_series.reindex(current_stock_list).sort_values(ascending=False).index - today = ( - score_series[~score_series.index.isin(last)] - .sort_values(ascending=False) - .index[: self.n_drop + self.topk - len(last)] - ) - comb = score_series.reindex(last.union(today)).sort_values(ascending=False).index - if self.method == "bottom": - sell = last[last.isin(comb[-self.n_drop :])] - elif self.method == "random": - sell = pd.Index(np.random.choice(last, self.n_drop) if len(last) else []) + # The new stocks today want to buy **at most** + if self.method_buy == "top": + today = get_first_n( + score_series[~score_series.index.isin(last)].sort_values(ascending=False).index, + self.n_drop + self.topk - len(last), + ) + elif self.method_buy == "random": + topk_candi = get_first_n(score_series.sort_values(ascending=False).index, self.topk) + candi = list(filter(lambda x: x not in last, topk_candi)) + n = self.n_drop + self.topk - len(last) + try: + today = np.random.choice(candi, n, replace=False) + except ValueError: + today = candi + else: + raise NotImplementedError(f"This type of input is not supported") + # combine(new stocks + last stocks), we will drop stocks from this list + # In case of dropping higher score stock and buying lower score stock. + comb = score_series.reindex(last.union(pd.Index(today))).sort_values(ascending=False).index + + # Get the stock list we really want to sell (After filtering the case that we sell high and buy low) + if self.method_sell == "bottom": + sell = last[last.isin(get_last_n(comb, self.n_drop))] + elif self.method_sell == "random": + candi = filter_stock(last) + try: + sell = pd.Index(np.random.choice(candi, self.n_drop, replace=False) if len(last) else []) + except ValueError: # No enough candidates + sell = candi + else: + raise NotImplementedError(f"This type of input is not supported") + + # Get the stock list we really want to buy buy = today[: len(sell) + self.topk - len(last)] + + # buy singal: if a stock falls into topk, it appear in the buy_sinal + buy_signal = score_series.sort_values(ascending=False).iloc[: self.topk].index + for code in current_stock_list: if not trade_exchange.is_stock_tradable(stock_id=code, trade_date=trade_date): continue @@ -285,12 +369,14 @@ class TopkDropoutStrategy(BaseStrategy, ListAdjustTimer): if trade_exchange.check_order(sell_order): sell_order_list.append(sell_order) trade_val, trade_cost, trade_price = trade_exchange.deal_order(sell_order, position=current_temp) + # update cash + cash += trade_val - trade_cost # sold del self.stock_count[code] else: # no buy signal, but the stock is kept self.stock_count[code] += 1 - elif code in buy: + elif code in buy_signal: # NOTE: This is different from the original version # get new buy signal # Only the stock fall in to topk will produce buy signal @@ -300,7 +386,7 @@ class TopkDropoutStrategy(BaseStrategy, ListAdjustTimer): # buy new stock # note the current has been changed current_stock_list = current_temp.get_stock_list() - value = current_temp.get_cash() * self.risk_degree / len(buy) if len(buy) > 0 else 0 + value = cash * self.risk_degree / len(buy) if len(buy) > 0 else 0 # open_cost should be considered in the real trading environment, while the backtest in evaluate.py does not consider it # as the aim of demo is to accomplish same strategy as evaluate.py, so comment out this line diff --git a/scripts/README.md b/scripts/README.md index 88ebdc680..99af4a457 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -43,6 +43,8 @@ python get_data.py qlib_data --help ### US data +> Need to download data first: [Downlaod US Data](#Downlaod-US-Data) + ```python import qlib from qlib.config import REG_US @@ -52,6 +54,8 @@ qlib.init(provider_uri=provider_uri, region=REG_US) ### CN data +> Need to download data first: [Download CN Data](#Download-CN-Data) + ```python import qlib from qlib.config import REG_CN diff --git a/scripts/dump_bin.py b/scripts/dump_bin.py index 2bca4f037..9f6dd88e2 100644 --- a/scripts/dump_bin.py +++ b/scripts/dump_bin.py @@ -140,7 +140,7 @@ class DumpDataBase: def _get_source_data(self, file_path: Path) -> pd.DataFrame: df = pd.read_csv(str(file_path.resolve()), low_memory=False) - df[self.date_field_name] = df[self.date_field_name].astype(np.datetime64) + df[self.date_field_name] = df[self.date_field_name].astype(str).astype(np.datetime64) # df.drop_duplicates([self.date_field_name], inplace=True) return df @@ -339,10 +339,10 @@ class DumpDataFix(DumpDataAll): def dump(self): self._calendars_list = self._read_calendars(self._calendars_dir.joinpath(f"{self.freq}.txt")) # noinspection PyAttributeOutsideInit - self._old_instruments = self._read_instruments( - self._instruments_dir.joinpath(self.INSTRUMENTS_FILE_NAME) - ).to_dict( - orient="index" + self._old_instruments = ( + self._read_instruments(self._instruments_dir.joinpath(self.INSTRUMENTS_FILE_NAME)) + .set_index([self.symbol_field_name]) + .to_dict(orient="index") ) # type: dict self._dump_instruments() self._dump_features() From cd7c81cfd0d6e18048eb3e5f4e89c1beff6ae7b4 Mon Sep 17 00:00:00 2001 From: meng-ustc Date: Wed, 25 Nov 2020 17:46:34 +0800 Subject: [PATCH 108/241] add pretrain-mode to gats --- .../benchmarks/GATs/worflow_config_gats.yaml | 5 +++-- examples/benchmarks/GRU/model_gru_csi300.pkl | Bin 0 -> 157578 bytes .../benchmarks/LSTM/model_lstm_csi300.pkl | Bin 0 -> 209290 bytes examples/workflow_by_code_gats.py | 5 +++-- qlib/contrib/model/pytorch_gats.py | 21 ++++++++++++++++++ 5 files changed, 27 insertions(+), 4 deletions(-) create mode 100644 examples/benchmarks/GRU/model_gru_csi300.pkl create mode 100644 examples/benchmarks/LSTM/model_lstm_csi300.pkl diff --git a/examples/benchmarks/GATs/worflow_config_gats.yaml b/examples/benchmarks/GATs/worflow_config_gats.yaml index 84eeff4db..37bced99d 100644 --- a/examples/benchmarks/GATs/worflow_config_gats.yaml +++ b/examples/benchmarks/GATs/worflow_config_gats.yaml @@ -37,9 +37,10 @@ task: lr: 1e-3 early_stop: 20 batch_size: 800 - metric: IC + metric: loss loss: mse - base_model: GRU + base_model: LSTM + with_pretrain: True seed: 0 GPU: 0 dataset: diff --git a/examples/benchmarks/GRU/model_gru_csi300.pkl b/examples/benchmarks/GRU/model_gru_csi300.pkl new file mode 100644 index 0000000000000000000000000000000000000000..46347ce8c748e6e790dee25da6d5f546b19568a3 GIT binary patch literal 157578 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≷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

8jr?!> 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 Date: Wed, 25 Nov 2020 17:48:12 +0800 Subject: [PATCH 109/241] update --- .../GATs/{worflow_config_gats.yaml => workflow_config_gats.yaml} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename examples/benchmarks/GATs/{worflow_config_gats.yaml => workflow_config_gats.yaml} (100%) diff --git a/examples/benchmarks/GATs/worflow_config_gats.yaml b/examples/benchmarks/GATs/workflow_config_gats.yaml similarity index 100% rename from examples/benchmarks/GATs/worflow_config_gats.yaml rename to examples/benchmarks/GATs/workflow_config_gats.yaml From b31480a06a01bdd376eaed0a1279ef49c2033b3c Mon Sep 17 00:00:00 2001 From: Jactus Date: Wed, 25 Nov 2020 18:03:52 +0800 Subject: [PATCH 110/241] Update doc strings and fix --- .../benchmarks/DNN/workflow_config_dnn.yaml | 2 +- qlib/contrib/evaluate.py | 9 ++- qlib/contrib/strategy/strategy.py | 35 +++++----- qlib/data/dataset/__init__.py | 65 ++++++++++++------- qlib/data/dataset/handler.py | 11 ++-- qlib/data/dataset/loader.py | 36 +++++----- 6 files changed, 94 insertions(+), 64 deletions(-) diff --git a/examples/benchmarks/DNN/workflow_config_dnn.yaml b/examples/benchmarks/DNN/workflow_config_dnn.yaml index e853726ca..0f9ae7254 100644 --- a/examples/benchmarks/DNN/workflow_config_dnn.yaml +++ b/examples/benchmarks/DNN/workflow_config_dnn.yaml @@ -30,7 +30,7 @@ task: module_path: qlib.contrib.model.pytorch_nn kwargs: loss: mse - input_dim: 360 + input_dim: 158 output_dim: 1 lr: 0.002 lr_decay: 0.96 diff --git a/qlib/contrib/evaluate.py b/qlib/contrib/evaluate.py index a9b08719a..cf1793c93 100644 --- a/qlib/contrib/evaluate.py +++ b/qlib/contrib/evaluate.py @@ -190,7 +190,8 @@ def backtest(pred, account=1e9, shift=1, benchmark="SH000905", verbose=True, **k Parameters ---------- - # backtest workflow related or commmon arguments + - **backtest workflow related or commmon arguments** + pred : pandas.DataFrame predict should has index and one `score` column account : float @@ -202,7 +203,8 @@ def backtest(pred, account=1e9, shift=1, benchmark="SH000905", verbose=True, **k verbose : bool whether to print log - # strategy related arguments + - **strategy related arguments** + strategy : Strategy() strategy used in backtest topk : int (Default value: 50) @@ -225,7 +227,8 @@ def backtest(pred, account=1e9, shift=1, benchmark="SH000905", verbose=True, **k str_type: 'amount', 'weight' or 'dropout' strategy type: TopkAmountStrategy ,TopkWeightStrategy or TopkDropoutStrategy - # exchange related arguments + - **exchange related arguments** + exchange: Exchange() pass the exchange for speeding up. subscribe_fields: list diff --git a/qlib/contrib/strategy/strategy.py b/qlib/contrib/strategy/strategy.py index 6eac9bafe..0e6a4ae2d 100644 --- a/qlib/contrib/strategy/strategy.py +++ b/qlib/contrib/strategy/strategy.py @@ -26,7 +26,9 @@ class BaseStrategy: def generate_order_list(self, score_series, current, trade_exchange, pred_date, trade_date): """ - Parameters: + DO NOT directly change the state of current + + Parameters ----------- score_series : pd.Seires stock_id , score @@ -39,14 +41,13 @@ class BaseStrategy: predict date trade_date : pd.Timestamp trade date - - DO NOT directly change the state of current """ pass def update(self, score_series, pred_date, trade_date): """User can use this method to update strategy state each trade date. - Parameters: + + Parameters ----------- score_series : pd.Series stock_id , score @@ -98,8 +99,9 @@ class AdjustTimer: """AdjustTimer Responsible for timing of position adjusting - This is designed as multiple inheritance mechanism due to + This is designed as multiple inheritance mechanism due to: - the is_adjust may need access to the internel state of a strategy + - it can be reguard as a enhancement to the existing strategy """ @@ -140,21 +142,24 @@ class WeightStrategyBase(BaseStrategy, AdjustTimer): def generate_target_weight_position(self, score, current, trade_date): """ - Parameters: + Generate target position from score for this date and the current position.The cash is not considered in the position + + Parameters ----------- - score : pred score for this trade date, pd.Series, index is stock_id, contain 'score' column - current : current position, use Position() class + score : pd.Series + pred score for this trade date, index is stock_id, contain 'score' column + current : Position() + current position trade_exchange : Exchange() - trade_date : trade date - generate target position from score for this date and the current position - The cash is not considered in the position + trade_date : pd.Timestamp + trade date """ raise NotImplementedError() def generate_order_list(self, score_series, current, trade_exchange, pred_date, trade_date): """ - Parameters: - ---------- + Parameters + ----------- score_series : pd.Seires stock_id , score current : Position() @@ -188,7 +193,7 @@ class WeightStrategyBase(BaseStrategy, AdjustTimer): class TopkDropoutStrategy(BaseStrategy, ListAdjustTimer): def __init__(self, topk, n_drop, method="bottom", risk_degree=0.95, thresh=1, hold_thresh=1, **kwargs): """ - Parameters: + Parameters ----------- topk : int The number of stocks in the portfolio @@ -229,7 +234,7 @@ class TopkDropoutStrategy(BaseStrategy, ListAdjustTimer): """ Gnererate order list according to score_series at trade_date, will not change current. - Parameters: + Parameters ----------- score_series : pd.Series stock_id , score diff --git a/qlib/data/dataset/__init__.py b/qlib/data/dataset/__init__.py index c46528944..3dbc17c23 100644 --- a/qlib/data/dataset/__init__.py +++ b/qlib/data/dataset/__init__.py @@ -14,9 +14,11 @@ class Dataset(Serializable): def __init__(self, *args, **kwargs): """ - init is designed to finish following steps + init is designed to finish following steps: + - setup data - The data related attributes' names should start with '_' so that it will not be saved on disk when serializing + - initialize the state of the dataset(info to prepare the data) - The name of essential state for preparing data should not start with '_' so that it could be serialized on disk when serializing. @@ -29,11 +31,15 @@ class Dataset(Serializable): """ setup the data - We split the setup_data function for following situation - - 1) User have a Dataset object with learned status on disk - - 2) User load the Dataset object from the disk(Note the init function is skiped) - - 3) User call `setup_data` to load new data - - 4) User prepare data for model based on previous status + We split the setup_data function for following situation: + + - User have a Dataset object with learned status on disk + + - User load the Dataset object from the disk(Note the init function is skiped) + + - User call `setup_data` to load new data + + - User prepare data for model based on previous status """ pass @@ -41,8 +47,9 @@ class Dataset(Serializable): """ The type of dataset depends on the model. (It could be pd.DataFrame, pytorch.DataLoader, etc.) The parameters should specify the scope for the prepared data - The method sould + The method should: - process the data + - return the processed data Returns @@ -55,11 +62,12 @@ class Dataset(Serializable): class DatasetH(Dataset): """ - Dataset with Data(H)anler + Dataset with Data(H)andler User should try to put the data preprocessing functions into handler. - Only following data processing functions should be placed in Dataset + Only following data processing functions should be placed in Dataset: - The processing is related to specific model. + - The processing is related to data split """ @@ -81,21 +89,26 @@ class DatasetH(Dataset): Parameters ---------- handler : Union[dict, DataHandler] - handler could be - 1) insntance of `DataHandler` - 2) config of `DataHandler`. Please refer to `DataHandler` + handler could be: + + - insntance of `DataHandler` + + - config of `DataHandler`. Please refer to `DataHandler` segments : list Describe the options to segment the data. - Here are some examples - 1) 'segments': { - 'train': ("2008-01-01", "2014-12-31"), - 'valid': ("2017-01-01", "2020-08-01",), - 'test': ("2015-01-01", "2016-12-31",), - } - 2) 'segments': { - 'insample': ("2008-01-01", "2014-12-31"), - 'outsample': ("2017-01-01", "2020-08-01",), - } + Here are some examples: + + .. code-block:: + + 1) 'segments': { + 'train': ("2008-01-01", "2014-12-31"), + 'valid': ("2017-01-01", "2020-08-01",), + 'test': ("2015-01-01", "2016-12-31",), + } + 2) 'segments': { + 'insample': ("2008-01-01", "2014-12-31"), + 'outsample': ("2017-01-01", "2020-08-01",), + } """ self._handler = init_instance_by_config(handler, accept_types=DataHandler) self._segments = segments.copy() @@ -114,9 +127,11 @@ class DatasetH(Dataset): ---------- segments : Union[List[str], Tuple[str], str, slice] Describe the scope of the data to be prepared - Here are some examples - 1) 'train' - 2) ['train', 'valid'] + Here are some examples: + + - 'train' + + - ['train', 'valid'] col_set : str The col_set will be passed to self._handler when fetching data data_key: str diff --git a/qlib/data/dataset/handler.py b/qlib/data/dataset/handler.py index e0a4d809a..4d3d88c38 100644 --- a/qlib/data/dataset/handler.py +++ b/qlib/data/dataset/handler.py @@ -41,7 +41,7 @@ class DataHandler(Serializable): Example of the data: The multi-index of the columns is optional. - .. code-block:: + .. code-block:: python feature label $close $volume Ref($close, 1) Mean($close, 3) $high-$low LABEL0 @@ -109,7 +109,8 @@ class DataHandler(Serializable): Parameters ---------- enable_cache : bool - default value is false + default value is false: + - if `enable_cache` == True: the processed data will be saved on disk, and handler will load the cached data from the disk directly @@ -378,8 +379,10 @@ class DataHandlerLP(DataHandler): init_type : str The type `IT_*` listed above enable_cache : bool - default value is false - if `enable_cache` == True: + default value is false: + + - if `enable_cache` == True: + the processed data will be saved on disk, and handler will load the cached data from the disk directly when we call `init` next time """ diff --git a/qlib/data/dataset/loader.py b/qlib/data/dataset/loader.py index e95dc4479..404313e80 100644 --- a/qlib/data/dataset/loader.py +++ b/qlib/data/dataset/loader.py @@ -39,14 +39,16 @@ class DataLoader(abc.ABC): pd.DataFrame: data load from the under layer source - Example of the data: - (The multi-index of the columns is optional.) - feature label - $close $volume Ref($close, 1) Mean($close, 3) $high-$low LABEL0 - datetime instrument - 2010-01-04 SH600000 81.807068 17145150.0 83.737389 83.016739 2.741058 0.0032 - SH600004 13.313329 11800983.0 13.313329 13.317701 0.183632 0.0042 - SH600005 37.796539 12231662.0 38.258602 37.919757 0.970325 0.0289 + Example of the data (The multi-index of the columns is optional.): + + .. code-block:: + + feature label + $close $volume Ref($close, 1) Mean($close, 3) $high-$low LABEL0 + datetime instrument + 2010-01-04 SH600000 81.807068 17145150.0 83.737389 83.016739 2.741058 0.0032 + SH600004 13.313329 11800983.0 13.313329 13.317701 0.183632 0.0042 + SH600005 37.796539 12231662.0 38.258602 37.919757 0.970325 0.0289 """ pass @@ -55,7 +57,7 @@ class DLWParser(DataLoader): """ (D)ata(L)oader (W)ith (P)arser for features and names - Extracting this class so that QlibDataLoader and other dataloaders(such as QdbDataLoader) can share the fields + Extracting this class so that QlibDataLoader and other dataloaders(such as QdbDataLoader) can share the fields. """ def __init__(self, config: Tuple[list, tuple, dict]): @@ -65,14 +67,16 @@ class DLWParser(DataLoader): config : Tuple[list, tuple, dict] Config will be used to describe the fields and column names - := { - "group_name1": - "group_name2": - } - or - := + .. code-block:: YAML - := ["expr", ...] | (["expr", ...], ["col_name", ...]) + := { + "group_name1": + "group_name2": + } + or + := + + := ["expr", ...] | (["expr", ...], ["col_name", ...]) """ self.is_group = isinstance(config, dict) From 53620d4c085d6e1b675823e2b8c12f668d819bd8 Mon Sep 17 00:00:00 2001 From: lwwang1995 Date: Wed, 25 Nov 2020 18:38:17 +0800 Subject: [PATCH 111/241] Add readme for SFM. --- examples/benchmarks/SFM/README.md | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 examples/benchmarks/SFM/README.md diff --git a/examples/benchmarks/SFM/README.md b/examples/benchmarks/SFM/README.md new file mode 100644 index 000000000..06ca50485 --- /dev/null +++ b/examples/benchmarks/SFM/README.md @@ -0,0 +1,4 @@ +# State-Frequency-Memory +- State Frequency Memory (SFM) is a novel recurrent network that uses Discrete Fourier Transform (DFT) to decompose the hidden states of memory cells and capture the multi-frequency trading patterns from past market data to make stock price predictions. +- The code used in Qlib is a pyTorch implementation of SFM (Zhang, L., Aggarwal, C., & Qi, G. J. (2017,)). +- Paper: Stock Price Prediction via Discovering Multi-Frequency Trading Patterns. https://www.cs.ucf.edu/~gqi/publications/kdd2017_stock.pdf. \ No newline at end of file From 8c4bc5be65796c941336ed902ce6b5198e3d6a43 Mon Sep 17 00:00:00 2001 From: lwwang1995 Date: Wed, 25 Nov 2020 18:44:35 +0800 Subject: [PATCH 112/241] Add copyright. --- qlib/contrib/model/catboost_model.py | 12 ++++++++++++ qlib/contrib/model/xgboost.py | 13 +++++++++++-- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/qlib/contrib/model/catboost_model.py b/qlib/contrib/model/catboost_model.py index e487a6d1e..bba006c35 100644 --- a/qlib/contrib/model/catboost_model.py +++ b/qlib/contrib/model/catboost_model.py @@ -1,3 +1,15 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + import numpy as np import pandas as pd from catboost import Pool, CatBoost diff --git a/qlib/contrib/model/xgboost.py b/qlib/contrib/model/xgboost.py index e0691ba16..b45e12e10 100755 --- a/qlib/contrib/model/xgboost.py +++ b/qlib/contrib/model/xgboost.py @@ -1,5 +1,14 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. import numpy as np import pandas as pd From 62ea2f89aef956b5086d55526177a997c127c8c1 Mon Sep 17 00:00:00 2001 From: Hong Zhang Date: Wed, 25 Nov 2020 19:03:16 +0800 Subject: [PATCH 113/241] hats3 --- examples/benchmarks/HATS/requirements.txt | 4 + .../benchmarks/HATS/worflow_config_hats.yaml | 64 +++ examples/workflow_by_code_hats.py | 145 +++++ qlib/contrib/model/pytorch_hats.py | 497 ++++++++++++++++++ 4 files changed, 710 insertions(+) create mode 100644 examples/benchmarks/HATS/requirements.txt create mode 100644 examples/benchmarks/HATS/worflow_config_hats.yaml create mode 100644 examples/workflow_by_code_hats.py create mode 100644 qlib/contrib/model/pytorch_hats.py diff --git a/examples/benchmarks/HATS/requirements.txt b/examples/benchmarks/HATS/requirements.txt new file mode 100644 index 000000000..16de0a438 --- /dev/null +++ b/examples/benchmarks/HATS/requirements.txt @@ -0,0 +1,4 @@ +pandas==1.1.2 +numpy==1.17.4 +scikit_learn==0.23.2 +torch==1.7.0 diff --git a/examples/benchmarks/HATS/worflow_config_hats.yaml b/examples/benchmarks/HATS/worflow_config_hats.yaml new file mode 100644 index 000000000..a7ab0d2d7 --- /dev/null +++ b/examples/benchmarks/HATS/worflow_config_hats.yaml @@ -0,0 +1,64 @@ +provider_uri: "~/.qlib/qlib_data/cn_data" +region: cn +market: &market csi300 +benchmark: &benchmark SH000300 +data_handler_config: &data_handler_config + start_time: 2008-01-01 + end_time: 2020-08-01 + fit_start_time: 2008-01-01 + fit_end_time: 2014-12-31 + instruments: *market +port_analysis_config: &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 +task: + model: + class: HATS + module_path: qlib.contrib.model.pytorch_gats + kwargs: + d_feat: 6 + hidden_size: 64 + num_layers: 2 + dropout: 0.6 + n_epochs: 200 + lr: 1e-3 + early_stop: 20 + batch_size: 800 + metric: IC + loss: mse + base_model: GRU + seed: 0 + GPU: 0 + dataset: + class: DatasetH + module_path: qlib.data.dataset + kwargs: + handler: + class: ALPHA360_Denoise + module_path: qlib.contrib.data.handler + kwargs: *data_handler_config + segments: + train: [2008-01-01, 2014-12-31] + valid: [2015-01-01, 2016-12-31] + test: [2017-01-01, 2020-08-01] + record: + - class: SignalRecord + module_path: qlib.workflow.record_temp + kwargs: {} + - class: PortAnaRecord + module_path: qlib.workflow.record_temp + kwargs: + config: *port_analysis_config \ No newline at end of file diff --git a/examples/workflow_by_code_hats.py b/examples/workflow_by_code_hats.py new file mode 100644 index 000000000..0cba29b63 --- /dev/null +++ b/examples/workflow_by_code_hats.py @@ -0,0 +1,145 @@ +# 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.pytorch_hats import HATS +from qlib.contrib.data.handler import ALPHA360_Denoise +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 + +# from qlib.model.learner import train_model +from qlib.utils import init_instance_by_config + +import pickle + +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_cn(target_dir=provider_uri) + + qlib.init(provider_uri=provider_uri, region=REG_CN) + + MARKET = "csi300" + BENCHMARK = "SH000300" + + ################################### + # train model + ################################### + DATA_HANDLER_CONFIG = { + "start_time": "2008-01-01", + "end_time": "2020-08-01", + "fit_start_time": "2008-01-01", + "fit_end_time": "2014-12-31", + "instruments": MARKET, + } + + TRAINER_CONFIG = { + "train_start_time": "2008-01-01", + "train_end_time": "2014-12-31", + "validate_start_time": "2015-01-01", + "validate_end_time": "2016-12-31", + "test_start_time": "2017-01-01", + "test_end_time": "2020-08-01", + } + + task = { + "model": { + "class": "HATS", + "module_path": "qlib.contrib.model.pytorch_hats", + "kwargs": { + "d_feat": 6, + "hidden_size": 64, + "num_layers": 2, + "dropout": 0.6, + "n_epochs": 200, + "lr": 1e-3, + "early_stop": 20, + "batch_size": 800, + "metric": "IC", + "loss": "mse", + "base_model": "LSTM", + "seed": 0, + "GPU": 0, + }, + }, + "dataset": { + "class": "DatasetH", + "module_path": "qlib.data.dataset", + "kwargs": { + "handler": { + "class": "ALPHA360_Denoise", + "module_path": "qlib.contrib.data.handler", + "kwargs": DATA_HANDLER_CONFIG, + }, + "segments": { + "train": ("2008-01-01", "2014-12-31"), + "valid": ("2015-01-01", "2016-12-31"), + "test": ("2017-01-01", "2020-08-01"), + }, + }, + } + # You shoud record the data in specific sequence + # "record": ['SignalRecord', 'SigAnaRecord', 'PortAnaRecord'], + } + + # model = train_model(task) + model = init_instance_by_config(task["model"]) + dataset = init_instance_by_config(task["dataset"]) + model.fit(dataset,save_path='benchmarks/HATS/model_hat.pkl') + + pred_score = model.predict(dataset) + + # save pred_score to file + pred_score_path = Path("~/tmp/qlib/pred_score.pkl").expanduser() + pred_score_path.parent.mkdir(exist_ok=True, parents=True) + pred_score.to_pickle(pred_score_path) + + ################################### + # backtest + ################################### + STRATEGY_CONFIG = { + "topk": 50, + "n_drop": 5, + } + BACKTEST_CONFIG = { + "verbose": False, + "limit_threshold": 0.095, + "account": 100000000, + "benchmark": BENCHMARK, + "deal_price": "close", + "open_cost": 0.0005, + "close_cost": 0.0015, + "min_cost": 5, + } + + # use default strategy + # custom Strategy, refer to: TODO: Strategy API url + strategy = TopkDropoutStrategy(**STRATEGY_CONFIG) + report_normal, positions_normal = normal_backtest(pred_score, strategy=strategy, **BACKTEST_CONFIG) + + ################################### + # analyze + # If need a more detailed analysis, refer to: examples/train_and_bakctest.ipynb + ################################### + 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 + print(analysis_df) diff --git a/qlib/contrib/model/pytorch_hats.py b/qlib/contrib/model/pytorch_hats.py new file mode 100644 index 000000000..6a09e685b --- /dev/null +++ b/qlib/contrib/model/pytorch_hats.py @@ -0,0 +1,497 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + + +from __future__ import division +from __future__ import print_function + +import os +import numpy as np +import pandas as pd +import copy +from sklearn.metrics import roc_auc_score, mean_squared_error +import logging +from ...utils import unpack_archive_with_buffer, save_multiple_parts_file, create_save_path, drop_nan_by_y_index +from ...log import get_module_logger, TimeInspector + +import torch +import torch.nn as nn +import torch.optim as optim + +from ...model.base import Model +from ...data.dataset import DatasetH +from ...data.dataset.handler import DataHandlerLP + + +class HATS(Model): + """HATS Model + + Parameters + ---------- + input_dim : int + input dimension + output_dim : int + output dimension + layers : tuple + layer sizes + lr : float + learning rate + optimizer : str + optimizer name + GPU : str + the GPU ID(s) used for training + """ + + def __init__( + self, + d_feat=6, + hidden_size=64, + num_layers=2, + dropout=0.5, + n_epochs=200, + lr=0.01, + metric="IC", + batch_size=800, + early_stop=20, + loss="mse", + base_model="GRU", + with_pretrain=True, + optimizer="adam", + GPU="0", + seed=0, + **kwargs + ): + # Set logger. + self.logger = get_module_logger("HATS") + self.logger.info("HATS pytorch version...") + + # set hyper-parameters. + self.d_feat = d_feat + self.hidden_size = hidden_size + self.num_layers = num_layers + self.dropout = dropout + self.n_epochs = n_epochs + self.lr = lr + self.metric = metric + self.batch_size = batch_size + self.early_stop = early_stop + self.optimizer = optimizer.lower() + self.loss = loss + self.base_model = base_model + self.with_pretrain = with_pretrain #### True if train HATS with pretrained base model + self.visible_GPU = GPU + self.use_gpu = torch.cuda.is_available() + self.seed = seed + + self.logger.info( + "HATS parameters setting:" + "\nd_feat : {}" + "\nhidden_size : {}" + "\nnum_layers : {}" + "\ndropout : {}" + "\nn_epochs : {}" + "\nlr : {}" + "\nmetric : {}" + "\nbatch_size : {}" + "\nearly_stop : {}" + "\noptimizer : {}" + "\nloss_type : {}" + "\nbase_model : {}" + "\nwith_pretrain : {}" ##### debug + "\nvisible_GPU : {}" + "\nuse_GPU : {}" + "\nseed : {}".format( + d_feat, + hidden_size, + num_layers, + dropout, + n_epochs, + lr, + metric, + batch_size, + early_stop, + optimizer.lower(), + loss, + base_model, + with_pretrain, ### debug + GPU, + self.use_gpu, + seed, + ) + ) + + if loss not in {"mse", "binary"}: + raise NotImplementedError("loss {} is not supported!".format(loss)) + self._scorer = mean_squared_error if loss == "mse" else roc_auc_score + + self.HATS_model = HATSModel( + d_feat=self.d_feat, + hidden_size=self.hidden_size, + num_layers=self.num_layers, + dropout=self.dropout, + base_model=self.base_model, + ) + if optimizer.lower() == "adam": + self.train_optimizer = optim.Adam(self.HATS_model.parameters(), lr=self.lr) + elif optimizer.lower() == "gd": + self.train_optimizer = optim.SGD(self.HATS_model.parameters(), lr=self.lr) + else: + raise NotImplementedError("optimizer {} is not supported!".format(optimizer)) + + self._fitted = False + if self.use_gpu: + self.HATS_model.cuda() + # set the visible GPU + if self.visible_GPU: + os.environ["CUDA_VISIBLE_DEVICES"] = self.visible_GPU + + def mse(self, pred, label): + loss = (pred - label) ** 2 + return torch.mean(loss) + + def loss_fn(self, pred, label): + mask = ~torch.isnan(label) + + if self.loss == "mse": + return self.mse(pred[mask], label[mask]) + + raise ValueError("unknown loss `%s`" % self.loss) + + def metric_fn(self, pred, label): + + mask = torch.isfinite(label) + if self.metric == "IC": + return self.cal_ic(pred[mask], label[mask]) + + if self.metric == "" or self.metric == "loss": # use loss + return -self.loss_fn(pred[mask], label[mask]) + + raise ValueError("unknown metric `%s`" % self.metric) + + def cal_ic(self, pred, label): + return torch.mean(pred * label) + + def train_epoch(self, x_train, y_train): + + x_train_values = x_train.values + y_train_values = np.squeeze(y_train.values) * 100 + + self.HATS_model.train() + + indices = np.arange(len(x_train_values)) + np.random.shuffle(indices) + + for i in range(len(indices))[:: self.batch_size]: + + if len(indices) - i < self.batch_size: + break + + feature = torch.from_numpy(x_train_values[indices[i : i + self.batch_size]]).float() + label = torch.from_numpy(y_train_values[indices[i : i + self.batch_size]]).float() + + if self.use_gpu: + feature = feature.cuda() + label = label.cuda() + + pred = self.HATS_model(feature) + loss = self.loss_fn(pred, label) + + self.train_optimizer.zero_grad() + loss.backward() + torch.nn.utils.clip_grad_value_(self.HATS_model.parameters(), 3.0) + self.train_optimizer.step() + + def test_epoch(self, data_x, data_y): + + # prepare training data + x_values = data_x.values + y_values = np.squeeze(data_y.values) + + self.HATS_model.eval() + + scores = [] + losses = [] + + indices = np.arange(len(x_values)) + np.random.shuffle(indices) + + for i in range(len(indices))[:: self.batch_size]: + + if len(indices) - i < self.batch_size: + break + + feature = torch.from_numpy(x_values[indices[i : i + self.batch_size]]).float() + label = torch.from_numpy(y_values[indices[i : i + self.batch_size]]).float() + + if self.use_gpu: + feature = feature.cuda() + label = label.cuda() + + pred = self.HATS_model(feature) + loss = self.loss_fn(pred, label) + losses.append(loss.item()) + + score = self.metric_fn(pred, label) + scores.append(score.item()) + + return np.mean(losses), np.mean(scores) + + def fit( + self, + dataset: DatasetH, + evals_result=dict(), + verbose=True, + save_path=None, + ): + + df_train, df_valid, df_test = dataset.prepare( + ["train", "valid", "test"], col_set=["feature", "label"], data_key=DataHandlerLP.DK_L + ) + + x_train, y_train = df_train["feature"], df_train["label"] + x_valid, y_valid = df_valid["feature"], df_valid["label"] + + if save_path == None: + save_path = create_save_path(save_path) + stop_steps = 0 + train_loss = 0 + best_score = -np.inf + best_epoch = 0 + evals_result["train"] = [] + evals_result["valid"] = [] + + # load pretrained base_model + if self.with_pretrain: + self.logger.info("loading pretrained model...") + if self.base_model == "LSTM": + from ...contrib.model.pytorch_lstm import LSTMModel + pretrained_model = LSTMModel() + pretrained_model.load_state_dict(torch.load('benchmarks/LSTM/model_lstm_csi300.pkl')) + elif self.base_model == "GRU": + from ...contrib.model.pytorch_gru import GRUModel + pretrained_model = GRUModel() + pretrained_model.load_state_dict(torch.load('benchmarks/GRU/model_gru_csi300.pkl')) + model_dict = self.HATS_model.state_dict() + + # filter unnecessary parameters + pretrained_dict = {k: v for k, v in pretrained_model.state_dict().items() if k in model_dict} + # overwrite entries in the existing state dict + model_dict.update(pretrained_dict) + # load the new state dict + self.HATS_model.load_state_dict(model_dict) + self.logger.info("loading pretrained model Done...") + + + # train + self.logger.info("training...") + self._fitted = True + # return + + for step in range(self.n_epochs): + self.logger.info("Epoch%d:", step) + self.logger.info("training...") + self.train_epoch(x_train, y_train) + self.logger.info("evaluating...") + train_loss, train_score = self.test_epoch(x_train, y_train) + val_loss, val_score = self.test_epoch(x_valid, y_valid) + self.logger.info("train %.6f, valid %.6f" % (train_score, val_score)) + evals_result["train"].append(train_score) + evals_result["valid"].append(val_score) + + if val_score > best_score: + best_score = val_score + stop_steps = 0 + best_epoch = step + best_param = copy.deepcopy(self.HATS_model.state_dict()) + else: + stop_steps += 1 + if stop_steps >= self.early_stop: + self.logger.info("early stop") + break + + self.logger.info("best score: %.6lf @ %d" % (best_score, best_epoch)) + self.HATS_model.load_state_dict(best_param) + torch.save(best_param, save_path) + + if self.use_gpu: + torch.cuda.empty_cache() + + def predict(self, dataset): + if not self._fitted: + raise ValueError("model is not fitted yet!") + + x_test = dataset.prepare("test", col_set="feature") + index = x_test.index + self.HATS_model.eval() + x_values = x_test.values + sample_num = x_values.shape[0] + preds = [] + + for begin in range(sample_num)[:: self.batch_size]: + + if sample_num - begin < self.batch_size: + end = sample_num + else: + end = begin + self.batch_size + + x_batch = torch.from_numpy(x_values[begin:end]).float() + + if self.use_gpu: + x_batch = x_batch.cuda() + + with torch.no_grad(): + if self.use_gpu: + pred = self.HATS_model(x_batch).detach().cpu().numpy() + else: + pred = self.HATS_model(x_batch).detach().numpy() + + preds.append(pred) + + return pd.Series(np.concatenate(preds), index=index) + + +class HATSModel(nn.Module): + def __init__(self, d_feat=6, hidden_size=64, num_layers=2, dropout=0.0, base_model="GRU"): + super().__init__() + + if base_model == "GRU": + self.model = nn.GRU( + input_size=d_feat, + hidden_size=hidden_size, + num_layers=num_layers, + batch_first=True, + dropout=dropout, + ) + elif base_model == "LSTM": + self.model = nn.LSTM( + input_size=d_feat, + hidden_size=hidden_size, + num_layers=num_layers, + batch_first=True, + dropout=dropout, + ) + else: + raise ValueError("unknown base model name `%s`" % base_model) + + self.hidden_size = hidden_size + self.bn1 = nn.BatchNorm1d(num_features=hidden_size, track_running_stats=False) + self.fc = nn.Linear(hidden_size, hidden_size) + self.bn2 = nn.BatchNorm1d(num_features=hidden_size, track_running_stats=False) + self.fc_out = nn.Linear(hidden_size, 1) + self.leaky_relu = nn.LeakyReLU() + self.softmax = nn.Softmax(dim=1) + self.d_feat = d_feat + + num_head_att = [1]*num_layers + hidden_dim = [hidden_size]*num_layers + dims = [d_feat] + [d*nh for (d, nh) in zip(hidden_dim, num_head_att[:-1])] + [num_head_att[-1]] + in_dims = dims[:-1] + out_dims = [d // nh for (d, nh) in zip(dims[1:], num_head_att)] + self.attn = nn.ModuleList([GraphAttention(i, o, nh, dropout) for (i, o, nh) in zip(in_dims, out_dims,num_head_att)]) + self.bns = nn.ModuleList([nn.BatchNorm1d(dim) for dim in dims[1:-1]]) + self.dropout = nn.Dropout(dropout) + self.elu = nn.ELU() + + def forward(self, x): + x = x.reshape(len(x), self.d_feat, -1) # [N, F, T] + x = x.permute(0, 2, 1) # [N, T, F] + out,_ = self.model(x) + hidden = out[:, -1, :] + hidden = self.bn1(hidden) + attention = GraphAttention.cal_attention(hidden, hidden) + output = attention.mm(hidden) + output = self.fc(output) + output = self.bn2(output) + output = self.leaky_relu(output) + return self.fc_out(output).squeeze() + + + +class GraphAttention(nn.Module): + + def __init__(self, input_dim, output_dim, num_heads, dropout=0.5): + + super().__init__() + + """ + Parameters + ---------- + input_dim : int + Dimension of input node features. + output_dim : int + Dimension of output node features. + num_heads : list of ints + Number of attention heads in each hidden layer and output layer. Must be non empty. Note that len(num_heads) = len(hidden_dims)+1. + dropout : float + Dropout rate. Default: 0.5. + """ + + self.input_dim = input_dim + self.output_dim = output_dim + self.num_heads = num_heads + + self.fcs = nn.ModuleList([nn.Linear(input_dim, output_dim) for _ in range(num_heads)]) + self.a = nn.ModuleList([nn.Linear(2*output_dim, 1) for _ in range(num_heads)]) + + self.dropout = nn.Dropout(dropout) + self.softmax = nn.Softmax(dim=0) + self.leakyrelu = nn.LeakyReLU() + + def forward(self, features, nodes, mapping, rows): + + """ + Parameters + ---------- + features : torch.Tensor + An (n' x input_dim) tensor of input node features. + node_layers : list of numpy array + node_layers[i] is an array of the nodes in the ith layer of the + computation graph. + mappings : list of dictionary + mappings[i] is a dictionary mapping node v (labelled 0 to |V|-1) + in node_layers[i] to its position in node_layers[i]. For example, + if node_layers[i] = [2,5], then mappings[i][2] = 0 and + mappings[i][5] = 1. + rows : numpy array + rows[i] is an array of neighbors of node i. + Returns + ------- + out : torch.Tensor + An (len(node_layers[-1]) x output_dim) tensor of output node features. + """ + + nprime = features.shape[0] + rows = [np.array([mapping[v] for v in row], dtype=np.int64) for row in rows] + sum_degs = np.hstack(([0], np.cumsum([len(row) for row in rows]))) + mapped_nodes = [mapping[v] for v in nodes] + indices = torch.LongTensor([[v, c] for (v, row) in zip(mapped_nodes, rows) for c in row]).t() + + + out = [] + for k in range(self.num_heads): + h = self.fcs[k](features) + + nbr_h = torch.cat(tuple([h[row] for row in rows]), dim=0) + self_h = torch.cat(tuple([h[mapping[nodes[i]]].repeat(len(row), 1) for (i, row) in enumerate(rows)]), dim=0) + cat_h = torch.cat((self_h, nbr_h), dim=1) + + e = self.leakyrelu(self.a[k](cat_h)) + + alpha = [self.softmax(e[lo : hi]) for (lo, hi) in zip(sum_degs, sum_degs[1:])] + alpha = torch.cat(tuple(alpha), dim=0) + alpha = alpha.squeeze(1) + alpha = self.dropout(alpha) + + adj = torch.sparse.FloatTensor(indices, alpha, torch.Size([nprime, nprime])) + out.append(torch.sparse.mm(adj, h)[mapped_nodes]) + + return out + + def cal_attention(x, y): + + att_x = torch.mean(x, dim = 1).reshape(-1, 1) + att_y = torch.mean(y, dim = 1).reshape(-1, 1) + att = att_x.mm(torch.t(att_y)) + x_att = x.reshape(x.shape[0], 1, x.shape[1]).repeat(1, y.shape[0], 1) + y_att = y.reshape(1, y.shape[0], y.shape[1]).repeat(x.shape[0], 1, 1) + return torch.mean(x.reshape(x.shape[0], 1, x.shape[1]).repeat(1, y.shape[0], 1)*y.reshape(1, y.shape[0], y.shape[1]).repeat(x.shape[0], 1, 1), dim = 2)-att \ No newline at end of file From 33a40c290fadbe45ebef647f5eb175117636f876 Mon Sep 17 00:00:00 2001 From: Don-ustc <43958178+Don-ustc@users.noreply.github.com> Date: Wed, 25 Nov 2020 19:12:26 +0800 Subject: [PATCH 114/241] Update about ALSTM --- examples/workflow_by_code_alstm.py | 145 +++++++++++++++++++++++++++++ 1 file changed, 145 insertions(+) create mode 100644 examples/workflow_by_code_alstm.py diff --git a/examples/workflow_by_code_alstm.py b/examples/workflow_by_code_alstm.py new file mode 100644 index 000000000..3137b6605 --- /dev/null +++ b/examples/workflow_by_code_alstm.py @@ -0,0 +1,145 @@ +# 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.pytorch_alstm import ALSTM +from qlib.contrib.data.handler import ALPHA360_Denoise +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 + +# from qlib.model.learner import train_model +from qlib.utils import init_instance_by_config + +import pickle + +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": "2008-01-01", + "end_time": "2020-08-01", + "fit_start_time": "2008-01-01", + "fit_end_time": "2014-12-31", + "instruments": MARKET, + } + + TRAINER_CONFIG = { + "train_start_time": "2008-01-01", + "train_end_time": "2014-12-31", + "validate_start_time": "2015-01-01", + "validate_end_time": "2016-12-31", + "test_start_time": "2017-01-01", + "test_end_time": "2020-08-01", + } + + task = { + "model": { + "class": "ALSTM", + "module_path": "qlib.contrib.model.pytorch_alstm", + "kwargs": { + "d_feat": 6, + "hidden_size": 64, + "num_layers": 2, + "dropout": 0.0, + "n_epochs": 200, + "lr": 1e-3, + "early_stop": 20, + "batch_size": 800, + "metric": "IC", + "loss": "mse", + "seed": 0, + "GPU": 0, + "rnn_type": "GRU" + }, + }, + "dataset": { + "class": "DatasetH", + "module_path": "qlib.data.dataset", + "kwargs": { + "handler": { + "class": "ALPHA360_Denoise", + "module_path": "qlib.contrib.data.handler", + "kwargs": DATA_HANDLER_CONFIG, + }, + "segments": { + "train": ("2008-01-01", "2014-12-31"), + "valid": ("2015-01-01", "2016-12-31"), + "test": ("2017-01-01", "2020-08-01"), + }, + }, + } + # You shoud record the data in specific sequence + # "record": ['SignalRecord', 'SigAnaRecord', 'PortAnaRecord'], + } + + # model = train_model(task) + model = init_instance_by_config(task["model"]) + dataset = init_instance_by_config(task["dataset"]) + model.fit(dataset) + + pred_score = model.predict(dataset) + + # save pred_score to file + pred_score_path = Path("~/tmp/qlib/pred_score.pkl").expanduser() + pred_score_path.parent.mkdir(exist_ok=True, parents=True) + pred_score.to_pickle(pred_score_path) + + ################################### + # backtest + ################################### + STRATEGY_CONFIG = { + "topk": 50, + "n_drop": 5, + } + BACKTEST_CONFIG = { + "verbose": False, + "limit_threshold": 0.095, + "account": 100000000, + "benchmark": BENCHMARK, + "deal_price": "close", + "open_cost": 0.0005, + "close_cost": 0.0015, + "min_cost": 5, + } + + # use default strategy + # custom Strategy, refer to: TODO: Strategy API url + strategy = TopkDropoutStrategy(**STRATEGY_CONFIG) + report_normal, positions_normal = normal_backtest(pred_score, strategy=strategy, **BACKTEST_CONFIG) + + ################################### + # analyze + # If need a more detailed analysis, refer to: examples/train_and_bakctest.ipynb + ################################### + 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 + print(analysis_df) \ No newline at end of file From 05599d1de8ec7b0e48ba7b71a2905027d40f2666 Mon Sep 17 00:00:00 2001 From: Don-ustc <43958178+Don-ustc@users.noreply.github.com> Date: Wed, 25 Nov 2020 19:13:53 +0800 Subject: [PATCH 115/241] Update about ALSTM --- qlib/contrib/model/pytorch_alstm.py | 392 ++++++++++++++++++++++++++++ 1 file changed, 392 insertions(+) create mode 100644 qlib/contrib/model/pytorch_alstm.py diff --git a/qlib/contrib/model/pytorch_alstm.py b/qlib/contrib/model/pytorch_alstm.py new file mode 100644 index 000000000..b302925ec --- /dev/null +++ b/qlib/contrib/model/pytorch_alstm.py @@ -0,0 +1,392 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + + +from __future__ import division +from __future__ import print_function + +import os +import numpy as np +import pandas as pd +import copy +from sklearn.metrics import roc_auc_score, mean_squared_error +import logging +from ...utils import unpack_archive_with_buffer, save_multiple_parts_file, create_save_path, drop_nan_by_y_index +from ...log import get_module_logger, TimeInspector + +import torch +import torch.nn as nn +import torch.optim as optim + +from ...model.base import Model +from ...data.dataset import DatasetH +from ...data.dataset.handler import DataHandlerLP + + +class ALSTM(Model): + """ALSTM Model + + Parameters + ---------- + input_dim : int + input dimension + output_dim : int + output dimension + layers : tuple + layer sizes + lr : float + learning rate + optimizer : str + optimizer name + GPU : str + the GPU ID(s) used for training + """ + + def __init__( + self, + d_feat=6, + hidden_size=64, + num_layers=2, + dropout=0.0, + n_epochs=200, + lr=0.001, + metric="IC", + batch_size=2000, + early_stop=20, + loss="mse", + optimizer="adam", + GPU="0", + seed=0, + rnn_type="GRU", + **kwargs + ): + # Set logger. + self.logger = get_module_logger("ALSTM") + self.logger.info("ALSTM pytorch version...") + + # set hyper-parameters. + self.d_feat = d_feat + self.hidden_size = hidden_size + self.num_layers = num_layers + self.dropout = dropout + self.n_epochs = n_epochs + self.lr = lr + self.metric = metric + self.batch_size = batch_size + self.early_stop = early_stop + self.optimizer = optimizer.lower() + self.loss = loss + self.visible_GPU = GPU + self.use_gpu = torch.cuda.is_available() + self.seed = seed + self.rnn_type = rnn_type + + self.logger.info( + "ALSTM parameters setting:" + "\nd_feat : {}" + "\nhidden_size : {}" + "\nnum_layers : {}" + "\ndropout : {}" + "\nn_epochs : {}" + "\nlr : {}" + "\nmetric : {}" + "\nbatch_size : {}" + "\nearly_stop : {}" + "\noptimizer : {}" + "\nloss_type : {}" + "\nvisible_GPU : {}" + "\nuse_GPU : {}" + "\nseed : {}" + "\nrnn_type : {}".format( + d_feat, + hidden_size, + num_layers, + dropout, + n_epochs, + lr, + metric, + batch_size, + early_stop, + optimizer.lower(), + loss, + GPU, + self.use_gpu, + seed, + self.rnn_type, + ) + ) + + if loss not in {"mse", "binary"}: + raise NotImplementedError("loss {} is not supported!".format(loss)) + self._scorer = mean_squared_error if loss == "mse" else roc_auc_score + + self.alstm_model = ALSTMModel( + d_feat=self.d_feat, hidden_size=self.hidden_size, num_layers=self.num_layers, dropout=self.dropout + ) + # def __init__(self, d_feat=6, hidden_size=64, num_layers=2, dropout=0.0, input_day=20, rnn_type="GRU"): + + if optimizer.lower() == "adam": + self.train_optimizer = optim.Adam(self.alstm_model.parameters(), lr=self.lr) + elif optimizer.lower() == "gd": + self.train_optimizer = optim.SGD(self.alstm_model.parameters(), lr=self.lr) + else: + raise NotImplementedError("optimizer {} is not supported!".format(optimizer)) + + self._fitted = False + if self.use_gpu: + self.alstm_model.cuda() + # set the visible GPU + if self.visible_GPU: + os.environ["CUDA_VISIBLE_DEVICES"] = self.visible_GPU + + def mse(self, pred, label): + loss = (pred - label) ** 2 + return torch.mean(loss) + + def loss_fn(self, pred, label): + mask = ~torch.isnan(label) + + if self.loss == "mse": + return self.mse(pred[mask], label[mask]) + + raise ValueError("unknown loss `%s`" % self.loss) + + def metric_fn(self, pred, label): + + mask = torch.isfinite(label) + if self.metric == "IC": + return self.cal_ic(pred[mask], label[mask]) + + if self.metric == "" or self.metric == "loss": # use loss + return -self.loss_fn(pred[mask], label[mask]) + + raise ValueError("unknown metric `%s`" % self.metric) + + def cal_ic(self, pred, label): + return torch.mean(pred * label) + + def train_epoch(self, x_train, y_train): + + x_train_values = x_train.values + y_train_values = np.squeeze(y_train.values) * 100 + + self.alstm_model.train() + + indices = np.arange(len(x_train_values)) + np.random.shuffle(indices) + + for i in range(len(indices))[:: self.batch_size]: + + if len(indices) - i < self.batch_size: + break + + feature = torch.from_numpy(x_train_values[indices[i : i + self.batch_size]]).float() + label = torch.from_numpy(y_train_values[indices[i : i + self.batch_size]]).float() + + if self.use_gpu: + feature = feature.cuda() + label = label.cuda() + + pred = self.alstm_model(feature) + loss = self.loss_fn(pred, label) + + self.train_optimizer.zero_grad() + loss.backward() + torch.nn.utils.clip_grad_value_(self.alstm_model.parameters(), 3.0) + self.train_optimizer.step() + + def test_epoch(self, data_x, data_y): + + # prepare training data + x_values = data_x.values + y_values = np.squeeze(data_y.values) + + self.alstm_model.eval() + + scores = [] + losses = [] + + indices = np.arange(len(x_values)) + np.random.shuffle(indices) + + for i in range(len(indices))[:: self.batch_size]: + + if len(indices) - i < self.batch_size: + break + + feature = torch.from_numpy(x_values[indices[i : i + self.batch_size]]).float() + label = torch.from_numpy(y_values[indices[i : i + self.batch_size]]).float() + + if self.use_gpu: + feature = feature.cuda() + label = label.cuda() + + pred = self.alstm_model(feature) + loss = self.loss_fn(pred, label) + losses.append(loss.item()) + + score = self.metric_fn(pred, label) + scores.append(score.item()) + + return np.mean(losses), np.mean(scores) + + def fit( + self, + dataset: DatasetH, + evals_result=dict(), + verbose=True, + save_path=None, + ): + + df_train, df_valid, df_test = dataset.prepare( + ["train", "valid", "test"], col_set=["feature", "label"], data_key=DataHandlerLP.DK_L + ) + + x_train, y_train = df_train["feature"], df_train["label"] + x_valid, y_valid = df_valid["feature"], df_valid["label"] + + if save_path == None: + save_path = create_save_path(save_path) + stop_steps = 0 + train_loss = 0 + best_score = -np.inf + best_epoch = 0 + evals_result["train"] = [] + evals_result["valid"] = [] + + # train + self.logger.info("training...") + self._fitted = True + # return + + for step in range(self.n_epochs): + self.logger.info("Epoch%d:", step) + self.logger.info("training...") + self.train_epoch(x_train, y_train) + self.logger.info("evaluating...") + train_loss, train_score = self.test_epoch(x_train, y_train) + val_loss, val_score = self.test_epoch(x_valid, y_valid) + self.logger.info("train %.6f, valid %.6f" % (train_score, val_score)) + evals_result["train"].append(train_score) + evals_result["valid"].append(val_score) + + if val_score > best_score: + best_score = val_score + stop_steps = 0 + best_epoch = step + best_param = copy.deepcopy(self.alstm_model.state_dict()) + else: + stop_steps += 1 + if stop_steps >= self.early_stop: + self.logger.info("early stop") + break + + self.logger.info("best score: %.6lf @ %d" % (best_score, best_epoch)) + self.alstm_model.load_state_dict(best_param) + torch.save(best_param, save_path) + + if self.use_gpu: + torch.cuda.empty_cache() + + def predict(self, dataset): + if not self._fitted: + raise ValueError("model is not fitted yet!") + + x_test = dataset.prepare("test", col_set="feature") + index = x_test.index + self.alstm_model.eval() + x_values = x_test.values + sample_num = x_values.shape[0] + preds = [] + + for begin in range(sample_num)[:: self.batch_size]: + + if sample_num - begin < self.batch_size: + end = sample_num + else: + end = begin + self.batch_size + + x_batch = torch.from_numpy(x_values[begin:end]).float() + + if self.use_gpu: + x_batch = x_batch.cuda() + + with torch.no_grad(): + if self.use_gpu: + pred = self.alstm_model(x_batch).detach().cpu().numpy() + else: + pred = self.alstm_model(x_batch).detach().numpy() + + preds.append(pred) + + return pd.Series(np.concatenate(preds), index=index) + + +class GRUModel(nn.Module): + def __init__(self, d_feat=6, hidden_size=64, num_layers=2, dropout=0.0): + super().__init__() + + self.rnn = nn.GRU( + input_size=d_feat, + hidden_size=hidden_size, + num_layers=num_layers, + batch_first=True, + dropout=dropout, + ) + self.fc_out = nn.Linear(hidden_size, 1) + + self.d_feat = d_feat + + def forward(self, x): + # x: [N, F*T] + x = x.reshape(len(x), self.d_feat, -1) # [N, F, T] + x = x.permute(0, 2, 1) # [N, T, F] + out, _ = self.rnn(x) + return self.fc_out(out[:, -1, :]).squeeze() + + + +class ALSTMModel(nn.Module): + def __init__(self, d_feat=6, hidden_size=64, num_layers=2, dropout=0.0, rnn_type="GRU"): + super().__init__() + self.hid_size = hidden_size + self.input_size = d_feat + self.dropout = dropout + self.rnn_type = rnn_type + self.rnn_layer = num_layers + self._build_model() + + def _build_model(self): + try: + klass = getattr(nn, self.rnn_type.upper()) + except: + raise ValueError('unknown rnn_type `%s`' % self.rnn_type) + self.net = nn.Sequential() + self.net.add_module('fc_in', nn.Linear(in_features=self.input_size, out_features=self.hid_size)) + self.net.add_module('act', nn.Tanh()) + self.rnn = klass(input_size=self.hid_size, + hidden_size=self.hid_size, + num_layers=self.rnn_layer, + batch_first=True, + dropout=self.dropout) + self.fc_out = nn.Linear(in_features=self.hid_size*2, out_features=1) + # self.fc_out = nn.Linear(in_features=self.hid_size, out_features=1) + self.att_net = nn.Sequential() + self.att_net.add_module('att_fc_in', nn.Linear(in_features=self.hid_size, out_features=int(self.hid_size/2))) + self.att_net.add_module('att_dropout', torch.nn.Dropout(self.dropout)) + self.att_net.add_module('att_act', nn.Tanh()) + self.att_net.add_module('att_fc_out', nn.Linear(in_features=int(self.hid_size/2), out_features=1, bias=False)) + self.att_net.add_module('att_softmax', nn.Softmax(dim=1)) + + def forward(self, inputs): + # inputs: [batch_size, input_size*input_day] + inputs = inputs.view(len(inputs), self.input_size, -1) + inputs = inputs.permute(0, 2, 1) # [batch, input_size, seq_len] -> [batch, seq_len, input_size] + rnn_out, _ = self.rnn(self.net(inputs)) # [batch, seq_len, num_directions * hidden_size] + attention_score = self.att_net(rnn_out) # [batch, seq_len, 1] + out_att = torch.mul(rnn_out, attention_score) + out_att = torch.sum(out_att, dim=1) + out = self.fc_out(torch.cat((rnn_out[:, -1, :], out_att), dim=1)) # [batch, seq_len, num_directions * hidden_size] -> [batch, 1] + # out = self.fc_out(rnn_out[:, -1, :] + out_att) + return out[..., 0] + From a99db6a1dc6afb3df12a7ece52375e535204ae65 Mon Sep 17 00:00:00 2001 From: Jactus Date: Wed, 25 Nov 2020 19:29:30 +0800 Subject: [PATCH 116/241] Add ALSTM config --- README.md | 4 +- examples/benchmarks/ALSTM/requirements.txt | 4 ++ .../ALSTM/workflow_config_alstm.yaml | 69 +++++++++++++++++++ examples/workflow_by_code_alstm.py | 4 +- examples/workflow_by_code_hats.py | 2 +- qlib/contrib/evaluate.py | 2 +- qlib/contrib/model/pytorch_alstm.py | 42 +++++------ qlib/contrib/model/pytorch_gats.py | 6 +- qlib/contrib/model/pytorch_hats.py | 55 ++++++++------- qlib/data/dataset/__init__.py | 4 +- 10 files changed, 139 insertions(+), 53 deletions(-) create mode 100644 examples/benchmarks/ALSTM/requirements.txt create mode 100644 examples/benchmarks/ALSTM/workflow_config_alstm.yaml diff --git a/README.md b/README.md index 4383dea26..cd0c8542f 100644 --- a/README.md +++ b/README.md @@ -196,10 +196,12 @@ Here is a list of models built on `Qlib`. - [MLP based on pytorch](qlib/contrib/model/pytorch_nn.py) - [GRU based on pytorch](qlib/contrib/model/pytorch_gru.py) - [LSTM based on pytorcn](qlib/contrib/model/pytorch_lstm.py) +- [ALSTM based on pytorcn](qlib/contrib/model/pytorch_alstm.py) - [GATs based on pytorch](qlib/contrib/model/pytorch_gats.py) - [TabNet based on pytorch](qlib/contrib/model/tabnet.py) - [SFM based on pytorch](qlib/contrib/model/pytorch_sfm.py) - +- [HATs based on pytorch](qlib/contrib/model/pytorch_hats.py) +- [TFT based on tensorflow](examples/benchmarks/TFT/tft.py) Your PR of new Quant models is highly welcomed. diff --git a/examples/benchmarks/ALSTM/requirements.txt b/examples/benchmarks/ALSTM/requirements.txt new file mode 100644 index 000000000..1fc2779c0 --- /dev/null +++ b/examples/benchmarks/ALSTM/requirements.txt @@ -0,0 +1,4 @@ +numpy==1.17.4 +pandas==1.1.2 +scikit_learn==0.23.2 +torch==1.7.0 diff --git a/examples/benchmarks/ALSTM/workflow_config_alstm.yaml b/examples/benchmarks/ALSTM/workflow_config_alstm.yaml new file mode 100644 index 000000000..bb35b6da5 --- /dev/null +++ b/examples/benchmarks/ALSTM/workflow_config_alstm.yaml @@ -0,0 +1,69 @@ +provider_uri: "~/.qlib/qlib_data/cn_data" +region: cn +market: &market csi300 +benchmark: &benchmark SH000300 +data_handler_config: &data_handler_config + start_time: 2008-01-01 + end_time: 2020-08-01 + fit_start_time: 2008-01-01 + fit_end_time: 2014-12-31 + instruments: *market +port_analysis_config: &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 +task: + model: + class: ALSTM + module_path: qlib.contrib.model.pytorch_alstm + kwargs: + d_feat: 6 + hidden_size: 64 + num_layers: 2 + dropout: 0.0 + n_epochs: 200 + lr: 1e-3 + early_stop: 20 + batch_size: 800 + metric: IC + loss: mse + seed: 0 + GPU: 0 + rnn_type: GRU + dataset: + class: DatasetH + module_path: qlib.data.dataset + kwargs: + handler: + class: ALPHA360_Denoise + module_path: qlib.contrib.data.handler + kwargs: *data_handler_config + segments: + train: [2008-01-01, 2014-12-31] + valid: [2015-01-01, 2016-12-31] + test: [2017-01-01, 2020-08-01] + record: + - class: SignalRecord + module_path: qlib.workflow.record_temp + kwargs: {} + - class: SigAnaRecord + module_path: qlib.workflow.record_temp + kwargs: + ana_long_short: False + ann_scaler: 252 + - class: PortAnaRecord + module_path: qlib.workflow.record_temp + kwargs: + config: *port_analysis_config \ No newline at end of file diff --git a/examples/workflow_by_code_alstm.py b/examples/workflow_by_code_alstm.py index 3137b6605..eabce3b07 100644 --- a/examples/workflow_by_code_alstm.py +++ b/examples/workflow_by_code_alstm.py @@ -74,7 +74,7 @@ if __name__ == "__main__": "loss": "mse", "seed": 0, "GPU": 0, - "rnn_type": "GRU" + "rnn_type": "GRU", }, }, "dataset": { @@ -142,4 +142,4 @@ if __name__ == "__main__": report_normal["return"] - report_normal["bench"] - report_normal["cost"] ) analysis_df = pd.concat(analysis) # type: pd.DataFrame - print(analysis_df) \ No newline at end of file + print(analysis_df) diff --git a/examples/workflow_by_code_hats.py b/examples/workflow_by_code_hats.py index 0cba29b63..3ea81ba49 100644 --- a/examples/workflow_by_code_hats.py +++ b/examples/workflow_by_code_hats.py @@ -100,7 +100,7 @@ if __name__ == "__main__": # model = train_model(task) model = init_instance_by_config(task["model"]) dataset = init_instance_by_config(task["dataset"]) - model.fit(dataset,save_path='benchmarks/HATS/model_hat.pkl') + model.fit(dataset, save_path="benchmarks/HATS/model_hat.pkl") pred_score = model.predict(dataset) diff --git a/qlib/contrib/evaluate.py b/qlib/contrib/evaluate.py index cf1793c93..2b85f1a9b 100644 --- a/qlib/contrib/evaluate.py +++ b/qlib/contrib/evaluate.py @@ -228,7 +228,7 @@ def backtest(pred, account=1e9, shift=1, benchmark="SH000905", verbose=True, **k strategy type: TopkAmountStrategy ,TopkWeightStrategy or TopkDropoutStrategy - **exchange related arguments** - + exchange: Exchange() pass the exchange for speeding up. subscribe_fields: list diff --git a/qlib/contrib/model/pytorch_alstm.py b/qlib/contrib/model/pytorch_alstm.py index b302925ec..bdf1e3ea0 100644 --- a/qlib/contrib/model/pytorch_alstm.py +++ b/qlib/contrib/model/pytorch_alstm.py @@ -345,7 +345,6 @@ class GRUModel(nn.Module): return self.fc_out(out[:, -1, :]).squeeze() - class ALSTMModel(nn.Module): def __init__(self, d_feat=6, hidden_size=64, num_layers=2, dropout=0.0, rnn_type="GRU"): super().__init__() @@ -360,33 +359,36 @@ class ALSTMModel(nn.Module): try: klass = getattr(nn, self.rnn_type.upper()) except: - raise ValueError('unknown rnn_type `%s`' % self.rnn_type) + raise ValueError("unknown rnn_type `%s`" % self.rnn_type) self.net = nn.Sequential() - self.net.add_module('fc_in', nn.Linear(in_features=self.input_size, out_features=self.hid_size)) - self.net.add_module('act', nn.Tanh()) - self.rnn = klass(input_size=self.hid_size, - hidden_size=self.hid_size, - num_layers=self.rnn_layer, - batch_first=True, - dropout=self.dropout) - self.fc_out = nn.Linear(in_features=self.hid_size*2, out_features=1) + self.net.add_module("fc_in", nn.Linear(in_features=self.input_size, out_features=self.hid_size)) + self.net.add_module("act", nn.Tanh()) + self.rnn = klass( + input_size=self.hid_size, + hidden_size=self.hid_size, + num_layers=self.rnn_layer, + batch_first=True, + dropout=self.dropout, + ) + self.fc_out = nn.Linear(in_features=self.hid_size * 2, out_features=1) # self.fc_out = nn.Linear(in_features=self.hid_size, out_features=1) self.att_net = nn.Sequential() - self.att_net.add_module('att_fc_in', nn.Linear(in_features=self.hid_size, out_features=int(self.hid_size/2))) - self.att_net.add_module('att_dropout', torch.nn.Dropout(self.dropout)) - self.att_net.add_module('att_act', nn.Tanh()) - self.att_net.add_module('att_fc_out', nn.Linear(in_features=int(self.hid_size/2), out_features=1, bias=False)) - self.att_net.add_module('att_softmax', nn.Softmax(dim=1)) + self.att_net.add_module("att_fc_in", nn.Linear(in_features=self.hid_size, out_features=int(self.hid_size / 2))) + self.att_net.add_module("att_dropout", torch.nn.Dropout(self.dropout)) + self.att_net.add_module("att_act", nn.Tanh()) + self.att_net.add_module("att_fc_out", nn.Linear(in_features=int(self.hid_size / 2), out_features=1, bias=False)) + self.att_net.add_module("att_softmax", nn.Softmax(dim=1)) def forward(self, inputs): # inputs: [batch_size, input_size*input_day] inputs = inputs.view(len(inputs), self.input_size, -1) - inputs = inputs.permute(0, 2, 1) # [batch, input_size, seq_len] -> [batch, seq_len, input_size] - rnn_out, _ = self.rnn(self.net(inputs)) # [batch, seq_len, num_directions * hidden_size] - attention_score = self.att_net(rnn_out) # [batch, seq_len, 1] + inputs = inputs.permute(0, 2, 1) # [batch, input_size, seq_len] -> [batch, seq_len, input_size] + rnn_out, _ = self.rnn(self.net(inputs)) # [batch, seq_len, num_directions * hidden_size] + attention_score = self.att_net(rnn_out) # [batch, seq_len, 1] out_att = torch.mul(rnn_out, attention_score) out_att = torch.sum(out_att, dim=1) - out = self.fc_out(torch.cat((rnn_out[:, -1, :], out_att), dim=1)) # [batch, seq_len, num_directions * hidden_size] -> [batch, 1] + out = self.fc_out( + torch.cat((rnn_out[:, -1, :], out_att), dim=1) + ) # [batch, seq_len, num_directions * hidden_size] -> [batch, 1] # out = self.fc_out(rnn_out[:, -1, :] + out_att) return out[..., 0] - diff --git a/qlib/contrib/model/pytorch_gats.py b/qlib/contrib/model/pytorch_gats.py index 77e3b9de9..07af4eda4 100755 --- a/qlib/contrib/model/pytorch_gats.py +++ b/qlib/contrib/model/pytorch_gats.py @@ -265,12 +265,14 @@ class GAT(Model): self.logger.info("Loading pretrained model...") if self.base_model == "LSTM": from ...contrib.model.pytorch_lstm import LSTMModel + pretrained_model = LSTMModel() - pretrained_model.load_state_dict(torch.load('benchmarks/LSTM/model_lstm_csi300.pkl')) + pretrained_model.load_state_dict(torch.load("benchmarks/LSTM/model_lstm_csi300.pkl")) elif self.base_model == "GRU": from ...contrib.model.pytorch_gru import GRUModel + pretrained_model = GRUModel() - pretrained_model.load_state_dict(torch.load('benchmarks/GRU/model_gru_csi300.pkl')) + pretrained_model.load_state_dict(torch.load("benchmarks/GRU/model_gru_csi300.pkl")) model_dict = self.GAT_model.state_dict() pretrained_dict = {k: v for k, v in pretrained_model.state_dict().items() if k in model_dict} model_dict.update(pretrained_dict) diff --git a/qlib/contrib/model/pytorch_hats.py b/qlib/contrib/model/pytorch_hats.py index 6a09e685b..7b4307e25 100644 --- a/qlib/contrib/model/pytorch_hats.py +++ b/qlib/contrib/model/pytorch_hats.py @@ -78,7 +78,7 @@ class HATS(Model): self.optimizer = optimizer.lower() self.loss = loss self.base_model = base_model - self.with_pretrain = with_pretrain #### True if train HATS with pretrained base model + self.with_pretrain = with_pretrain #### True if train HATS with pretrained base model self.visible_GPU = GPU self.use_gpu = torch.cuda.is_available() self.seed = seed @@ -97,7 +97,7 @@ class HATS(Model): "\noptimizer : {}" "\nloss_type : {}" "\nbase_model : {}" - "\nwith_pretrain : {}" ##### debug + "\nwith_pretrain : {}" ##### debug "\nvisible_GPU : {}" "\nuse_GPU : {}" "\nseed : {}".format( @@ -113,7 +113,7 @@ class HATS(Model): optimizer.lower(), loss, base_model, - with_pretrain, ### debug + with_pretrain, ### debug GPU, self.use_gpu, seed, @@ -265,12 +265,14 @@ class HATS(Model): self.logger.info("loading pretrained model...") if self.base_model == "LSTM": from ...contrib.model.pytorch_lstm import LSTMModel + pretrained_model = LSTMModel() - pretrained_model.load_state_dict(torch.load('benchmarks/LSTM/model_lstm_csi300.pkl')) + pretrained_model.load_state_dict(torch.load("benchmarks/LSTM/model_lstm_csi300.pkl")) elif self.base_model == "GRU": from ...contrib.model.pytorch_gru import GRUModel + pretrained_model = GRUModel() - pretrained_model.load_state_dict(torch.load('benchmarks/GRU/model_gru_csi300.pkl')) + pretrained_model.load_state_dict(torch.load("benchmarks/GRU/model_gru_csi300.pkl")) model_dict = self.HATS_model.state_dict() # filter unnecessary parameters @@ -281,7 +283,6 @@ class HATS(Model): self.HATS_model.load_state_dict(model_dict) self.logger.info("loading pretrained model Done...") - # train self.logger.info("training...") self._fitted = True @@ -382,22 +383,24 @@ class HATSModel(nn.Module): self.softmax = nn.Softmax(dim=1) self.d_feat = d_feat - num_head_att = [1]*num_layers - hidden_dim = [hidden_size]*num_layers - dims = [d_feat] + [d*nh for (d, nh) in zip(hidden_dim, num_head_att[:-1])] + [num_head_att[-1]] + num_head_att = [1] * num_layers + hidden_dim = [hidden_size] * num_layers + dims = [d_feat] + [d * nh for (d, nh) in zip(hidden_dim, num_head_att[:-1])] + [num_head_att[-1]] in_dims = dims[:-1] out_dims = [d // nh for (d, nh) in zip(dims[1:], num_head_att)] - self.attn = nn.ModuleList([GraphAttention(i, o, nh, dropout) for (i, o, nh) in zip(in_dims, out_dims,num_head_att)]) + self.attn = nn.ModuleList( + [GraphAttention(i, o, nh, dropout) for (i, o, nh) in zip(in_dims, out_dims, num_head_att)] + ) self.bns = nn.ModuleList([nn.BatchNorm1d(dim) for dim in dims[1:-1]]) self.dropout = nn.Dropout(dropout) self.elu = nn.ELU() def forward(self, x): - x = x.reshape(len(x), self.d_feat, -1) # [N, F, T] - x = x.permute(0, 2, 1) # [N, T, F] - out,_ = self.model(x) + x = x.reshape(len(x), self.d_feat, -1) # [N, F, T] + x = x.permute(0, 2, 1) # [N, T, F] + out, _ = self.model(x) hidden = out[:, -1, :] - hidden = self.bn1(hidden) + hidden = self.bn1(hidden) attention = GraphAttention.cal_attention(hidden, hidden) output = attention.mm(hidden) output = self.fc(output) @@ -406,9 +409,7 @@ class HATSModel(nn.Module): return self.fc_out(output).squeeze() - class GraphAttention(nn.Module): - def __init__(self, input_dim, output_dim, num_heads, dropout=0.5): super().__init__() @@ -431,7 +432,7 @@ class GraphAttention(nn.Module): self.num_heads = num_heads self.fcs = nn.ModuleList([nn.Linear(input_dim, output_dim) for _ in range(num_heads)]) - self.a = nn.ModuleList([nn.Linear(2*output_dim, 1) for _ in range(num_heads)]) + self.a = nn.ModuleList([nn.Linear(2 * output_dim, 1) for _ in range(num_heads)]) self.dropout = nn.Dropout(dropout) self.softmax = nn.Softmax(dim=0) @@ -465,7 +466,6 @@ class GraphAttention(nn.Module): sum_degs = np.hstack(([0], np.cumsum([len(row) for row in rows]))) mapped_nodes = [mapping[v] for v in nodes] indices = torch.LongTensor([[v, c] for (v, row) in zip(mapped_nodes, rows) for c in row]).t() - out = [] for k in range(self.num_heads): @@ -477,7 +477,7 @@ class GraphAttention(nn.Module): e = self.leakyrelu(self.a[k](cat_h)) - alpha = [self.softmax(e[lo : hi]) for (lo, hi) in zip(sum_degs, sum_degs[1:])] + alpha = [self.softmax(e[lo:hi]) for (lo, hi) in zip(sum_degs, sum_degs[1:])] alpha = torch.cat(tuple(alpha), dim=0) alpha = alpha.squeeze(1) alpha = self.dropout(alpha) @@ -487,11 +487,18 @@ class GraphAttention(nn.Module): return out - def cal_attention(x, y): - - att_x = torch.mean(x, dim = 1).reshape(-1, 1) - att_y = torch.mean(y, dim = 1).reshape(-1, 1) + def cal_attention(x, y): + + att_x = torch.mean(x, dim=1).reshape(-1, 1) + att_y = torch.mean(y, dim=1).reshape(-1, 1) att = att_x.mm(torch.t(att_y)) x_att = x.reshape(x.shape[0], 1, x.shape[1]).repeat(1, y.shape[0], 1) y_att = y.reshape(1, y.shape[0], y.shape[1]).repeat(x.shape[0], 1, 1) - return torch.mean(x.reshape(x.shape[0], 1, x.shape[1]).repeat(1, y.shape[0], 1)*y.reshape(1, y.shape[0], y.shape[1]).repeat(x.shape[0], 1, 1), dim = 2)-att \ No newline at end of file + return ( + torch.mean( + x.reshape(x.shape[0], 1, x.shape[1]).repeat(1, y.shape[0], 1) + * y.reshape(1, y.shape[0], y.shape[1]).repeat(x.shape[0], 1, 1), + dim=2, + ) + - att + ) diff --git a/qlib/data/dataset/__init__.py b/qlib/data/dataset/__init__.py index 3dbc17c23..e972aba3c 100644 --- a/qlib/data/dataset/__init__.py +++ b/qlib/data/dataset/__init__.py @@ -18,7 +18,7 @@ class Dataset(Serializable): - setup data - The data related attributes' names should start with '_' so that it will not be saved on disk when serializing - + - initialize the state of the dataset(info to prepare the data) - The name of essential state for preparing data should not start with '_' so that it could be serialized on disk when serializing. @@ -99,7 +99,7 @@ class DatasetH(Dataset): Here are some examples: .. code-block:: - + 1) 'segments': { 'train': ("2008-01-01", "2014-12-31"), 'valid': ("2017-01-01", "2020-08-01",), From 5c25f97e64a10e355692925a1dd000d1892eef13 Mon Sep 17 00:00:00 2001 From: Alex Wang Date: Wed, 25 Nov 2020 19:40:57 +0800 Subject: [PATCH 117/241] update hyper-parameter --- examples/benchmarks/CatBoost/workflow_config_catboost.yaml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/examples/benchmarks/CatBoost/workflow_config_catboost.yaml b/examples/benchmarks/CatBoost/workflow_config_catboost.yaml index 8bf3bb72b..574b52ddd 100644 --- a/examples/benchmarks/CatBoost/workflow_config_catboost.yaml +++ b/examples/benchmarks/CatBoost/workflow_config_catboost.yaml @@ -30,8 +30,9 @@ task: module_path: qlib.contrib.model.catboost_model kwargs: loss: RMSE - iterations: 5 - learning_rate: 0.03 + learning_rate: 0.0421 + subsample: 0.8789 + thread_count: 20 dataset: class: DatasetH module_path: qlib.data.dataset @@ -56,4 +57,4 @@ task: - class: PortAnaRecord module_path: qlib.workflow.record_temp kwargs: - config: *port_analysis_config \ No newline at end of file + config: *port_analysis_config From 64b7748033326b0aaa3c4c907ccd9b2d353d553d Mon Sep 17 00:00:00 2001 From: Alex Wang Date: Wed, 25 Nov 2020 19:46:48 +0800 Subject: [PATCH 118/241] update --- examples/workflow_by_code_sfm.py | 32 +++++--- qlib/contrib/model/pytorch_sfm.py | 124 +++++++++++++++++------------- 2 files changed, 91 insertions(+), 65 deletions(-) diff --git a/examples/workflow_by_code_sfm.py b/examples/workflow_by_code_sfm.py index 1942bfb33..ffd71b7da 100644 --- a/examples/workflow_by_code_sfm.py +++ b/examples/workflow_by_code_sfm.py @@ -1,5 +1,15 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. +# Copyright (c) Microsoft Corporation. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. import sys from pathlib import Path @@ -61,22 +71,22 @@ if __name__ == "__main__": "module_path": "qlib.contrib.model.pytorch_sfm", "kwargs": { "d_feat": 6, - "hidden_size": 64, - "output_dim": 1, - "freq_dim": 15, + "hidden_size": 32, + "output_dim" : 16, + "freq_dim" : 25, "dropout_W": 0.5, "dropout_U": 0.5, - "n_epochs": 10, + "n_epochs": 200, "lr": 1e-3, - "batch_size": 800, + "batch_size": 200, "early_stop": 20, "eval_steps": 5, "loss": "mse", - "lr_decay": 0.96, - "lr_decay_steps": 100, - "optimizer": "gd", + "lr_decay" : 0.96, + "lr_decay_steps" : 100, + "optimizer" : "adam", "GPU": 1, - "seed": 0, + "seed": 710, }, }, "dataset": { diff --git a/qlib/contrib/model/pytorch_sfm.py b/qlib/contrib/model/pytorch_sfm.py index 8564c491c..5dfc0f135 100644 --- a/qlib/contrib/model/pytorch_sfm.py +++ b/qlib/contrib/model/pytorch_sfm.py @@ -21,12 +21,11 @@ from ...model.base import Model from ...data.dataset import DatasetH from ...data.dataset.handler import DataHandlerLP - class SFM_Model(nn.Module): - def __init__(self, d_feat=6, output_dim=1, freq_dim=10, hidden_size=64, dropout_W=0.0, dropout_U=0.0, device="cpu"): + def __init__(self, d_feat=6, output_dim = 1, freq_dim = 10, hidden_size = 64, dropout_W = 0.0, dropout_U = 0.0, device = "cpu"): super().__init__() - self.input_dim = d_feat + self.input_dim = d_feat self.output_dim = output_dim self.freq_dim = freq_dim self.hidden_dim = hidden_size @@ -57,22 +56,22 @@ class SFM_Model(nn.Module): self.W_p = nn.Parameter(init.xavier_uniform_(torch.empty(self.hidden_dim, self.output_dim))) self.b_p = nn.Parameter(torch.zeros(self.output_dim)) - + self.activation = nn.Tanh() self.inner_activation = nn.Hardsigmoid() self.dropout_W, self.dropout_U = (dropout_W, dropout_U) self.fc_out = nn.Linear(self.output_dim, 1) self.states = [] - + def forward(self, input): - input = input.reshape(len(input), self.input_dim, -1) # [N, F, T] - input = input.permute(0, 2, 1) # [N, T, F] + input = input.reshape(len(input), self.input_dim, -1) # [N, F, T] + input = input.permute(0, 2, 1) # [N, T, F] time_step = input.shape[1] - + for ts in range(time_step): - x = input[:, ts, :] - if len(self.states) == 0: # hasn't initialized yet + x = input[:, ts,:] + if(len(self.states)==0): #hasn't initialized yet self.init_states(x) self.get_constants(x) p_tm1 = self.states[0] @@ -89,79 +88,77 @@ class SFM_Model(nn.Module): x_fre = torch.matmul(x * B_W[0], self.W_fre) + self.b_fre x_c = torch.matmul(x * B_W[0], self.W_c) + self.b_c x_o = torch.matmul(x * B_W[0], self.W_o) + self.b_o - - i = self.inner_activation( - x_i + torch.matmul(h_tm1 * B_U[0], self.U_i) - ) # not sure whether I am doing in the right unsquuze + + i = self.inner_activation(x_i + torch.matmul(h_tm1 * B_U[0], self.U_i)) # not sure whether I am doing in the right unsquuze + ste = self.inner_activation(x_ste + torch.matmul(h_tm1 * B_U[0], self.U_ste)) fre = self.inner_activation(x_fre + torch.matmul(h_tm1 * B_U[0], self.U_fre)) ste = torch.reshape(ste, (-1, self.hidden_dim, 1)) fre = torch.reshape(fre, (-1, 1, self.freq_dim)) - + f = ste * fre - + c = i * self.activation(x_c + torch.matmul(h_tm1 * B_U[0], self.U_c)) time = time_tm1 + 1 omega = torch.tensor(2 * np.pi) * time * frequency - re = torch.cos(omega) + re = torch.cos(omega) im = torch.sin(omega) - + c = torch.reshape(c, (-1, self.hidden_dim, 1)) S_re = f * S_re_tm1 + c * re S_im = f * S_im_tm1 + c * im - + A = torch.square(S_re) + torch.square(S_im) A = torch.reshape(A, (-1, self.freq_dim)).float() A_a = torch.matmul(A * B_U[0], self.U_a) A_a = torch.reshape(A_a, (-1, self.hidden_dim)) a = self.activation(A_a + self.b_a) - + o = self.inner_activation(x_o + torch.matmul(h_tm1 * B_U[0], self.U_o)) h = o * a p = torch.matmul(h, self.W_p) + self.b_p self.states = [p, h, S_re, S_im, time, None, None, None] - self.states = [] + self.states = [] return self.fc_out(p).squeeze() def init_states(self, x): reducer_f = torch.zeros((self.hidden_dim, self.freq_dim)).to(self.device) reducer_p = torch.zeros((self.hidden_dim, self.output_dim)).to(self.device) - + init_state_h = torch.zeros(self.hidden_dim).to(self.device) init_state_p = torch.matmul(init_state_h, reducer_p) - + init_state = torch.zeros_like(init_state_h).to(self.device) init_freq = torch.matmul(init_state_h, reducer_f) init_state = torch.reshape(init_state, (-1, self.hidden_dim, 1)) init_freq = torch.reshape(init_freq, (-1, 1, self.freq_dim)) - + init_state_S_re = init_state * init_freq init_state_S_im = init_state * init_freq - + init_state_time = torch.tensor(0).to(self.device) self.states = [init_state_p, init_state_h, init_state_S_re, init_state_S_im, init_state_time, None, None, None] def get_constants(self, x): constants = [] - constants.append([torch.tensor(1.0).to(self.device) for _ in range(6)]) - constants.append([torch.tensor(1.0).to(self.device) for _ in range(7)]) - array = np.array([float(ii) / self.freq_dim for ii in range(self.freq_dim)]) + constants.append([torch.tensor(1.).to(self.device) for _ in range(6)]) + constants.append([torch.tensor(1.).to(self.device) for _ in range(7)]) + array = np.array([float(ii)/self.freq_dim for ii in range(self.freq_dim)]) constants.append(torch.tensor(array).to(self.device)) self.states[5:] = constants - class SFM(Model): """SFM Model @@ -188,7 +185,7 @@ class SFM(Model): d_feat=6, hidden_size=64, output_dim=1, - freq_dim=10, + freq_dim = 10, dropout_W=0.0, dropout_U=0.0, n_epochs=200, @@ -224,7 +221,7 @@ class SFM(Model): self.lr_decay_steps = lr_decay_steps self.optimizer = optimizer.lower() self.loss_type = loss - self.device = "cuda:%d" % (GPU) if torch.cuda.is_available() else "cpu" + self.device = 'cuda:%d'%(GPU) if torch.cuda.is_available() else 'cpu' self.use_gpu = torch.cuda.is_available() self.seed = seed @@ -232,7 +229,8 @@ class SFM(Model): "SFM parameters setting:" "\nd_feat : {}" "\nhidden_size : {}" - "\nfrequency_dimension : {}" + "\noutput_size : {}" + "\nfrequency_dimension : {}" "\ndropout_W: {}" "\ndropout_U: {}" "\nn_epochs : {}" @@ -249,6 +247,7 @@ class SFM(Model): "\nseed : {}".format( d_feat, hidden_size, + output_dim, freq_dim, dropout_W, dropout_U, @@ -272,14 +271,14 @@ class SFM(Model): self._scorer = mean_squared_error if loss == "mse" else roc_auc_score self.sfm_model = SFM_Model( - d_feat=self.d_feat, - output_dim=self.output_dim, - hidden_size=self.hidden_size, - freq_dim=self.freq_dim, - dropout_W=self.dropout_W, - dropout_U=self.dropout_U, - device=self.device, - ) + d_feat=self.d_feat, + output_dim = self.output_dim, + hidden_size = self.hidden_size, + freq_dim = self.freq_dim, + dropout_W=self.dropout_W, + dropout_U = self.dropout_U, + device = self.device + ) if optimizer.lower() == "adam": self.train_optimizer = optim.Adam(self.sfm_model.parameters(), lr=self.lr) elif optimizer.lower() == "gd": @@ -304,7 +303,14 @@ class SFM(Model): self._fitted = False self.sfm_model.to(self.device) - def fit(self, dataset: DatasetH, evals_result=dict(), verbose=True, save_path=None, **kwargs): + def fit( + self, + dataset: DatasetH, + evals_result=dict(), + verbose=True, + save_path=None, + **kwargs + ): df_train, df_valid = dataset.prepare( ["train", "valid"], col_set=["feature", "label"], data_key=DataHandlerLP.DK_L @@ -360,7 +366,6 @@ class SFM(Model): # validation train_loss += loss.val - # print(loss.val) if step and step % self.eval_steps == 0: stop_steps += 1 train_loss /= self.eval_steps @@ -394,12 +399,12 @@ class SFM(Model): # update learning rate self.scheduler.step(cur_loss_val) - if self.device != "cpu": + if self.device != 'cpu': torch.cuda.empty_cache() def get_loss(self, pred, target, loss_type): if loss_type == "mse": - sqr_loss = (pred - target) ** 2 + sqr_loss = (pred - target)**2 loss = sqr_loss.mean() return loss elif loss_type == "binary": @@ -414,17 +419,30 @@ class SFM(Model): x_test = dataset.prepare("test", col_set="feature") index = x_test.index - x_test = torch.from_numpy(x_test.values).float() - - x_test = x_test.to(self.device) self.sfm_model.eval() + x_values = x_test.values + sample_num = x_values.shape[0] + preds = [] - with torch.no_grad(): - if self.device != "cpu": - preds = self.sfm_model(x_test).detach().cpu().numpy() + for begin in range(sample_num)[::self.batch_size]: + if sample_num-begin Date: Wed, 25 Nov 2020 19:53:22 +0800 Subject: [PATCH 119/241] Fix config and format --- .../benchmarks/HATS/worflow_config_hats.yaml | 2 +- examples/workflow_by_code_sfm.py | 10 +- qlib/contrib/model/pytorch_sfm.py | 104 +++++++++--------- 3 files changed, 57 insertions(+), 59 deletions(-) diff --git a/examples/benchmarks/HATS/worflow_config_hats.yaml b/examples/benchmarks/HATS/worflow_config_hats.yaml index a7ab0d2d7..d8fb55198 100644 --- a/examples/benchmarks/HATS/worflow_config_hats.yaml +++ b/examples/benchmarks/HATS/worflow_config_hats.yaml @@ -27,7 +27,7 @@ port_analysis_config: &port_analysis_config task: model: class: HATS - module_path: qlib.contrib.model.pytorch_gats + module_path: qlib.contrib.model.pytorch_hats kwargs: d_feat: 6 hidden_size: 64 diff --git a/examples/workflow_by_code_sfm.py b/examples/workflow_by_code_sfm.py index ffd71b7da..ccc2d412c 100644 --- a/examples/workflow_by_code_sfm.py +++ b/examples/workflow_by_code_sfm.py @@ -72,8 +72,8 @@ if __name__ == "__main__": "kwargs": { "d_feat": 6, "hidden_size": 32, - "output_dim" : 16, - "freq_dim" : 25, + "output_dim": 16, + "freq_dim": 25, "dropout_W": 0.5, "dropout_U": 0.5, "n_epochs": 200, @@ -82,9 +82,9 @@ if __name__ == "__main__": "early_stop": 20, "eval_steps": 5, "loss": "mse", - "lr_decay" : 0.96, - "lr_decay_steps" : 100, - "optimizer" : "adam", + "lr_decay": 0.96, + "lr_decay_steps": 100, + "optimizer": "adam", "GPU": 1, "seed": 710, }, diff --git a/qlib/contrib/model/pytorch_sfm.py b/qlib/contrib/model/pytorch_sfm.py index 5dfc0f135..631841fbe 100644 --- a/qlib/contrib/model/pytorch_sfm.py +++ b/qlib/contrib/model/pytorch_sfm.py @@ -21,11 +21,12 @@ from ...model.base import Model from ...data.dataset import DatasetH from ...data.dataset.handler import DataHandlerLP + class SFM_Model(nn.Module): - def __init__(self, d_feat=6, output_dim = 1, freq_dim = 10, hidden_size = 64, dropout_W = 0.0, dropout_U = 0.0, device = "cpu"): + def __init__(self, d_feat=6, output_dim=1, freq_dim=10, hidden_size=64, dropout_W=0.0, dropout_U=0.0, device="cpu"): super().__init__() - self.input_dim = d_feat + self.input_dim = d_feat self.output_dim = output_dim self.freq_dim = freq_dim self.hidden_dim = hidden_size @@ -56,22 +57,22 @@ class SFM_Model(nn.Module): self.W_p = nn.Parameter(init.xavier_uniform_(torch.empty(self.hidden_dim, self.output_dim))) self.b_p = nn.Parameter(torch.zeros(self.output_dim)) - + self.activation = nn.Tanh() self.inner_activation = nn.Hardsigmoid() self.dropout_W, self.dropout_U = (dropout_W, dropout_U) self.fc_out = nn.Linear(self.output_dim, 1) self.states = [] - + def forward(self, input): - input = input.reshape(len(input), self.input_dim, -1) # [N, F, T] - input = input.permute(0, 2, 1) # [N, T, F] + input = input.reshape(len(input), self.input_dim, -1) # [N, F, T] + input = input.permute(0, 2, 1) # [N, T, F] time_step = input.shape[1] - + for ts in range(time_step): - x = input[:, ts,:] - if(len(self.states)==0): #hasn't initialized yet + x = input[:, ts, :] + if len(self.states) == 0: # hasn't initialized yet self.init_states(x) self.get_constants(x) p_tm1 = self.states[0] @@ -88,77 +89,79 @@ class SFM_Model(nn.Module): x_fre = torch.matmul(x * B_W[0], self.W_fre) + self.b_fre x_c = torch.matmul(x * B_W[0], self.W_c) + self.b_c x_o = torch.matmul(x * B_W[0], self.W_o) + self.b_o - - i = self.inner_activation(x_i + torch.matmul(h_tm1 * B_U[0], self.U_i)) # not sure whether I am doing in the right unsquuze - + + i = self.inner_activation( + x_i + torch.matmul(h_tm1 * B_U[0], self.U_i) + ) # not sure whether I am doing in the right unsquuze ste = self.inner_activation(x_ste + torch.matmul(h_tm1 * B_U[0], self.U_ste)) fre = self.inner_activation(x_fre + torch.matmul(h_tm1 * B_U[0], self.U_fre)) ste = torch.reshape(ste, (-1, self.hidden_dim, 1)) fre = torch.reshape(fre, (-1, 1, self.freq_dim)) - + f = ste * fre - + c = i * self.activation(x_c + torch.matmul(h_tm1 * B_U[0], self.U_c)) time = time_tm1 + 1 omega = torch.tensor(2 * np.pi) * time * frequency - re = torch.cos(omega) + re = torch.cos(omega) im = torch.sin(omega) - + c = torch.reshape(c, (-1, self.hidden_dim, 1)) S_re = f * S_re_tm1 + c * re S_im = f * S_im_tm1 + c * im - + A = torch.square(S_re) + torch.square(S_im) A = torch.reshape(A, (-1, self.freq_dim)).float() A_a = torch.matmul(A * B_U[0], self.U_a) A_a = torch.reshape(A_a, (-1, self.hidden_dim)) a = self.activation(A_a + self.b_a) - + o = self.inner_activation(x_o + torch.matmul(h_tm1 * B_U[0], self.U_o)) h = o * a p = torch.matmul(h, self.W_p) + self.b_p self.states = [p, h, S_re, S_im, time, None, None, None] - self.states = [] + self.states = [] return self.fc_out(p).squeeze() def init_states(self, x): reducer_f = torch.zeros((self.hidden_dim, self.freq_dim)).to(self.device) reducer_p = torch.zeros((self.hidden_dim, self.output_dim)).to(self.device) - + init_state_h = torch.zeros(self.hidden_dim).to(self.device) init_state_p = torch.matmul(init_state_h, reducer_p) - + init_state = torch.zeros_like(init_state_h).to(self.device) init_freq = torch.matmul(init_state_h, reducer_f) init_state = torch.reshape(init_state, (-1, self.hidden_dim, 1)) init_freq = torch.reshape(init_freq, (-1, 1, self.freq_dim)) - + init_state_S_re = init_state * init_freq init_state_S_im = init_state * init_freq - + init_state_time = torch.tensor(0).to(self.device) self.states = [init_state_p, init_state_h, init_state_S_re, init_state_S_im, init_state_time, None, None, None] def get_constants(self, x): constants = [] - constants.append([torch.tensor(1.).to(self.device) for _ in range(6)]) - constants.append([torch.tensor(1.).to(self.device) for _ in range(7)]) - array = np.array([float(ii)/self.freq_dim for ii in range(self.freq_dim)]) + constants.append([torch.tensor(1.0).to(self.device) for _ in range(6)]) + constants.append([torch.tensor(1.0).to(self.device) for _ in range(7)]) + array = np.array([float(ii) / self.freq_dim for ii in range(self.freq_dim)]) constants.append(torch.tensor(array).to(self.device)) self.states[5:] = constants + class SFM(Model): """SFM Model @@ -185,7 +188,7 @@ class SFM(Model): d_feat=6, hidden_size=64, output_dim=1, - freq_dim = 10, + freq_dim=10, dropout_W=0.0, dropout_U=0.0, n_epochs=200, @@ -221,7 +224,7 @@ class SFM(Model): self.lr_decay_steps = lr_decay_steps self.optimizer = optimizer.lower() self.loss_type = loss - self.device = 'cuda:%d'%(GPU) if torch.cuda.is_available() else 'cpu' + self.device = "cuda:%d" % (GPU) if torch.cuda.is_available() else "cpu" self.use_gpu = torch.cuda.is_available() self.seed = seed @@ -230,7 +233,7 @@ class SFM(Model): "\nd_feat : {}" "\nhidden_size : {}" "\noutput_size : {}" - "\nfrequency_dimension : {}" + "\nfrequency_dimension : {}" "\ndropout_W: {}" "\ndropout_U: {}" "\nn_epochs : {}" @@ -271,14 +274,14 @@ class SFM(Model): self._scorer = mean_squared_error if loss == "mse" else roc_auc_score self.sfm_model = SFM_Model( - d_feat=self.d_feat, - output_dim = self.output_dim, - hidden_size = self.hidden_size, - freq_dim = self.freq_dim, - dropout_W=self.dropout_W, - dropout_U = self.dropout_U, - device = self.device - ) + d_feat=self.d_feat, + output_dim=self.output_dim, + hidden_size=self.hidden_size, + freq_dim=self.freq_dim, + dropout_W=self.dropout_W, + dropout_U=self.dropout_U, + device=self.device, + ) if optimizer.lower() == "adam": self.train_optimizer = optim.Adam(self.sfm_model.parameters(), lr=self.lr) elif optimizer.lower() == "gd": @@ -303,14 +306,7 @@ class SFM(Model): self._fitted = False self.sfm_model.to(self.device) - def fit( - self, - dataset: DatasetH, - evals_result=dict(), - verbose=True, - save_path=None, - **kwargs - ): + def fit(self, dataset: DatasetH, evals_result=dict(), verbose=True, save_path=None, **kwargs): df_train, df_valid = dataset.prepare( ["train", "valid"], col_set=["feature", "label"], data_key=DataHandlerLP.DK_L @@ -399,12 +395,12 @@ class SFM(Model): # update learning rate self.scheduler.step(cur_loss_val) - if self.device != 'cpu': + if self.device != "cpu": torch.cuda.empty_cache() def get_loss(self, pred, target, loss_type): if loss_type == "mse": - sqr_loss = (pred - target)**2 + sqr_loss = (pred - target) ** 2 loss = sqr_loss.mean() return loss elif loss_type == "binary": @@ -424,24 +420,24 @@ class SFM(Model): sample_num = x_values.shape[0] preds = [] - for begin in range(sample_num)[::self.batch_size]: - if sample_num-begin Date: Wed, 25 Nov 2020 20:11:26 +0800 Subject: [PATCH 120/241] Fix dumpmp_bin --- scripts/dump_bin.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/scripts/dump_bin.py b/scripts/dump_bin.py index 9f6dd88e2..bdc227029 100644 --- a/scripts/dump_bin.py +++ b/scripts/dump_bin.py @@ -333,7 +333,9 @@ class DumpDataFix(DumpDataAll): _dt_map[self.INSTRUMENTS_START_FIELD] = self._format_datetime(_begin_time) _dt_map[self.INSTRUMENTS_END_FIELD] = self._format_datetime(_end_time) p_bar.update() - self.save_instruments(pd.DataFrame.from_dict(self._old_instruments, orient="index")) + _inst_df = pd.DataFrame.from_dict(self._old_instruments, orient="index") + _inst_df.index.names = [self.symbol_field_name] + self.save_instruments(_inst_df.reset_index()) logger.info("end of instruments dump.\n") def dump(self): From 47cf46f0c21e2f6f86f29cc9dfa3398391cdb008 Mon Sep 17 00:00:00 2001 From: Haoyu Wang <48108414+javaThonc@users.noreply.github.com> Date: Wed, 25 Nov 2020 20:28:00 +0800 Subject: [PATCH 121/241] Add copy right --- qlib/contrib/model/pytorch_sfm.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/qlib/contrib/model/pytorch_sfm.py b/qlib/contrib/model/pytorch_sfm.py index 631841fbe..d8baa9cb2 100644 --- a/qlib/contrib/model/pytorch_sfm.py +++ b/qlib/contrib/model/pytorch_sfm.py @@ -1,5 +1,15 @@ # Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. from __future__ import division from __future__ import print_function From c897ecac334ff112e174541b4d9f8f15e2282f43 Mon Sep 17 00:00:00 2001 From: Young Date: Wed, 25 Nov 2020 12:32:39 +0000 Subject: [PATCH 122/241] fix CI bug --- qlib/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qlib/config.py b/qlib/config.py index 640701ee5..869ea99c9 100644 --- a/qlib/config.py +++ b/qlib/config.py @@ -64,7 +64,7 @@ class Config: REG_CN = "cn" REG_US = "us" -NUM_USABLE_CPU = multiprocessing.cpu_count() - 2 +NUM_USABLE_CPU = max(multiprocessing.cpu_count() - 2, 1) _default_config = { # data provider config From 93323ed6b37e3ea46940006cb8ad5e928cd400f7 Mon Sep 17 00:00:00 2001 From: Wendi Li Date: Mon, 23 Nov 2020 15:54:27 +0800 Subject: [PATCH 123/241] Add TFT benchmark --- .../TFT/data_formatters/__init__.py | 15 + .../benchmarks/TFT/data_formatters/base.py | 235 +++ .../TFT/data_formatters/electricity.py | 261 ++++ .../TFT/data_formatters/favorita.py | 327 ++++ .../TFT/data_formatters/qlib_Alpha158.py | 220 +++ .../benchmarks/TFT/data_formatters/traffic.py | 117 ++ .../TFT/data_formatters/volatility.py | 214 +++ .../benchmarks/TFT/expt_settings/__init__.py | 15 + .../benchmarks/TFT/expt_settings/configs.py | 111 ++ examples/benchmarks/TFT/libs/__init__.py | 15 + .../benchmarks/TFT/libs/hyperparam_opt.py | 438 ++++++ examples/benchmarks/TFT/libs/tft_model.py | 1391 +++++++++++++++++ examples/benchmarks/TFT/libs/utils.py | 236 +++ examples/benchmarks/TFT/tft.py | 246 +++ .../benchmarks/TFT/workflow_by_code_tft.py | 130 ++ 15 files changed, 3971 insertions(+) create mode 100644 examples/benchmarks/TFT/data_formatters/__init__.py create mode 100644 examples/benchmarks/TFT/data_formatters/base.py create mode 100644 examples/benchmarks/TFT/data_formatters/electricity.py create mode 100644 examples/benchmarks/TFT/data_formatters/favorita.py create mode 100644 examples/benchmarks/TFT/data_formatters/qlib_Alpha158.py create mode 100644 examples/benchmarks/TFT/data_formatters/traffic.py create mode 100644 examples/benchmarks/TFT/data_formatters/volatility.py create mode 100644 examples/benchmarks/TFT/expt_settings/__init__.py create mode 100644 examples/benchmarks/TFT/expt_settings/configs.py create mode 100644 examples/benchmarks/TFT/libs/__init__.py create mode 100644 examples/benchmarks/TFT/libs/hyperparam_opt.py create mode 100644 examples/benchmarks/TFT/libs/tft_model.py create mode 100644 examples/benchmarks/TFT/libs/utils.py create mode 100644 examples/benchmarks/TFT/tft.py create mode 100644 examples/benchmarks/TFT/workflow_by_code_tft.py diff --git a/examples/benchmarks/TFT/data_formatters/__init__.py b/examples/benchmarks/TFT/data_formatters/__init__.py new file mode 100644 index 000000000..9a1980462 --- /dev/null +++ b/examples/benchmarks/TFT/data_formatters/__init__.py @@ -0,0 +1,15 @@ +# coding=utf-8 +# Copyright 2020 The Google Research Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + diff --git a/examples/benchmarks/TFT/data_formatters/base.py b/examples/benchmarks/TFT/data_formatters/base.py new file mode 100644 index 000000000..f4ce2764f --- /dev/null +++ b/examples/benchmarks/TFT/data_formatters/base.py @@ -0,0 +1,235 @@ +# coding=utf-8 +# Copyright 2020 The Google Research Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Lint as: python3 +"""Default data formatting functions for experiments. + +For new datasets, inherit form GenericDataFormatter and implement +all abstract functions. + +These dataset-specific methods: +1) Define the column and input types for tabular dataframes used by model +2) Perform the necessary input feature engineering & normalisation steps +3) Reverts the normalisation for predictions +4) Are responsible for train, validation and test splits + + +""" + +import abc +import enum + + +# Type defintions +class DataTypes(enum.IntEnum): + """Defines numerical types of each column.""" + REAL_VALUED = 0 + CATEGORICAL = 1 + DATE = 2 + + +class InputTypes(enum.IntEnum): + """Defines input types of each column.""" + TARGET = 0 + OBSERVED_INPUT = 1 + KNOWN_INPUT = 2 + STATIC_INPUT = 3 + ID = 4 # Single column used as an entity identifier + TIME = 5 # Single column exclusively used as a time index + + +class GenericDataFormatter(abc.ABC): + """Abstract base class for all data formatters. + + User can implement the abstract methods below to perform dataset-specific + manipulations. + + """ + + @abc.abstractmethod + def set_scalers(self, df): + """Calibrates scalers using the data supplied.""" + raise NotImplementedError() + + @abc.abstractmethod + def transform_inputs(self, df): + """Performs feature transformation.""" + raise NotImplementedError() + + @abc.abstractmethod + def format_predictions(self, df): + """Reverts any normalisation to give predictions in original scale.""" + raise NotImplementedError() + + @abc.abstractmethod + def split_data(self, df): + """Performs the default train, validation and test splits.""" + raise NotImplementedError() + + @property + @abc.abstractmethod + def _column_definition(self): + """Defines order, input type and data type of each column.""" + raise NotImplementedError() + + @abc.abstractmethod + def get_fixed_params(self): + """Defines the fixed parameters used by the model for training. + + Requires the following keys: + 'total_time_steps': Defines the total number of time steps used by TFT + 'num_encoder_steps': Determines length of LSTM encoder (i.e. history) + 'num_epochs': Maximum number of epochs for training + 'early_stopping_patience': Early stopping param for keras + 'multiprocessing_workers': # of cpus for data processing + + + Returns: + A dictionary of fixed parameters, e.g.: + + fixed_params = { + 'total_time_steps': 252 + 5, + 'num_encoder_steps': 252, + 'num_epochs': 100, + 'early_stopping_patience': 5, + 'multiprocessing_workers': 5, + } + """ + raise NotImplementedError + + # Shared functions across data-formatters + @property + def num_classes_per_cat_input(self): + """Returns number of categories per relevant input. + + This is seqeuently required for keras embedding layers. + """ + return self._num_classes_per_cat_input + + def get_num_samples_for_calibration(self): + """Gets the default number of training and validation samples. + + Use to sub-sample the data for network calibration and a value of -1 uses + all available samples. + + Returns: + Tuple of (training samples, validation samples) + """ + return -1, -1 + + def get_column_definition(self): + """"Returns formatted column definition in order expected by the TFT.""" + + column_definition = self._column_definition + + # Sanity checks first. + # Ensure only one ID and time column exist + def _check_single_column(input_type): + + length = len([tup for tup in column_definition if tup[2] == input_type]) + + if length != 1: + raise ValueError('Illegal number of inputs ({}) of type {}'.format( + length, input_type)) + + _check_single_column(InputTypes.ID) + _check_single_column(InputTypes.TIME) + + identifier = [tup for tup in column_definition if tup[2] == InputTypes.ID] + time = [tup for tup in column_definition if tup[2] == InputTypes.TIME] + real_inputs = [ + tup for tup in column_definition if tup[1] == DataTypes.REAL_VALUED and + tup[2] not in {InputTypes.ID, InputTypes.TIME} + ] + categorical_inputs = [ + tup for tup in column_definition if tup[1] == DataTypes.CATEGORICAL and + tup[2] not in {InputTypes.ID, InputTypes.TIME} + ] + + return identifier + time + real_inputs + categorical_inputs + + def _get_input_columns(self): + """Returns names of all input columns.""" + return [ + tup[0] + for tup in self.get_column_definition() + if tup[2] not in {InputTypes.ID, InputTypes.TIME} + ] + + def _get_tft_input_indices(self): + """Returns the relevant indexes and input sizes required by TFT.""" + + # Functions + def _extract_tuples_from_data_type(data_type, defn): + return [ + tup for tup in defn if tup[1] == data_type and + tup[2] not in {InputTypes.ID, InputTypes.TIME} + ] + + def _get_locations(input_types, defn): + return [i for i, tup in enumerate(defn) if tup[2] in input_types] + + # Start extraction + column_definition = [ + tup for tup in self.get_column_definition() + if tup[2] not in {InputTypes.ID, InputTypes.TIME} + ] + + categorical_inputs = _extract_tuples_from_data_type(DataTypes.CATEGORICAL, + column_definition) + real_inputs = _extract_tuples_from_data_type(DataTypes.REAL_VALUED, + column_definition) + + locations = { + 'input_size': + len(self._get_input_columns()), + 'output_size': + len(_get_locations({InputTypes.TARGET}, column_definition)), + 'category_counts': + self.num_classes_per_cat_input, + 'input_obs_loc': + _get_locations({InputTypes.TARGET}, column_definition), + 'static_input_loc': + _get_locations({InputTypes.STATIC_INPUT}, column_definition), + 'known_regular_inputs': + _get_locations({InputTypes.STATIC_INPUT, InputTypes.KNOWN_INPUT}, + real_inputs), + 'known_categorical_inputs': + _get_locations({InputTypes.STATIC_INPUT, InputTypes.KNOWN_INPUT}, + categorical_inputs), + } + + return locations + + def get_experiment_params(self): + """Returns fixed model parameters for experiments.""" + + required_keys = [ + 'total_time_steps', 'num_encoder_steps', 'num_epochs', + 'early_stopping_patience', 'multiprocessing_workers' + ] + + fixed_params = self.get_fixed_params() + + for k in required_keys: + if k not in fixed_params: + raise ValueError('Field {}'.format(k) + + ' missing from fixed parameter definitions!') + + fixed_params['column_definition'] = self.get_column_definition() + + fixed_params.update(self._get_tft_input_indices()) + + return fixed_params diff --git a/examples/benchmarks/TFT/data_formatters/electricity.py b/examples/benchmarks/TFT/data_formatters/electricity.py new file mode 100644 index 000000000..062a77eb2 --- /dev/null +++ b/examples/benchmarks/TFT/data_formatters/electricity.py @@ -0,0 +1,261 @@ +# coding=utf-8 +# Copyright 2020 The Google Research Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Lint as: python3 +"""Custom formatting functions for Electricity dataset. + +Defines dataset specific column definitions and data transformations. Uses +entity specific z-score normalization. +""" + +import data_formatters.base +import libs.utils as utils +import pandas as pd +import sklearn.preprocessing + +GenericDataFormatter = data_formatters.base.GenericDataFormatter +DataTypes = data_formatters.base.DataTypes +InputTypes = data_formatters.base.InputTypes + + +class ElectricityFormatter(GenericDataFormatter): + """Defines and formats data for the electricity dataset. + + Note that per-entity z-score normalization is used here, and is implemented + across functions. + + Attributes: + column_definition: Defines input and data type of column used in the + experiment. + identifiers: Entity identifiers used in experiments. + """ + + _column_definition = [ + ('id', DataTypes.REAL_VALUED, InputTypes.ID), + ('hours_from_start', DataTypes.REAL_VALUED, InputTypes.TIME), + ('power_usage', DataTypes.REAL_VALUED, InputTypes.TARGET), + ('hour', DataTypes.REAL_VALUED, InputTypes.KNOWN_INPUT), + ('day_of_week', DataTypes.REAL_VALUED, InputTypes.KNOWN_INPUT), + ('hours_from_start', DataTypes.REAL_VALUED, InputTypes.KNOWN_INPUT), + ('categorical_id', DataTypes.CATEGORICAL, InputTypes.STATIC_INPUT), + ] + + def __init__(self): + """Initialises formatter.""" + + self.identifiers = None + self._real_scalers = None + self._cat_scalers = None + self._target_scaler = None + self._num_classes_per_cat_input = None + self._time_steps = self.get_fixed_params()['total_time_steps'] + + def split_data(self, df, valid_boundary=1315, test_boundary=1339): + """Splits data frame into training-validation-test data frames. + + This also calibrates scaling object, and transforms data for each split. + + Args: + df: Source data frame to split. + valid_boundary: Starting year for validation data + test_boundary: Starting year for test data + + Returns: + Tuple of transformed (train, valid, test) data. + """ + + print('Formatting train-valid-test splits.') + + index = df['days_from_start'] + train = df.loc[index < valid_boundary] + valid = df.loc[(index >= valid_boundary - 7) & (index < test_boundary)] + test = df.loc[index >= test_boundary - 7] + + self.set_scalers(train) + + return (self.transform_inputs(data) for data in [train, valid, test]) + + def set_scalers(self, df): + """Calibrates scalers using the data supplied. + + Args: + df: Data to use to calibrate scalers. + """ + print('Setting scalers with training data...') + + column_definitions = self.get_column_definition() + id_column = utils.get_single_col_by_input_type(InputTypes.ID, + column_definitions) + target_column = utils.get_single_col_by_input_type(InputTypes.TARGET, + column_definitions) + + # Format real scalers + real_inputs = utils.extract_cols_from_data_type( + DataTypes.REAL_VALUED, column_definitions, + {InputTypes.ID, InputTypes.TIME}) + + # Initialise scaler caches + self._real_scalers = {} + self._target_scaler = {} + identifiers = [] + for identifier, sliced in df.groupby(id_column): + + if len(sliced) >= self._time_steps: + + data = sliced[real_inputs].values + targets = sliced[[target_column]].values + self._real_scalers[identifier] \ + = sklearn.preprocessing.StandardScaler().fit(data) + + self._target_scaler[identifier] \ + = sklearn.preprocessing.StandardScaler().fit(targets) + identifiers.append(identifier) + + # Format categorical scalers + categorical_inputs = utils.extract_cols_from_data_type( + DataTypes.CATEGORICAL, column_definitions, + {InputTypes.ID, InputTypes.TIME}) + + categorical_scalers = {} + num_classes = [] + for col in categorical_inputs: + # Set all to str so that we don't have mixed integer/string columns + srs = df[col].apply(str) + categorical_scalers[col] = sklearn.preprocessing.LabelEncoder().fit( + srs.values) + num_classes.append(srs.nunique()) + + # Set categorical scaler outputs + self._cat_scalers = categorical_scalers + self._num_classes_per_cat_input = num_classes + + # Extract identifiers in case required + self.identifiers = identifiers + + def transform_inputs(self, df): + """Performs feature transformations. + + This includes both feature engineering, preprocessing and normalisation. + + Args: + df: Data frame to transform. + + Returns: + Transformed data frame. + + """ + + if self._real_scalers is None and self._cat_scalers is None: + raise ValueError('Scalers have not been set!') + + # Extract relevant columns + column_definitions = self.get_column_definition() + id_col = utils.get_single_col_by_input_type(InputTypes.ID, + column_definitions) + real_inputs = utils.extract_cols_from_data_type( + DataTypes.REAL_VALUED, column_definitions, + {InputTypes.ID, InputTypes.TIME}) + categorical_inputs = utils.extract_cols_from_data_type( + DataTypes.CATEGORICAL, column_definitions, + {InputTypes.ID, InputTypes.TIME}) + + # Transform real inputs per entity + df_list = [] + for identifier, sliced in df.groupby(id_col): + + # Filter out any trajectories that are too short + if len(sliced) >= self._time_steps: + sliced_copy = sliced.copy() + sliced_copy[real_inputs] = self._real_scalers[identifier].transform( + sliced_copy[real_inputs].values) + df_list.append(sliced_copy) + + output = pd.concat(df_list, axis=0) + + # Format categorical inputs + for col in categorical_inputs: + string_df = df[col].apply(str) + output[col] = self._cat_scalers[col].transform(string_df) + + return output + + def format_predictions(self, predictions): + """Reverts any normalisation to give predictions in original scale. + + Args: + predictions: Dataframe of model predictions. + + Returns: + Data frame of unnormalised predictions. + """ + + if self._target_scaler is None: + raise ValueError('Scalers have not been set!') + + column_names = predictions.columns + + df_list = [] + for identifier, sliced in predictions.groupby('identifier'): + sliced_copy = sliced.copy() + target_scaler = self._target_scaler[identifier] + + for col in column_names: + if col not in {'forecast_time', 'identifier'}: + sliced_copy[col] = target_scaler.inverse_transform(sliced_copy[col]) + df_list.append(sliced_copy) + + output = pd.concat(df_list, axis=0) + + return output + + # Default params + def get_fixed_params(self): + """Returns fixed model parameters for experiments.""" + + fixed_params = { + 'total_time_steps': 8 * 24, + 'num_encoder_steps': 7 * 24, + 'num_epochs': 100, + 'early_stopping_patience': 5, + 'multiprocessing_workers': 5 + } + + return fixed_params + + def get_default_model_params(self): + """Returns default optimised model parameters.""" + + model_params = { + 'dropout_rate': 0.1, + 'hidden_layer_size': 160, + 'learning_rate': 0.001, + 'minibatch_size': 64, + 'max_gradient_norm': 0.01, + 'num_heads': 4, + 'stack_size': 1 + } + + return model_params + + def get_num_samples_for_calibration(self): + """Gets the default number of training and validation samples. + + Use to sub-sample the data for network calibration and a value of -1 uses + all available samples. + + Returns: + Tuple of (training samples, validation samples) + """ + return 450000, 50000 diff --git a/examples/benchmarks/TFT/data_formatters/favorita.py b/examples/benchmarks/TFT/data_formatters/favorita.py new file mode 100644 index 000000000..26fae632c --- /dev/null +++ b/examples/benchmarks/TFT/data_formatters/favorita.py @@ -0,0 +1,327 @@ +# coding=utf-8 +# Copyright 2020 The Google Research Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Lint as: python3 +"""Custom formatting functions for Favorita dataset. + +Defines dataset specific column definitions and data transformations. +""" + +import data_formatters.base +import libs.utils as utils +import pandas as pd +import sklearn.preprocessing + +DataTypes = data_formatters.base.DataTypes +InputTypes = data_formatters.base.InputTypes + + +class FavoritaFormatter(data_formatters.base.GenericDataFormatter): + """Defines and formats data for the Favorita dataset. + + Attributes: + column_definition: Defines input and data type of column used in the + experiment. + identifiers: Entity identifiers used in experiments. + """ + + _column_definition = [ + ('traj_id', DataTypes.REAL_VALUED, InputTypes.ID), + ('date', DataTypes.DATE, InputTypes.TIME), + ('log_sales', DataTypes.REAL_VALUED, InputTypes.TARGET), + ('onpromotion', DataTypes.CATEGORICAL, InputTypes.KNOWN_INPUT), + ('transactions', DataTypes.REAL_VALUED, InputTypes.OBSERVED_INPUT), + ('oil', DataTypes.REAL_VALUED, InputTypes.OBSERVED_INPUT), + ('day_of_week', DataTypes.CATEGORICAL, InputTypes.KNOWN_INPUT), + ('day_of_month', DataTypes.REAL_VALUED, InputTypes.KNOWN_INPUT), + ('month', DataTypes.REAL_VALUED, InputTypes.KNOWN_INPUT), + ('national_hol', DataTypes.CATEGORICAL, InputTypes.KNOWN_INPUT), + ('regional_hol', DataTypes.CATEGORICAL, InputTypes.KNOWN_INPUT), + ('local_hol', DataTypes.CATEGORICAL, InputTypes.KNOWN_INPUT), + ('open', DataTypes.REAL_VALUED, InputTypes.KNOWN_INPUT), + ('item_nbr', DataTypes.CATEGORICAL, InputTypes.STATIC_INPUT), + ('store_nbr', DataTypes.CATEGORICAL, InputTypes.STATIC_INPUT), + ('city', DataTypes.CATEGORICAL, InputTypes.STATIC_INPUT), + ('state', DataTypes.CATEGORICAL, InputTypes.STATIC_INPUT), + ('type', DataTypes.CATEGORICAL, InputTypes.STATIC_INPUT), + ('cluster', DataTypes.CATEGORICAL, InputTypes.STATIC_INPUT), + ('family', DataTypes.CATEGORICAL, InputTypes.STATIC_INPUT), + ('class', DataTypes.CATEGORICAL, InputTypes.STATIC_INPUT), + ('perishable', DataTypes.CATEGORICAL, InputTypes.STATIC_INPUT) + ] + + def __init__(self): + """Initialises formatter.""" + + self.identifiers = None + self._real_scalers = None + self._cat_scalers = None + self._target_scaler = None + self._num_classes_per_cat_input = None + + def split_data(self, df, valid_boundary=None, test_boundary=None): + """Splits data frame into training-validation-test data frames. + + This also calibrates scaling object, and transforms data for each split. + + Args: + df: Source data frame to split. + valid_boundary: Starting year for validation data + test_boundary: Starting year for test data + + Returns: + Tuple of transformed (train, valid, test) data. + """ + + print('Formatting train-valid-test splits.') + + if valid_boundary is None: + valid_boundary = pd.datetime(2015, 12, 1) + + fixed_params = self.get_fixed_params() + time_steps = fixed_params['total_time_steps'] + lookback = fixed_params['num_encoder_steps'] + forecast_horizon = time_steps - lookback + + df['date'] = pd.to_datetime(df['date']) + df_lists = {'train': [], 'valid': [], 'test': []} + for _, sliced in df.groupby('traj_id'): + index = sliced['date'] + train = sliced.loc[index < valid_boundary] + train_len = len(train) + valid_len = train_len + forecast_horizon + valid = sliced.iloc[train_len - lookback:valid_len, :] + test = sliced.iloc[valid_len - lookback:valid_len + forecast_horizon, :] + + sliced_map = {'train': train, 'valid': valid, 'test': test} + + for k in sliced_map: + item = sliced_map[k] + + if len(item) >= time_steps: + df_lists[k].append(item) + + dfs = {k: pd.concat(df_lists[k], axis=0) for k in df_lists} + + train = dfs['train'] + self.set_scalers(train, set_real=True) + + # Use all data for label encoding to handle labels not present in training. + self.set_scalers(df, set_real=False) + + # Filter out identifiers not present in training (i.e. cold-started items). + def filter_ids(frame): + identifiers = set(self.identifiers) + index = frame['traj_id'] + return frame.loc[index.apply(lambda x: x in identifiers)] + + valid = filter_ids(dfs['valid']) + test = filter_ids(dfs['test']) + + return (self.transform_inputs(data) for data in [train, valid, test]) + + def set_scalers(self, df, set_real=True): + """Calibrates scalers using the data supplied. + + Label encoding is applied to the entire dataset (i.e. including test), + so that unseen labels can be handled at run-time. + + Args: + df: Data to use to calibrate scalers. + set_real: Whether to fit set real-valued or categorical scalers + """ + print('Setting scalers with training data...') + + column_definitions = self.get_column_definition() + id_column = utils.get_single_col_by_input_type(InputTypes.ID, + column_definitions) + target_column = utils.get_single_col_by_input_type(InputTypes.TARGET, + column_definitions) + + if set_real: + + # Extract identifiers in case required + self.identifiers = list(df[id_column].unique()) + + # Format real scalers + self._real_scalers = {} + for col in ['oil', 'transactions', 'log_sales']: + self._real_scalers[col] = (df[col].mean(), df[col].std()) + + self._target_scaler = (df[target_column].mean(), df[target_column].std()) + + else: + # Format categorical scalers + categorical_inputs = utils.extract_cols_from_data_type( + DataTypes.CATEGORICAL, column_definitions, + {InputTypes.ID, InputTypes.TIME}) + + categorical_scalers = {} + num_classes = [] + if self.identifiers is None: + raise ValueError('Scale real-valued inputs first!') + id_set = set(self.identifiers) + valid_idx = df['traj_id'].apply(lambda x: x in id_set) + for col in categorical_inputs: + # Set all to str so that we don't have mixed integer/string columns + srs = df[col].apply(str).loc[valid_idx] + categorical_scalers[col] = sklearn.preprocessing.LabelEncoder().fit( + srs.values) + + num_classes.append(srs.nunique()) + + # Set categorical scaler outputs + self._cat_scalers = categorical_scalers + self._num_classes_per_cat_input = num_classes + + def transform_inputs(self, df): + """Performs feature transformations. + + This includes both feature engineering, preprocessing and normalisation. + + Args: + df: Data frame to transform. + + Returns: + Transformed data frame. + + """ + output = df.copy() + + if self._real_scalers is None and self._cat_scalers is None: + raise ValueError('Scalers have not been set!') + + column_definitions = self.get_column_definition() + + categorical_inputs = utils.extract_cols_from_data_type( + DataTypes.CATEGORICAL, column_definitions, + {InputTypes.ID, InputTypes.TIME}) + + # Format real inputs + for col in ['log_sales', 'oil', 'transactions']: + mean, std = self._real_scalers[col] + output[col] = (df[col] - mean) / std + + if col == 'log_sales': + output[col] = output[col].fillna(0.) # mean imputation + + # Format categorical inputs + for col in categorical_inputs: + string_df = df[col].apply(str) + output[col] = self._cat_scalers[col].transform(string_df) + + return output + + def format_predictions(self, predictions): + """Reverts any normalisation to give predictions in original scale. + + Args: + predictions: Dataframe of model predictions. + + Returns: + Data frame of unnormalised predictions. + """ + output = predictions.copy() + + column_names = predictions.columns + mean, std = self._target_scaler + for col in column_names: + if col not in {'forecast_time', 'identifier'}: + output[col] = (predictions[col] * std) + mean + + return output + + # Default params + def get_fixed_params(self): + """Returns fixed model parameters for experiments.""" + + fixed_params = { + 'total_time_steps': 120, + 'num_encoder_steps': 90, + 'num_epochs': 100, + 'early_stopping_patience': 5, + 'multiprocessing_workers': 5 + } + + return fixed_params + + def get_default_model_params(self): + """Returns default optimised model parameters.""" + + model_params = { + 'dropout_rate': 0.1, + 'hidden_layer_size': 240, + 'learning_rate': 0.001, + 'minibatch_size': 128, + 'max_gradient_norm': 100., + 'num_heads': 4, + 'stack_size': 1 + } + + return model_params + + def get_num_samples_for_calibration(self): + """Gets the default number of training and validation samples. + + Use to sub-sample the data for network calibration and a value of -1 uses + all available samples. + + Returns: + Tuple of (training samples, validation samples) + """ + return 450000, 50000 + + def get_column_definition(self): + """"Formats column definition in order expected by the TFT. + + Modified for Favorita to match column order of original experiment. + + Returns: + Favorita-specific column definition + """ + + column_definition = self._column_definition + + # Sanity checks first. + # Ensure only one ID and time column exist + def _check_single_column(input_type): + + length = len([tup for tup in column_definition if tup[2] == input_type]) + + if length != 1: + raise ValueError('Illegal number of inputs ({}) of type {}'.format( + length, input_type)) + + _check_single_column(InputTypes.ID) + _check_single_column(InputTypes.TIME) + + identifier = [tup for tup in column_definition if tup[2] == InputTypes.ID] + time = [tup for tup in column_definition if tup[2] == InputTypes.TIME] + real_inputs = [ + tup for tup in column_definition if tup[1] == DataTypes.REAL_VALUED and + tup[2] not in {InputTypes.ID, InputTypes.TIME} + ] + + col_definition_map = {tup[0]: tup for tup in column_definition} + col_order = [ + 'item_nbr', 'store_nbr', 'city', 'state', 'type', 'cluster', 'family', + 'class', 'perishable', 'onpromotion', 'day_of_week', 'national_hol', + 'regional_hol', 'local_hol' + ] + categorical_inputs = [ + col_definition_map[k] for k in col_order if k in col_definition_map + ] + + return identifier + time + real_inputs + categorical_inputs diff --git a/examples/benchmarks/TFT/data_formatters/qlib_Alpha158.py b/examples/benchmarks/TFT/data_formatters/qlib_Alpha158.py new file mode 100644 index 000000000..aa081fb17 --- /dev/null +++ b/examples/benchmarks/TFT/data_formatters/qlib_Alpha158.py @@ -0,0 +1,220 @@ +# coding=utf-8 +# Copyright 2020 The Google Research Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Lint as: python3 +"""Custom formatting functions for Alpha158 dataset. + +Defines dataset specific column definitions and data transformations. +""" + +import data_formatters.base +import libs.utils as utils +import sklearn.preprocessing + +GenericDataFormatter = data_formatters.base.GenericDataFormatter +DataTypes = data_formatters.base.DataTypes +InputTypes = data_formatters.base.InputTypes + +class Alpha158Formatter(GenericDataFormatter): + """Defines and formats data for the Alpha158 dataset. + + Attributes: + column_definition: Defines input and data type of column used in the + experiment. + identifiers: Entity identifiers used in experiments. + """ + + _column_definition = [ + ('instrument', DataTypes.CATEGORICAL, InputTypes.ID), + ('LABEL0', DataTypes.REAL_VALUED, InputTypes.TARGET), + ('date', DataTypes.DATE, InputTypes.TIME), + ('month', DataTypes.CATEGORICAL, InputTypes.KNOWN_INPUT), + ('day_of_week', DataTypes.CATEGORICAL, InputTypes.KNOWN_INPUT), + # Selected 10 features + ('RESI5', DataTypes.REAL_VALUED, InputTypes.OBSERVED_INPUT), + ('WVMA5', DataTypes.REAL_VALUED, InputTypes.OBSERVED_INPUT), + ('RSQR5', DataTypes.REAL_VALUED, InputTypes.OBSERVED_INPUT), + ('KLEN', DataTypes.REAL_VALUED, InputTypes.OBSERVED_INPUT), + ('RSQR10', DataTypes.REAL_VALUED, InputTypes.OBSERVED_INPUT), + ('CORR5', DataTypes.REAL_VALUED, InputTypes.OBSERVED_INPUT), + ('CORD5', DataTypes.REAL_VALUED, InputTypes.OBSERVED_INPUT), + ('CORR10', DataTypes.REAL_VALUED, InputTypes.OBSERVED_INPUT), + ('ROC60', DataTypes.REAL_VALUED, InputTypes.OBSERVED_INPUT), + ('RESI10', DataTypes.REAL_VALUED, InputTypes.OBSERVED_INPUT), + ('const', DataTypes.CATEGORICAL, InputTypes.STATIC_INPUT), + ] + + def __init__(self): + """Initialises formatter.""" + + self.identifiers = None + self._real_scalers = None + self._cat_scalers = None + self._target_scaler = None + self._num_classes_per_cat_input = None + + def split_data(self, df, valid_boundary=2016, test_boundary=2018): + """Splits data frame into training-validation-test data frames. + + This also calibrates scaling object, and transforms data for each split. + + Args: + df: Source data frame to split. + valid_boundary: Starting year for validation data + test_boundary: Starting year for test data + + Returns: + Tuple of transformed (train, valid, test) data. + """ + + print('Formatting train-valid-test splits.') + + index = df['year'] + train = df.loc[index < valid_boundary] + valid = df.loc[(index >= valid_boundary) & (index < test_boundary)] + test = df.loc[index >= test_boundary] + + self.set_scalers(train) + + return (self.transform_inputs(data) for data in [train, valid, test]) + + def set_scalers(self, df): + """Calibrates scalers using the data supplied. + + Args: + df: Data to use to calibrate scalers. + """ + print('Setting scalers with training data...') + + column_definitions = self.get_column_definition() + id_column = utils.get_single_col_by_input_type(InputTypes.ID, + column_definitions) + target_column = utils.get_single_col_by_input_type(InputTypes.TARGET, + column_definitions) + + # Extract identifiers in case required + self.identifiers = list(df[id_column].unique()) + + # Format real scalers + real_inputs = utils.extract_cols_from_data_type( + DataTypes.REAL_VALUED, column_definitions, + {InputTypes.ID, InputTypes.TIME}) + + data = df[real_inputs].values + self._real_scalers = sklearn.preprocessing.StandardScaler().fit(data) + self._target_scaler = sklearn.preprocessing.StandardScaler().fit( + df[[target_column]].values) # used for predictions + + # Format categorical scalers + categorical_inputs = utils.extract_cols_from_data_type( + DataTypes.CATEGORICAL, column_definitions, + {InputTypes.ID, InputTypes.TIME}) + + categorical_scalers = {} + num_classes = [] + for col in categorical_inputs: + # Set all to str so that we don't have mixed integer/string columns + srs = df[col].apply(str) + categorical_scalers[col] = sklearn.preprocessing.LabelEncoder().fit( + srs.values) + num_classes.append(srs.nunique()) + + # Set categorical scaler outputs + self._cat_scalers = categorical_scalers + self._num_classes_per_cat_input = num_classes + + def transform_inputs(self, df): + """Performs feature transformations. + + This includes both feature engineering, preprocessing and normalisation. + + Args: + df: Data frame to transform. + + Returns: + Transformed data frame. + + """ + output = df.copy() + + if self._real_scalers is None and self._cat_scalers is None: + raise ValueError('Scalers have not been set!') + + column_definitions = self.get_column_definition() + + real_inputs = utils.extract_cols_from_data_type( + DataTypes.REAL_VALUED, column_definitions, + {InputTypes.ID, InputTypes.TIME}) + categorical_inputs = utils.extract_cols_from_data_type( + DataTypes.CATEGORICAL, column_definitions, + {InputTypes.ID, InputTypes.TIME}) + + # Format real inputs + output[real_inputs] = self._real_scalers.transform(df[real_inputs].values) + + # Format categorical inputs + for col in categorical_inputs: + string_df = df[col].apply(str) + output[col] = self._cat_scalers[col].transform(string_df) + + return output + + def format_predictions(self, predictions): + """Reverts any normalisation to give predictions in original scale. + + Args: + predictions: Dataframe of model predictions. + + Returns: + Data frame of unnormalised predictions. + """ + output = predictions.copy() + + column_names = predictions.columns + + for col in column_names: + if col not in {'forecast_time', 'identifier'}: + output[col] = self._target_scaler.inverse_transform(predictions[col]) + + return output + + # Default params + def get_fixed_params(self): + """Returns fixed model parameters for experiments.""" + + fixed_params = { + 'total_time_steps': 16 + 6, + 'num_encoder_steps': 16, + 'num_epochs': 100, + 'early_stopping_patience': 5, + 'multiprocessing_workers': 5, + } + + return fixed_params + + def get_default_model_params(self): + """Returns default optimised model parameters.""" + + model_params = { + 'dropout_rate': 0.3, + 'hidden_layer_size': 160, + 'learning_rate': 0.01, + 'minibatch_size': 64, + 'max_gradient_norm': 0.01, + 'num_heads': 1, + 'stack_size': 1 + } + + return model_params diff --git a/examples/benchmarks/TFT/data_formatters/traffic.py b/examples/benchmarks/TFT/data_formatters/traffic.py new file mode 100644 index 000000000..49401e5cc --- /dev/null +++ b/examples/benchmarks/TFT/data_formatters/traffic.py @@ -0,0 +1,117 @@ +# coding=utf-8 +# Copyright 2020 The Google Research Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Lint as: python3 +"""Custom formatting functions for Traffic dataset. + +Defines dataset specific column definitions and data transformations. This also +performs z-score normalization across the entire dataset, hence re-uses most of +the same functions as volatility. +""" + +import data_formatters.base +import data_formatters.volatility + +VolatilityFormatter = data_formatters.volatility.VolatilityFormatter +DataTypes = data_formatters.base.DataTypes +InputTypes = data_formatters.base.InputTypes + + +class TrafficFormatter(VolatilityFormatter): + """Defines and formats data for the traffic dataset. + + This also performs z-score normalization across the entire dataset, hence + re-uses most of the same functions as volatility. + + Attributes: + column_definition: Defines input and data type of column used in the + experiment. + identifiers: Entity identifiers used in experiments. + """ + + _column_definition = [ + ('id', DataTypes.REAL_VALUED, InputTypes.ID), + ('hours_from_start', DataTypes.REAL_VALUED, InputTypes.TIME), + ('values', DataTypes.REAL_VALUED, InputTypes.TARGET), + ('time_on_day', DataTypes.REAL_VALUED, InputTypes.KNOWN_INPUT), + ('day_of_week', DataTypes.REAL_VALUED, InputTypes.KNOWN_INPUT), + ('hours_from_start', DataTypes.REAL_VALUED, InputTypes.KNOWN_INPUT), + ('categorical_id', DataTypes.CATEGORICAL, InputTypes.STATIC_INPUT), + ] + + def split_data(self, df, valid_boundary=151, test_boundary=166): + """Splits data frame into training-validation-test data frames. + + This also calibrates scaling object, and transforms data for each split. + + Args: + df: Source data frame to split. + valid_boundary: Starting year for validation data + test_boundary: Starting year for test data + + Returns: + Tuple of transformed (train, valid, test) data. + """ + + print('Formatting train-valid-test splits.') + + index = df['sensor_day'] + train = df.loc[index < valid_boundary] + valid = df.loc[(index >= valid_boundary - 7) & (index < test_boundary)] + test = df.loc[index >= test_boundary - 7] + + self.set_scalers(train) + + return (self.transform_inputs(data) for data in [train, valid, test]) + + # Default params + def get_fixed_params(self): + """Returns fixed model parameters for experiments.""" + + fixed_params = { + 'total_time_steps': 8 * 24, + 'num_encoder_steps': 7 * 24, + 'num_epochs': 100, + 'early_stopping_patience': 5, + 'multiprocessing_workers': 5 + } + + return fixed_params + + def get_default_model_params(self): + """Returns default optimised model parameters.""" + + model_params = { + 'dropout_rate': 0.3, + 'hidden_layer_size': 320, + 'learning_rate': 0.001, + 'minibatch_size': 128, + 'max_gradient_norm': 100., + 'num_heads': 4, + 'stack_size': 1 + } + + return model_params + + def get_num_samples_for_calibration(self): + """Gets the default number of training and validation samples. + + Use to sub-sample the data for network calibration and a value of -1 uses + all available samples. + + Returns: + Tuple of (training samples, validation samples) + """ + return 450000, 50000 diff --git a/examples/benchmarks/TFT/data_formatters/volatility.py b/examples/benchmarks/TFT/data_formatters/volatility.py new file mode 100644 index 000000000..37923a275 --- /dev/null +++ b/examples/benchmarks/TFT/data_formatters/volatility.py @@ -0,0 +1,214 @@ +# coding=utf-8 +# Copyright 2020 The Google Research Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Lint as: python3 +"""Custom formatting functions for Volatility dataset. + +Defines dataset specific column definitions and data transformations. +""" + +import data_formatters.base +import libs.utils as utils +import sklearn.preprocessing + +GenericDataFormatter = data_formatters.base.GenericDataFormatter +DataTypes = data_formatters.base.DataTypes +InputTypes = data_formatters.base.InputTypes + + +class VolatilityFormatter(GenericDataFormatter): + """Defines and formats data for the volatility dataset. + + Attributes: + column_definition: Defines input and data type of column used in the + experiment. + identifiers: Entity identifiers used in experiments. + """ + + _column_definition = [ + ('Symbol', DataTypes.CATEGORICAL, InputTypes.ID), + ('date', DataTypes.DATE, InputTypes.TIME), + ('log_vol', DataTypes.REAL_VALUED, InputTypes.TARGET), + ('open_to_close', DataTypes.REAL_VALUED, InputTypes.OBSERVED_INPUT), + ('days_from_start', DataTypes.REAL_VALUED, InputTypes.KNOWN_INPUT), + ('day_of_week', DataTypes.CATEGORICAL, InputTypes.KNOWN_INPUT), + ('day_of_month', DataTypes.CATEGORICAL, InputTypes.KNOWN_INPUT), + ('week_of_year', DataTypes.CATEGORICAL, InputTypes.KNOWN_INPUT), + ('month', DataTypes.CATEGORICAL, InputTypes.KNOWN_INPUT), + ('Region', DataTypes.CATEGORICAL, InputTypes.STATIC_INPUT), + ] + + def __init__(self): + """Initialises formatter.""" + + self.identifiers = None + self._real_scalers = None + self._cat_scalers = None + self._target_scaler = None + self._num_classes_per_cat_input = None + + def split_data(self, df, valid_boundary=2016, test_boundary=2018): + """Splits data frame into training-validation-test data frames. + + This also calibrates scaling object, and transforms data for each split. + + Args: + df: Source data frame to split. + valid_boundary: Starting year for validation data + test_boundary: Starting year for test data + + Returns: + Tuple of transformed (train, valid, test) data. + """ + + print('Formatting train-valid-test splits.') + + index = df['year'] + train = df.loc[index < valid_boundary] + valid = df.loc[(index >= valid_boundary) & (index < test_boundary)] + test = df.loc[index >= test_boundary] + + self.set_scalers(train) + + return (self.transform_inputs(data) for data in [train, valid, test]) + + def set_scalers(self, df): + """Calibrates scalers using the data supplied. + + Args: + df: Data to use to calibrate scalers. + """ + print('Setting scalers with training data...') + + column_definitions = self.get_column_definition() + id_column = utils.get_single_col_by_input_type(InputTypes.ID, + column_definitions) + target_column = utils.get_single_col_by_input_type(InputTypes.TARGET, + column_definitions) + + # Extract identifiers in case required + self.identifiers = list(df[id_column].unique()) + + # Format real scalers + real_inputs = utils.extract_cols_from_data_type( + DataTypes.REAL_VALUED, column_definitions, + {InputTypes.ID, InputTypes.TIME}) + + data = df[real_inputs].values + self._real_scalers = sklearn.preprocessing.StandardScaler().fit(data) + self._target_scaler = sklearn.preprocessing.StandardScaler().fit( + df[[target_column]].values) # used for predictions + + # Format categorical scalers + categorical_inputs = utils.extract_cols_from_data_type( + DataTypes.CATEGORICAL, column_definitions, + {InputTypes.ID, InputTypes.TIME}) + + categorical_scalers = {} + num_classes = [] + for col in categorical_inputs: + # Set all to str so that we don't have mixed integer/string columns + srs = df[col].apply(str) + categorical_scalers[col] = sklearn.preprocessing.LabelEncoder().fit( + srs.values) + num_classes.append(srs.nunique()) + + # Set categorical scaler outputs + self._cat_scalers = categorical_scalers + self._num_classes_per_cat_input = num_classes + + def transform_inputs(self, df): + """Performs feature transformations. + + This includes both feature engineering, preprocessing and normalisation. + + Args: + df: Data frame to transform. + + Returns: + Transformed data frame. + + """ + output = df.copy() + + if self._real_scalers is None and self._cat_scalers is None: + raise ValueError('Scalers have not been set!') + + column_definitions = self.get_column_definition() + + real_inputs = utils.extract_cols_from_data_type( + DataTypes.REAL_VALUED, column_definitions, + {InputTypes.ID, InputTypes.TIME}) + categorical_inputs = utils.extract_cols_from_data_type( + DataTypes.CATEGORICAL, column_definitions, + {InputTypes.ID, InputTypes.TIME}) + + # Format real inputs + output[real_inputs] = self._real_scalers.transform(df[real_inputs].values) + + # Format categorical inputs + for col in categorical_inputs: + string_df = df[col].apply(str) + output[col] = self._cat_scalers[col].transform(string_df) + + return output + + def format_predictions(self, predictions): + """Reverts any normalisation to give predictions in original scale. + + Args: + predictions: Dataframe of model predictions. + + Returns: + Data frame of unnormalised predictions. + """ + output = predictions.copy() + + column_names = predictions.columns + + for col in column_names: + if col not in {'forecast_time', 'identifier'}: + output[col] = self._target_scaler.inverse_transform(predictions[col]) + + return output + + # Default params + def get_fixed_params(self): + """Returns fixed model parameters for experiments.""" + + fixed_params = { + 'total_time_steps': 252 + 5, + 'num_encoder_steps': 252, + 'num_epochs': 100, + 'early_stopping_patience': 5, + 'multiprocessing_workers': 5, + } + + return fixed_params + + def get_default_model_params(self): + """Returns default optimised model parameters.""" + + model_params = { + 'dropout_rate': 0.3, + 'hidden_layer_size': 160, + 'learning_rate': 0.01, + 'minibatch_size': 64, + 'max_gradient_norm': 0.01, + 'num_heads': 1, + 'stack_size': 1 + } + + return model_params diff --git a/examples/benchmarks/TFT/expt_settings/__init__.py b/examples/benchmarks/TFT/expt_settings/__init__.py new file mode 100644 index 000000000..9a1980462 --- /dev/null +++ b/examples/benchmarks/TFT/expt_settings/__init__.py @@ -0,0 +1,15 @@ +# coding=utf-8 +# Copyright 2020 The Google Research Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + diff --git a/examples/benchmarks/TFT/expt_settings/configs.py b/examples/benchmarks/TFT/expt_settings/configs.py new file mode 100644 index 000000000..d28a39bb0 --- /dev/null +++ b/examples/benchmarks/TFT/expt_settings/configs.py @@ -0,0 +1,111 @@ +# coding=utf-8 +# Copyright 2020 The Google Research Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Lint as: python3 +"""Default configs for TFT experiments. + +Contains the default output paths for data, serialised models and predictions +for the main experiments used in the publication. +""" + +import os + +import data_formatters.electricity +import data_formatters.favorita +import data_formatters.traffic +import data_formatters.volatility +import data_formatters.qlib_Alpha158 + + +class ExperimentConfig(object): + """Defines experiment configs and paths to outputs. + + Attributes: + root_folder: Root folder to contain all experimental outputs. + experiment: Name of experiment to run. + data_folder: Folder to store data for experiment. + model_folder: Folder to store serialised models. + results_folder: Folder to store results. + data_csv_path: Path to primary data csv file used in experiment. + hyperparam_iterations: Default number of random search iterations for + experiment. + """ + + default_experiments = ['volatility', 'electricity', 'traffic', 'favorita', 'Alpha158'] + + def __init__(self, experiment='volatility', root_folder=None): + """Creates configs based on default experiment chosen. + + Args: + experiment: Name of experiment. + root_folder: Root folder to save all outputs of training. + """ + + if experiment not in self.default_experiments: + raise ValueError('Unrecognised experiment={}'.format(experiment)) + + # Defines all relevant paths + if root_folder is None: + root_folder = os.path.join( + os.path.dirname(os.path.realpath(__file__)), '..', 'outputs') + print('Using root folder {}'.format(root_folder)) + + self.root_folder = root_folder + self.experiment = experiment + self.data_folder = os.path.join(root_folder, 'data', experiment) + self.model_folder = os.path.join(root_folder, 'saved_models', experiment) + self.results_folder = os.path.join(root_folder, 'results', experiment) + + # Creates folders if they don't exist + for relevant_directory in [ + self.root_folder, self.data_folder, self.model_folder, + self.results_folder + ]: + if not os.path.exists(relevant_directory): + os.makedirs(relevant_directory) + + @property + def data_csv_path(self): + csv_map = { + 'volatility': 'formatted_omi_vol.csv', + 'electricity': 'hourly_electricity.csv', + 'traffic': 'hourly_data.csv', + 'favorita': 'favorita_consolidated.csv', + 'Alpha158': 'Alpha158.csv', + } + + return os.path.join(self.data_folder, csv_map[self.experiment]) + + @property + def hyperparam_iterations(self): + + return 240 if self.experiment == 'volatility' else 60 + + def make_data_formatter(self): + """Gets a data formatter object for experiment. + + Returns: + Default DataFormatter per experiment. + """ + + data_formatter_class = { + 'volatility': data_formatters.volatility.VolatilityFormatter, + 'electricity': data_formatters.electricity.ElectricityFormatter, + 'traffic': data_formatters.traffic.TrafficFormatter, + 'favorita': data_formatters.favorita.FavoritaFormatter, + 'Alpha158': data_formatters.qlib_Alpha158.Alpha158Formatter, + } + + return data_formatter_class[self.experiment]() diff --git a/examples/benchmarks/TFT/libs/__init__.py b/examples/benchmarks/TFT/libs/__init__.py new file mode 100644 index 000000000..9a1980462 --- /dev/null +++ b/examples/benchmarks/TFT/libs/__init__.py @@ -0,0 +1,15 @@ +# coding=utf-8 +# Copyright 2020 The Google Research Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + diff --git a/examples/benchmarks/TFT/libs/hyperparam_opt.py b/examples/benchmarks/TFT/libs/hyperparam_opt.py new file mode 100644 index 000000000..c9bc19e7c --- /dev/null +++ b/examples/benchmarks/TFT/libs/hyperparam_opt.py @@ -0,0 +1,438 @@ +# coding=utf-8 +# Copyright 2020 The Google Research Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Lint as: python3 +"""Classes used for hyperparameter optimisation. + +Two main classes exist: +1) HyperparamOptManager used for optimisation on a single machine/GPU. +2) DistributedHyperparamOptManager for multiple GPUs on different machines. +""" + +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +import collections +import os +import shutil +import libs.utils as utils +import numpy as np +import pandas as pd + +Deque = collections.deque + + +class HyperparamOptManager: + """Manages hyperparameter optimisation using random search for a single GPU. + + Attributes: + param_ranges: Discrete hyperparameter range for random search. + results: Dataframe of validation results. + fixed_params: Fixed model parameters per experiment. + saved_params: Dataframe of parameters trained. + best_score: Minimum validation loss observed thus far. + optimal_name: Key to best configuration. + hyperparam_folder: Where to save optimisation outputs. + """ + + def __init__(self, + param_ranges, + fixed_params, + model_folder, + override_w_fixed_params=True): + """Instantiates model. + + Args: + param_ranges: Discrete hyperparameter range for random search. + fixed_params: Fixed model parameters per experiment. + model_folder: Folder to store optimisation artifacts. + override_w_fixed_params: Whether to override serialsed fixed model + parameters with new supplied values. + """ + + self.param_ranges = param_ranges + + self._max_tries = 1000 + self.results = pd.DataFrame() + self.fixed_params = fixed_params + self.saved_params = pd.DataFrame() + + self.best_score = np.Inf + self.optimal_name = "" + + # Setup + # Create folder for saving if its not there + self.hyperparam_folder = model_folder + utils.create_folder_if_not_exist(self.hyperparam_folder) + + self._override_w_fixed_params = override_w_fixed_params + + def load_results(self): + """Loads results from previous hyperparameter optimisation. + + Returns: + A boolean indicating if previous results can be loaded. + """ + print("Loading results from", self.hyperparam_folder) + + results_file = os.path.join(self.hyperparam_folder, "results.csv") + params_file = os.path.join(self.hyperparam_folder, "params.csv") + + if os.path.exists(results_file) and os.path.exists(params_file): + + self.results = pd.read_csv(results_file, index_col=0) + self.saved_params = pd.read_csv(params_file, index_col=0) + + if not self.results.empty: + self.results.at["loss"] = self.results.loc["loss"].apply(float) + self.best_score = self.results.loc["loss"].min() + + is_optimal = self.results.loc["loss"] == self.best_score + self.optimal_name = self.results.T[is_optimal].index[0] + + return True + + return False + + def _get_params_from_name(self, name): + """Returns previously saved parameters given a key.""" + params = self.saved_params + + selected_params = dict(params[name]) + + if self._override_w_fixed_params: + for k in self.fixed_params: + selected_params[k] = self.fixed_params[k] + + return selected_params + + def get_best_params(self): + """Returns the optimal hyperparameters thus far.""" + + optimal_name = self.optimal_name + + return self._get_params_from_name(optimal_name) + + def clear(self): + """Clears all previous results and saved parameters.""" + shutil.rmtree(self.hyperparam_folder) + os.makedirs(self.hyperparam_folder) + self.results = pd.DataFrame() + self.saved_params = pd.DataFrame() + + def _check_params(self, params): + """Checks that parameter map is properly defined.""" + + valid_fields = list(self.param_ranges.keys()) + list( + self.fixed_params.keys()) + invalid_fields = [k for k in params if k not in valid_fields] + missing_fields = [k for k in valid_fields if k not in params] + + if invalid_fields: + raise ValueError("Invalid Fields Found {} - Valid ones are {}".format( + invalid_fields, valid_fields)) + if missing_fields: + raise ValueError("Missing Fields Found {} - Valid ones are {}".format( + missing_fields, valid_fields)) + + def _get_name(self, params): + """Returns a unique key for the supplied set of params.""" + + self._check_params(params) + + fields = list(params.keys()) + fields.sort() + + return "_".join([str(params[k]) for k in fields]) + + def get_next_parameters(self, ranges_to_skip=None): + """Returns the next set of parameters to optimise. + + Args: + ranges_to_skip: Explicitly defines a set of keys to skip. + """ + if ranges_to_skip is None: + ranges_to_skip = set(self.results.index) + + if not isinstance(self.param_ranges, dict): + raise ValueError("Only works for random search!") + + param_range_keys = list(self.param_ranges.keys()) + param_range_keys.sort() + + def _get_next(): + """Returns next hyperparameter set per try.""" + + parameters = { + k: np.random.choice(self.param_ranges[k]) for k in param_range_keys + } + + # Adds fixed params + for k in self.fixed_params: + parameters[k] = self.fixed_params[k] + + return parameters + + for _ in range(self._max_tries): + + parameters = _get_next() + name = self._get_name(parameters) + + if name not in ranges_to_skip: + return parameters + + raise ValueError("Exceeded max number of hyperparameter searches!!") + + def update_score(self, parameters, loss, model, info=""): + """Updates the results from last optimisation run. + + Args: + parameters: Hyperparameters used in optimisation. + loss: Validation loss obtained. + model: Model to serialised if required. + info: Any ancillary information to tag on to results. + + Returns: + Boolean flag indicating if the model is the best seen so far. + """ + + if np.isnan(loss): + loss = np.Inf + + if not os.path.isdir(self.hyperparam_folder): + os.makedirs(self.hyperparam_folder) + + name = self._get_name(parameters) + + is_optimal = self.results.empty or loss < self.best_score + + # save the first model + if is_optimal: + # Try saving first, before updating info + if model is not None: + print("Optimal model found, updating") + model.save(self.hyperparam_folder) + self.best_score = loss + self.optimal_name = name + + self.results[name] = pd.Series({"loss": loss, "info": info}) + self.saved_params[name] = pd.Series(parameters) + + self.results.to_csv(os.path.join(self.hyperparam_folder, "results.csv")) + self.saved_params.to_csv(os.path.join(self.hyperparam_folder, "params.csv")) + + return is_optimal + + +class DistributedHyperparamOptManager(HyperparamOptManager): + """Manages distributed hyperparameter optimisation across many gpus.""" + + def __init__(self, + param_ranges, + fixed_params, + root_model_folder, + worker_number, + search_iterations=1000, + num_iterations_per_worker=5, + clear_serialised_params=False): + """Instantiates optimisation manager. + + This hyperparameter optimisation pre-generates #search_iterations + hyperparameter combinations and serialises them + at the start. At runtime, each worker goes through their own set of + parameter ranges. The pregeneration + allows for multiple workers to run in parallel on different machines without + resulting in parameter overlaps. + + Args: + param_ranges: Discrete hyperparameter range for random search. + fixed_params: Fixed model parameters per experiment. + root_model_folder: Folder to store optimisation artifacts. + worker_number: Worker index definining which set of hyperparameters to + test. + search_iterations: Maximum numer of random search iterations. + num_iterations_per_worker: How many iterations are handled per worker. + clear_serialised_params: Whether to regenerate hyperparameter + combinations. + """ + + max_workers = int(np.ceil(search_iterations / num_iterations_per_worker)) + + # Sanity checks + if worker_number > max_workers: + raise ValueError( + "Worker number ({}) cannot be larger than the total number of workers!" + .format(max_workers)) + if worker_number > search_iterations: + raise ValueError( + "Worker number ({}) cannot be larger than the max search iterations ({})!" + .format(worker_number, search_iterations)) + + print("*** Creating hyperparameter manager for worker {} ***".format( + worker_number)) + + hyperparam_folder = os.path.join(root_model_folder, str(worker_number)) + super().__init__( + param_ranges, + fixed_params, + hyperparam_folder, + override_w_fixed_params=True) + + serialised_ranges_folder = os.path.join(root_model_folder, "hyperparams") + if clear_serialised_params: + print("Regenerating hyperparameter list") + if os.path.exists(serialised_ranges_folder): + shutil.rmtree(serialised_ranges_folder) + + utils.create_folder_if_not_exist(serialised_ranges_folder) + + self.serialised_ranges_path = os.path.join( + serialised_ranges_folder, "ranges_{}.csv".format(search_iterations)) + self.hyperparam_folder = hyperparam_folder # override + self.worker_num = worker_number + self.total_search_iterations = search_iterations + self.num_iterations_per_worker = num_iterations_per_worker + self.global_hyperparam_df = self.load_serialised_hyperparam_df() + self.worker_search_queue = self._get_worker_search_queue() + + @property + def optimisation_completed(self): + return False if self.worker_search_queue else True + + def get_next_parameters(self): + """Returns next dictionary of hyperparameters to optimise.""" + param_name = self.worker_search_queue.pop() + + params = self.global_hyperparam_df.loc[param_name, :].to_dict() + + # Always override! + for k in self.fixed_params: + print("Overriding saved {}: {}".format(k, self.fixed_params[k])) + + params[k] = self.fixed_params[k] + + return params + + def load_serialised_hyperparam_df(self): + """Loads serialsed hyperparameter ranges from file. + + Returns: + DataFrame containing hyperparameter combinations. + """ + print("Loading params for {} search iterations form {}".format( + self.total_search_iterations, self.serialised_ranges_path)) + + if os.path.exists(self.serialised_ranges_folder): + df = pd.read_csv(self.serialised_ranges_path, index_col=0) + else: + print("Unable to load - regenerating serach ranges instead") + df = self.update_serialised_hyperparam_df() + + return df + + def update_serialised_hyperparam_df(self): + """Regenerates hyperparameter combinations and saves to file. + + Returns: + DataFrame containing hyperparameter combinations. + """ + search_df = self._generate_full_hyperparam_df() + + print("Serialising params for {} search iterations to {}".format( + self.total_search_iterations, self.serialised_ranges_path)) + + search_df.to_csv(self.serialised_ranges_path) + + return search_df + + def _generate_full_hyperparam_df(self): + """Generates actual hyperparameter combinations. + + Returns: + DataFrame containing hyperparameter combinations. + """ + + np.random.seed(131) # for reproducibility of hyperparam list + + name_list = [] + param_list = [] + for _ in range(self.total_search_iterations): + params = super().get_next_parameters(name_list) + + name = self._get_name(params) + + name_list.append(name) + param_list.append(params) + + full_search_df = pd.DataFrame(param_list, index=name_list) + + return full_search_df + + def clear(self): # reset when cleared + """Clears results for hyperparameter manager and resets.""" + super().clear() + self.worker_search_queue = self._get_worker_search_queue() + + def load_results(self): + """Load results from file and queue parameter combinations to try. + + Returns: + Boolean indicating if results were successfully loaded. + """ + success = super().load_results() + + if success: + self.worker_search_queue = self._get_worker_search_queue() + + return success + + def _get_worker_search_queue(self): + """Generates the queue of param combinations for current worker. + + Returns: + Queue of hyperparameter combinations outstanding. + """ + global_df = self.assign_worker_numbers(self.global_hyperparam_df) + worker_df = global_df[global_df["worker"] == self.worker_num] + + left_overs = [s for s in worker_df.index if s not in self.results.columns] + + return Deque(left_overs) + + def assign_worker_numbers(self, df): + """Updates parameter combinations with the index of the worker used. + + Args: + df: DataFrame of parameter combinations. + + Returns: + Updated DataFrame with worker number. + """ + output = df.copy() + + n = self.total_search_iterations + batch_size = self.num_iterations_per_worker + + max_worker_num = int(np.ceil(n / batch_size)) + + worker_idx = np.concatenate([ + np.tile(i + 1, self.num_iterations_per_worker) + for i in range(max_worker_num) + ]) + + output["worker"] = worker_idx[:len(output)] + + return output diff --git a/examples/benchmarks/TFT/libs/tft_model.py b/examples/benchmarks/TFT/libs/tft_model.py new file mode 100644 index 000000000..2a41f4566 --- /dev/null +++ b/examples/benchmarks/TFT/libs/tft_model.py @@ -0,0 +1,1391 @@ +# coding=utf-8 +# Copyright 2020 The Google Research Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Lint as: python3 +"""Temporal Fusion Transformer Model. + +Contains the full TFT architecture and associated components. Defines functions +for training, evaluation and prediction using simple Pandas Dataframe inputs. +""" + +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +import gc +import json +import os +import shutil + +import data_formatters.base +import libs.utils as utils +import numpy as np +import pandas as pd +import tensorflow as tf + +# Layer definitions. +concat = tf.keras.backend.concatenate +stack = tf.keras.backend.stack +K = tf.keras.backend +Add = tf.keras.layers.Add +LayerNorm = tf.keras.layers.LayerNormalization +Dense = tf.keras.layers.Dense +Multiply = tf.keras.layers.Multiply +Dropout = tf.keras.layers.Dropout +Activation = tf.keras.layers.Activation +Lambda = tf.keras.layers.Lambda + +# Default input types. +InputTypes = data_formatters.base.InputTypes + + +# Layer utility functions. +def linear_layer(size, + activation=None, + use_time_distributed=False, + use_bias=True): + """Returns simple Keras linear layer. + + Args: + size: Output size + activation: Activation function to apply if required + use_time_distributed: Whether to apply layer across time + use_bias: Whether bias should be included in layer + """ + linear = tf.keras.layers.Dense(size, activation=activation, use_bias=use_bias) + if use_time_distributed: + linear = tf.keras.layers.TimeDistributed(linear) + return linear + + +def apply_mlp(inputs, + hidden_size, + output_size, + output_activation=None, + hidden_activation='tanh', + use_time_distributed=False): + """Applies simple feed-forward network to an input. + + Args: + inputs: MLP inputs + hidden_size: Hidden state size + output_size: Output size of MLP + output_activation: Activation function to apply on output + hidden_activation: Activation function to apply on input + use_time_distributed: Whether to apply across time + + Returns: + Tensor for MLP outputs. + """ + if use_time_distributed: + hidden = tf.keras.layers.TimeDistributed( + tf.keras.layers.Dense(hidden_size, activation=hidden_activation))( + inputs) + return tf.keras.layers.TimeDistributed( + tf.keras.layers.Dense(output_size, activation=output_activation))( + hidden) + else: + hidden = tf.keras.layers.Dense( + hidden_size, activation=hidden_activation)( + inputs) + return tf.keras.layers.Dense( + output_size, activation=output_activation)( + hidden) + + +def apply_gating_layer(x, + hidden_layer_size, + dropout_rate=None, + use_time_distributed=True, + activation=None): + """Applies a Gated Linear Unit (GLU) to an input. + + Args: + x: Input to gating layer + hidden_layer_size: Dimension of GLU + dropout_rate: Dropout rate to apply if any + use_time_distributed: Whether to apply across time + activation: Activation function to apply to the linear feature transform if + necessary + + Returns: + Tuple of tensors for: (GLU output, gate) + """ + + if dropout_rate is not None: + x = tf.keras.layers.Dropout(dropout_rate)(x) + + if use_time_distributed: + activation_layer = tf.keras.layers.TimeDistributed( + tf.keras.layers.Dense(hidden_layer_size, activation=activation))( + x) + gated_layer = tf.keras.layers.TimeDistributed( + tf.keras.layers.Dense(hidden_layer_size, activation='sigmoid'))( + x) + else: + activation_layer = tf.keras.layers.Dense( + hidden_layer_size, activation=activation)( + x) + gated_layer = tf.keras.layers.Dense( + hidden_layer_size, activation='sigmoid')( + x) + + return tf.keras.layers.Multiply()([activation_layer, + gated_layer]), gated_layer + + +def add_and_norm(x_list): + """Applies skip connection followed by layer normalisation. + + Args: + x_list: List of inputs to sum for skip connection + + Returns: + Tensor output from layer. + """ + tmp = Add()(x_list) + tmp = LayerNorm()(tmp) + return tmp + + +def gated_residual_network(x, + hidden_layer_size, + output_size=None, + dropout_rate=None, + use_time_distributed=True, + additional_context=None, + return_gate=False): + """Applies the gated residual network (GRN) as defined in paper. + + Args: + x: Network inputs + hidden_layer_size: Internal state size + output_size: Size of output layer + dropout_rate: Dropout rate if dropout is applied + use_time_distributed: Whether to apply network across time dimension + additional_context: Additional context vector to use if relevant + return_gate: Whether to return GLU gate for diagnostic purposes + + Returns: + Tuple of tensors for: (GRN output, GLU gate) + """ + + # Setup skip connection + if output_size is None: + output_size = hidden_layer_size + skip = x + else: + linear = Dense(output_size) + if use_time_distributed: + linear = tf.keras.layers.TimeDistributed(linear) + skip = linear(x) + + # Apply feedforward network + hidden = linear_layer( + hidden_layer_size, + activation=None, + use_time_distributed=use_time_distributed)( + x) + if additional_context is not None: + hidden = hidden + linear_layer( + hidden_layer_size, + activation=None, + use_time_distributed=use_time_distributed, + use_bias=False)( + additional_context) + hidden = tf.keras.layers.Activation('elu')(hidden) + hidden = linear_layer( + hidden_layer_size, + activation=None, + use_time_distributed=use_time_distributed)( + hidden) + + gating_layer, gate = apply_gating_layer( + hidden, + output_size, + dropout_rate=dropout_rate, + use_time_distributed=use_time_distributed, + activation=None) + + if return_gate: + return add_and_norm([skip, gating_layer]), gate + else: + return add_and_norm([skip, gating_layer]) + + +# Attention Components. +def get_decoder_mask(self_attn_inputs): + """Returns causal mask to apply for self-attention layer. + + Args: + self_attn_inputs: Inputs to self attention layer to determine mask shape + """ + len_s = tf.shape(self_attn_inputs)[1] + bs = tf.shape(self_attn_inputs)[:1] + mask = K.cumsum(tf.eye(len_s, batch_shape=bs), 1) + return mask + + +class ScaledDotProductAttention(): + """Defines scaled dot product attention layer. + + Attributes: + dropout: Dropout rate to use + activation: Normalisation function for scaled dot product attention (e.g. + softmax by default) + """ + + def __init__(self, attn_dropout=0.0): + self.dropout = Dropout(attn_dropout) + self.activation = Activation('softmax') + + def __call__(self, q, k, v, mask): + """Applies scaled dot product attention. + + Args: + q: Queries + k: Keys + v: Values + mask: Masking if required -- sets softmax to very large value + + Returns: + Tuple of (layer outputs, attention weights) + """ + temper = tf.sqrt(tf.cast(tf.shape(k)[-1], dtype='float32')) + attn = Lambda(lambda x: K.batch_dot(x[0], x[1], axes=[2, 2]) / temper)( + [q, k]) # shape=(batch, q, k) + if mask is not None: + mmask = Lambda(lambda x: (-1e+9) * (1. - K.cast(x, 'float32')))( + mask) # setting to infinity + attn = Add()([attn, mmask]) + attn = self.activation(attn) + attn = self.dropout(attn) + output = Lambda(lambda x: K.batch_dot(x[0], x[1]))([attn, v]) + return output, attn + + +class InterpretableMultiHeadAttention(): + """Defines interpretable multi-head attention layer. + + Attributes: + n_head: Number of heads + d_k: Key/query dimensionality per head + d_v: Value dimensionality + dropout: Dropout rate to apply + qs_layers: List of queries across heads + ks_layers: List of keys across heads + vs_layers: List of values across heads + attention: Scaled dot product attention layer + w_o: Output weight matrix to project internal state to the original TFT + state size + """ + + def __init__(self, n_head, d_model, dropout): + """Initialises layer. + + Args: + n_head: Number of heads + d_model: TFT state dimensionality + dropout: Dropout discard rate + """ + + self.n_head = n_head + self.d_k = self.d_v = d_k = d_v = d_model // n_head + self.dropout = dropout + + self.qs_layers = [] + self.ks_layers = [] + self.vs_layers = [] + + # Use same value layer to facilitate interp + vs_layer = Dense(d_v, use_bias=False) + + for _ in range(n_head): + self.qs_layers.append(Dense(d_k, use_bias=False)) + self.ks_layers.append(Dense(d_k, use_bias=False)) + self.vs_layers.append(vs_layer) # use same vs_layer + + self.attention = ScaledDotProductAttention() + self.w_o = Dense(d_model, use_bias=False) + + def __call__(self, q, k, v, mask=None): + """Applies interpretable multihead attention. + + Using T to denote the number of time steps fed into the transformer. + + Args: + q: Query tensor of shape=(?, T, d_model) + k: Key of shape=(?, T, d_model) + v: Values of shape=(?, T, d_model) + mask: Masking if required with shape=(?, T, T) + + Returns: + Tuple of (layer outputs, attention weights) + """ + n_head = self.n_head + + heads = [] + attns = [] + for i in range(n_head): + qs = self.qs_layers[i](q) + ks = self.ks_layers[i](k) + vs = self.vs_layers[i](v) + head, attn = self.attention(qs, ks, vs, mask) + + head_dropout = Dropout(self.dropout)(head) + heads.append(head_dropout) + attns.append(attn) + head = K.stack(heads) if n_head > 1 else heads[0] + attn = K.stack(attns) + + outputs = K.mean(head, axis=0) if n_head > 1 else head + outputs = self.w_o(outputs) + outputs = Dropout(self.dropout)(outputs) # output dropout + + return outputs, attn + + +class TFTDataCache(object): + """Caches data for the TFT.""" + + _data_cache = {} + + @classmethod + def update(cls, data, key): + """Updates cached data. + + Args: + data: Source to update + key: Key to dictionary location + """ + cls._data_cache[key] = data + + @classmethod + def get(cls, key): + """Returns data stored at key location.""" + return cls._data_cache[key].copy() + + @classmethod + def contains(cls, key): + """Retuns boolean indicating whether key is present in cache.""" + + return key in cls._data_cache + + +# TFT model definitions. +class TemporalFusionTransformer(object): + """Defines Temporal Fusion Transformer. + + Attributes: + name: Name of model + time_steps: Total number of input time steps per forecast date (i.e. Width + of Temporal fusion decoder N) + input_size: Total number of inputs + output_size: Total number of outputs + category_counts: Number of categories per categorical variable + n_multiprocessing_workers: Number of workers to use for parallel + computations + column_definition: List of tuples of (string, DataType, InputType) that + define each column + quantiles: Quantiles to forecast for TFT + use_cudnn: Whether to use Keras CuDNNLSTM or standard LSTM layers + hidden_layer_size: Internal state size of TFT + dropout_rate: Dropout discard rate + max_gradient_norm: Maximum norm for gradient clipping + learning_rate: Initial learning rate of ADAM optimizer + minibatch_size: Size of minibatches for training + num_epochs: Maximum number of epochs for training + early_stopping_patience: Maximum number of iterations of non-improvement + before early stopping kicks in + num_encoder_steps: Size of LSTM encoder -- i.e. number of past time steps + before forecast date to use + num_stacks: Number of self-attention layers to apply (default is 1 for basic + TFT) + num_heads: Number of heads for interpretable mulit-head attention + model: Keras model for TFT + """ + + def __init__(self, raw_params, use_cudnn=False): + """Builds TFT from parameters. + + Args: + raw_params: Parameters to define TFT + use_cudnn: Whether to use CUDNN GPU optimised LSTM + """ + + self.name = self.__class__.__name__ + + params = dict(raw_params) # copy locally + + # Data parameters + self.time_steps = int(params['total_time_steps']) + self.input_size = int(params['input_size']) + self.output_size = int(params['output_size']) + self.category_counts = json.loads(str(params['category_counts'])) + self.n_multiprocessing_workers = int(params['multiprocessing_workers']) + + # Relevant indices for TFT + self._input_obs_loc = json.loads(str(params['input_obs_loc'])) + self._static_input_loc = json.loads(str(params['static_input_loc'])) + self._known_regular_input_idx = json.loads( + str(params['known_regular_inputs'])) + self._known_categorical_input_idx = json.loads( + str(params['known_categorical_inputs'])) + + self.column_definition = params['column_definition'] + + # Network params + self.quantiles = [0.1, 0.5, 0.9] + self.use_cudnn = use_cudnn # Whether to use GPU optimised LSTM + self.hidden_layer_size = int(params['hidden_layer_size']) + self.dropout_rate = float(params['dropout_rate']) + self.max_gradient_norm = float(params['max_gradient_norm']) + self.learning_rate = float(params['learning_rate']) + self.minibatch_size = int(params['minibatch_size']) + self.num_epochs = int(params['num_epochs']) + self.early_stopping_patience = int(params['early_stopping_patience']) + + self.num_encoder_steps = int(params['num_encoder_steps']) + self.num_stacks = int(params['stack_size']) + self.num_heads = int(params['num_heads']) + + # Serialisation options + self._temp_folder = os.path.join(params['model_folder'], 'tmp') + self.reset_temp_folder() + + # Extra components to store Tensorflow nodes for attention computations + self._input_placeholder = None + self._attention_components = None + self._prediction_parts = None + + print('*** {} params ***'.format(self.name)) + for k in params: + print('# {} = {}'.format(k, params[k])) + + # Build model + self.model = self.build_model() + + def get_tft_embeddings(self, all_inputs): + """Transforms raw inputs to embeddings. + + Applies linear transformation onto continuous variables and uses embeddings + for categorical variables. + + Args: + all_inputs: Inputs to transform + + Returns: + Tensors for transformed inputs. + """ + + time_steps = self.time_steps + + # Sanity checks + for i in self._known_regular_input_idx: + if i in self._input_obs_loc: + raise ValueError('Observation cannot be known a priori!') + for i in self._input_obs_loc: + if i in self._static_input_loc: + raise ValueError('Observation cannot be static!') + + if all_inputs.get_shape().as_list()[-1] != self.input_size: + raise ValueError( + 'Illegal number of inputs! Inputs observed={}, expected={}'.format( + all_inputs.get_shape().as_list()[-1], self.input_size)) + + num_categorical_variables = len(self.category_counts) + num_regular_variables = self.input_size - num_categorical_variables + + embedding_sizes = [ + self.hidden_layer_size for i, size in enumerate(self.category_counts) + ] + + embeddings = [] + for i in range(num_categorical_variables): + + embedding = tf.keras.Sequential([ + tf.keras.layers.InputLayer([time_steps]), + tf.keras.layers.Embedding( + self.category_counts[i], + embedding_sizes[i], + input_length=time_steps, + dtype=tf.float32) + ]) + embeddings.append(embedding) + + regular_inputs, categorical_inputs \ + = all_inputs[:, :, :num_regular_variables], \ + all_inputs[:, :, num_regular_variables:] + + embedded_inputs = [ + embeddings[i](categorical_inputs[Ellipsis, i]) + for i in range(num_categorical_variables) + ] + + # Static inputs + if self._static_input_loc: + static_inputs = [tf.keras.layers.Dense(self.hidden_layer_size)( + regular_inputs[:, 0, i:i + 1]) for i in range(num_regular_variables) + if i in self._static_input_loc] \ + + [embedded_inputs[i][:, 0, :] + for i in range(num_categorical_variables) + if i + num_regular_variables in self._static_input_loc] + static_inputs = tf.keras.backend.stack(static_inputs, axis=1) + + else: + static_inputs = None + + def convert_real_to_embedding(x): + """Applies linear transformation for time-varying inputs.""" + return tf.keras.layers.TimeDistributed( + tf.keras.layers.Dense(self.hidden_layer_size))( + x) + + # Targets + obs_inputs = tf.keras.backend.stack([ + convert_real_to_embedding(regular_inputs[Ellipsis, i:i + 1]) + for i in self._input_obs_loc + ], + axis=-1) + + # Observed (a prioir unknown) inputs + wired_embeddings = [] + for i in range(num_categorical_variables): + if i not in self._known_categorical_input_idx \ + and i + num_regular_variables not in self._input_obs_loc: + e = embeddings[i](categorical_inputs[:, :, i]) + wired_embeddings.append(e) + + unknown_inputs = [] + for i in range(regular_inputs.shape[-1]): + if i not in self._known_regular_input_idx \ + and i not in self._input_obs_loc: + e = convert_real_to_embedding(regular_inputs[Ellipsis, i:i + 1]) + unknown_inputs.append(e) + + if unknown_inputs + wired_embeddings: + unknown_inputs = tf.keras.backend.stack( + unknown_inputs + wired_embeddings, axis=-1) + else: + unknown_inputs = None + + # A priori known inputs + known_regular_inputs = [ + convert_real_to_embedding(regular_inputs[Ellipsis, i:i + 1]) + for i in self._known_regular_input_idx + if i not in self._static_input_loc + ] + known_categorical_inputs = [ + embedded_inputs[i] + for i in self._known_categorical_input_idx + if i + num_regular_variables not in self._static_input_loc + ] + + known_combined_layer = tf.keras.backend.stack( + known_regular_inputs + known_categorical_inputs, axis=-1) + + return unknown_inputs, known_combined_layer, obs_inputs, static_inputs + + def _get_single_col_by_type(self, input_type): + """Returns name of single column for input type.""" + + return utils.get_single_col_by_input_type(input_type, + self.column_definition) + + def training_data_cached(self): + """Returns boolean indicating if training data has been cached.""" + + return TFTDataCache.contains('train') and TFTDataCache.contains('valid') + + def cache_batched_data(self, data, cache_key, num_samples=-1): + """Batches and caches data once for using during training. + + Args: + data: Data to batch and cache + cache_key: Key used for cache + num_samples: Maximum number of samples to extract (-1 to use all data) + """ + + if num_samples > 0: + TFTDataCache.update( + self._batch_sampled_data(data, max_samples=num_samples), cache_key) + else: + TFTDataCache.update(self._batch_data(data), cache_key) + + print('Cached data "{}" updated'.format(cache_key)) + + def _batch_sampled_data(self, data, max_samples): + """Samples segments into a compatible format. + + Args: + data: Sources data to sample and batch + max_samples: Maximum number of samples in batch + + Returns: + Dictionary of batched data with the maximum samples specified. + """ + + if max_samples < 1: + raise ValueError( + 'Illegal number of samples specified! samples={}'.format(max_samples)) + + id_col = self._get_single_col_by_type(InputTypes.ID) + time_col = self._get_single_col_by_type(InputTypes.TIME) + + data.sort_values(by=[id_col, time_col], inplace=True) + + print('Getting valid sampling locations.') + valid_sampling_locations = [] + split_data_map = {} + for identifier, df in data.groupby(id_col): + print('Getting locations for {}'.format(identifier)) + num_entries = len(df) + if num_entries >= self.time_steps: + valid_sampling_locations += [ + (identifier, self.time_steps + i) + for i in range(num_entries - self.time_steps + 1) + ] + split_data_map[identifier] = df + + inputs = np.zeros((max_samples, self.time_steps, self.input_size)) + outputs = np.zeros((max_samples, self.time_steps, self.output_size)) + time = np.empty((max_samples, self.time_steps, 1), dtype=object) + identifiers = np.empty((max_samples, self.time_steps, 1), dtype=object) + + if max_samples > 0 and len(valid_sampling_locations) > max_samples: + print('Extracting {} samples...'.format(max_samples)) + ranges = [ + valid_sampling_locations[i] for i in np.random.choice( + len(valid_sampling_locations), max_samples, replace=False) + ] + else: + print('Max samples={} exceeds # available segments={}'.format( + max_samples, len(valid_sampling_locations))) + ranges = valid_sampling_locations + + id_col = self._get_single_col_by_type(InputTypes.ID) + time_col = self._get_single_col_by_type(InputTypes.TIME) + target_col = self._get_single_col_by_type(InputTypes.TARGET) + input_cols = [ + tup[0] + for tup in self.column_definition + if tup[2] not in {InputTypes.ID, InputTypes.TIME} + ] + + for i, tup in enumerate(ranges): + if (i + 1 % 1000) == 0: + print(i + 1, 'of', max_samples, 'samples done...') + identifier, start_idx = tup + sliced = split_data_map[identifier].iloc[start_idx - + self.time_steps:start_idx] + inputs[i, :, :] = sliced[input_cols] + outputs[i, :, :] = sliced[[target_col]] + time[i, :, 0] = sliced[time_col] + identifiers[i, :, 0] = sliced[id_col] + + sampled_data = { + 'inputs': inputs, + 'outputs': outputs[:, self.num_encoder_steps:, :], + 'active_entries': np.ones_like(outputs[:, self.num_encoder_steps:, :]), + 'time': time, + 'identifier': identifiers + } + + return sampled_data + + def _batch_data(self, data): + """Batches data for training. + + Converts raw dataframe from a 2-D tabular format to a batched 3-D array + to feed into Keras model. + + Args: + data: DataFrame to batch + + Returns: + Batched Numpy array with shape=(?, self.time_steps, self.input_size) + """ + + # Functions. + def _batch_single_entity(input_data): + time_steps = len(input_data) + lags = self.time_steps + x = input_data.values + if time_steps >= lags: + return np.stack( + [x[i:time_steps - (lags - 1) + i, :] for i in range(lags)], axis=1) + + else: + return None + + id_col = self._get_single_col_by_type(InputTypes.ID) + time_col = self._get_single_col_by_type(InputTypes.TIME) + target_col = self._get_single_col_by_type(InputTypes.TARGET) + input_cols = [ + tup[0] + for tup in self.column_definition + if tup[2] not in {InputTypes.ID, InputTypes.TIME} + ] + + data_map = {} + for _, sliced in data.groupby(id_col): + + col_mappings = { + 'identifier': [id_col], + 'time': [time_col], + 'outputs': [target_col], + 'inputs': input_cols + } + + for k in col_mappings: + cols = col_mappings[k] + arr = _batch_single_entity(sliced[cols].copy()) + + if k not in data_map: + data_map[k] = [arr] + else: + data_map[k].append(arr) + + # Combine all data + for k in data_map: + # Wendi: Avoid returning None when the length is not enough + data_map[k] = np.concatenate([i for i in data_map[k] if i is not None], axis=0) + + # Shorten target so we only get decoder steps + data_map['outputs'] = data_map['outputs'][:, self.num_encoder_steps:, :] + + active_entries = np.ones_like(data_map['outputs']) + if 'active_entries' not in data_map: + data_map['active_entries'] = active_entries + else: + data_map['active_entries'].append(active_entries) + + return data_map + + def _get_active_locations(self, x): + """Formats sample weights for Keras training.""" + return (np.sum(x, axis=-1) > 0.0) * 1.0 + + def _build_base_graph(self): + """Returns graph defining layers of the TFT.""" + + # Size definitions. + time_steps = self.time_steps + combined_input_size = self.input_size + encoder_steps = self.num_encoder_steps + + # Inputs. + all_inputs = tf.keras.layers.Input( + shape=( + time_steps, + combined_input_size, + )) + + unknown_inputs, known_combined_layer, obs_inputs, static_inputs \ + = self.get_tft_embeddings(all_inputs) + + # Isolate known and observed historical inputs. + if unknown_inputs is not None: + historical_inputs = concat([ + unknown_inputs[:, :encoder_steps, :], + known_combined_layer[:, :encoder_steps, :], + obs_inputs[:, :encoder_steps, :] + ], + axis=-1) + else: + historical_inputs = concat([ + known_combined_layer[:, :encoder_steps, :], + obs_inputs[:, :encoder_steps, :] + ], + axis=-1) + + # Isolate only known future inputs. + future_inputs = known_combined_layer[:, encoder_steps:, :] + + def static_combine_and_mask(embedding): + """Applies variable selection network to static inputs. + + Args: + embedding: Transformed static inputs + + Returns: + Tensor output for variable selection network + """ + + # Add temporal features + _, num_static, _ = embedding.get_shape().as_list() + + flatten = tf.keras.layers.Flatten()(embedding) + + # Nonlinear transformation with gated residual network. + mlp_outputs = gated_residual_network( + flatten, + self.hidden_layer_size, + output_size=num_static, + dropout_rate=self.dropout_rate, + use_time_distributed=False, + additional_context=None) + + sparse_weights = tf.keras.layers.Activation('softmax')(mlp_outputs) + sparse_weights = K.expand_dims(sparse_weights, axis=-1) + + trans_emb_list = [] + for i in range(num_static): + e = gated_residual_network( + embedding[:, i:i + 1, :], + self.hidden_layer_size, + dropout_rate=self.dropout_rate, + use_time_distributed=False) + trans_emb_list.append(e) + + transformed_embedding = concat(trans_emb_list, axis=1) + + combined = tf.keras.layers.Multiply()( + [sparse_weights, transformed_embedding]) + + static_vec = K.sum(combined, axis=1) + + return static_vec, sparse_weights + + static_encoder, static_weights = static_combine_and_mask(static_inputs) + + static_context_variable_selection = gated_residual_network( + static_encoder, + self.hidden_layer_size, + dropout_rate=self.dropout_rate, + use_time_distributed=False) + static_context_enrichment = gated_residual_network( + static_encoder, + self.hidden_layer_size, + dropout_rate=self.dropout_rate, + use_time_distributed=False) + static_context_state_h = gated_residual_network( + static_encoder, + self.hidden_layer_size, + dropout_rate=self.dropout_rate, + use_time_distributed=False) + static_context_state_c = gated_residual_network( + static_encoder, + self.hidden_layer_size, + dropout_rate=self.dropout_rate, + use_time_distributed=False) + + def lstm_combine_and_mask(embedding): + """Apply temporal variable selection networks. + + Args: + embedding: Transformed inputs. + + Returns: + Processed tensor outputs. + """ + + # Add temporal features + _, time_steps, embedding_dim, num_inputs = embedding.get_shape().as_list() + + flatten = K.reshape(embedding, + [-1, time_steps, embedding_dim * num_inputs]) + + expanded_static_context = K.expand_dims( + static_context_variable_selection, axis=1) + + # Variable selection weights + mlp_outputs, static_gate = gated_residual_network( + flatten, + self.hidden_layer_size, + output_size=num_inputs, + dropout_rate=self.dropout_rate, + use_time_distributed=True, + additional_context=expanded_static_context, + return_gate=True) + + sparse_weights = tf.keras.layers.Activation('softmax')(mlp_outputs) + sparse_weights = tf.expand_dims(sparse_weights, axis=2) + + # Non-linear Processing & weight application + trans_emb_list = [] + for i in range(num_inputs): + grn_output = gated_residual_network( + embedding[Ellipsis, i], + self.hidden_layer_size, + dropout_rate=self.dropout_rate, + use_time_distributed=True) + trans_emb_list.append(grn_output) + + transformed_embedding = stack(trans_emb_list, axis=-1) + + combined = tf.keras.layers.Multiply()( + [sparse_weights, transformed_embedding]) + temporal_ctx = K.sum(combined, axis=-1) + + return temporal_ctx, sparse_weights, static_gate + + historical_features, historical_flags, _ = lstm_combine_and_mask( + historical_inputs) + future_features, future_flags, _ = lstm_combine_and_mask(future_inputs) + + # LSTM layer + def get_lstm(return_state): + """Returns LSTM cell initialized with default parameters.""" + if self.use_cudnn: + lstm = tf.keras.layers.CuDNNLSTM( + self.hidden_layer_size, + return_sequences=True, + return_state=return_state, + stateful=False, + ) + else: + lstm = tf.keras.layers.LSTM( + self.hidden_layer_size, + return_sequences=True, + return_state=return_state, + stateful=False, + # Additional params to ensure LSTM matches CuDNN, See TF 2.0 : + # (https://www.tensorflow.org/api_docs/python/tf/keras/layers/LSTM) + activation='tanh', + recurrent_activation='sigmoid', + recurrent_dropout=0, + unroll=False, + use_bias=True) + return lstm + + history_lstm, state_h, state_c \ + = get_lstm(return_state=True)(historical_features, + initial_state=[static_context_state_h, + static_context_state_c]) + + future_lstm = get_lstm(return_state=False)( + future_features, initial_state=[state_h, state_c]) + + lstm_layer = concat([history_lstm, future_lstm], axis=1) + + # Apply gated skip connection + input_embeddings = concat([historical_features, future_features], axis=1) + + lstm_layer, _ = apply_gating_layer( + lstm_layer, self.hidden_layer_size, self.dropout_rate, activation=None) + temporal_feature_layer = add_and_norm([lstm_layer, input_embeddings]) + + # Static enrichment layers + expanded_static_context = K.expand_dims(static_context_enrichment, axis=1) + enriched, _ = gated_residual_network( + temporal_feature_layer, + self.hidden_layer_size, + dropout_rate=self.dropout_rate, + use_time_distributed=True, + additional_context=expanded_static_context, + return_gate=True) + + # Decoder self attention + self_attn_layer = InterpretableMultiHeadAttention( + self.num_heads, self.hidden_layer_size, dropout=self.dropout_rate) + + mask = get_decoder_mask(enriched) + x, self_att \ + = self_attn_layer(enriched, enriched, enriched, + mask=mask) + + x, _ = apply_gating_layer( + x, + self.hidden_layer_size, + dropout_rate=self.dropout_rate, + activation=None) + x = add_and_norm([x, enriched]) + + # Nonlinear processing on outputs + decoder = gated_residual_network( + x, + self.hidden_layer_size, + dropout_rate=self.dropout_rate, + use_time_distributed=True) + + # Final skip connection + decoder, _ = apply_gating_layer( + decoder, self.hidden_layer_size, activation=None) + transformer_layer = add_and_norm([decoder, temporal_feature_layer]) + + # Attention components for explainability + attention_components = { + # Temporal attention weights + 'decoder_self_attn': self_att, + # Static variable selection weights + 'static_flags': static_weights[Ellipsis, 0], + # Variable selection weights of past inputs + 'historical_flags': historical_flags[Ellipsis, 0, :], + # Variable selection weights of future inputs + 'future_flags': future_flags[Ellipsis, 0, :] + } + + return transformer_layer, all_inputs, attention_components + + def build_model(self): + """Build model and defines training losses. + + Returns: + Fully defined Keras model. + """ + + with tf.variable_scope(self.name): + + transformer_layer, all_inputs, attention_components \ + = self._build_base_graph() + + outputs = tf.keras.layers.TimeDistributed( + tf.keras.layers.Dense(self.output_size * len(self.quantiles))) \ + (transformer_layer[Ellipsis, self.num_encoder_steps:, :]) + + self._attention_components = attention_components + + adam = tf.keras.optimizers.Adam( + lr=self.learning_rate, clipnorm=self.max_gradient_norm) + + model = tf.keras.Model(inputs=all_inputs, outputs=outputs) + + print(model.summary()) + + valid_quantiles = self.quantiles + output_size = self.output_size + + class QuantileLossCalculator(object): + """Computes the combined quantile loss for prespecified quantiles. + + Attributes: + quantiles: Quantiles to compute losses + """ + + def __init__(self, quantiles): + """Initializes computer with quantiles for loss calculations. + + Args: + quantiles: Quantiles to use for computations. + """ + self.quantiles = quantiles + + def quantile_loss(self, a, b): + """Returns quantile loss for specified quantiles. + + Args: + a: Targets + b: Predictions + """ + quantiles_used = set(self.quantiles) + + loss = 0. + for i, quantile in enumerate(valid_quantiles): + if quantile in quantiles_used: + loss += utils.tensorflow_quantile_loss( + a[Ellipsis, output_size * i:output_size * (i + 1)], + b[Ellipsis, output_size * i:output_size * (i + 1)], quantile) + return loss + + quantile_loss = QuantileLossCalculator(valid_quantiles).quantile_loss + + model.compile( + loss=quantile_loss, optimizer=adam, sample_weight_mode='temporal') + + self._input_placeholder = all_inputs + + return model + + def fit(self, train_df=None, valid_df=None): + """Fits deep neural network for given training and validation data. + + Args: + train_df: DataFrame for training data + valid_df: DataFrame for validation data + """ + + print('*** Fitting {} ***'.format(self.name)) + + # Add relevant callbacks + callbacks = [ + tf.keras.callbacks.EarlyStopping( + monitor='val_loss', + patience=self.early_stopping_patience, + min_delta=1e-4), + tf.keras.callbacks.ModelCheckpoint( + filepath=self.get_keras_saved_path(self._temp_folder), + monitor='val_loss', + save_best_only=True, + save_weights_only=True), + tf.keras.callbacks.TerminateOnNaN() + ] + + print('Getting batched_data') + if train_df is None: + print('Using cached training data') + train_data = TFTDataCache.get('train') + else: + train_data = self._batch_data(train_df) + + if valid_df is None: + print('Using cached validation data') + valid_data = TFTDataCache.get('valid') + else: + valid_data = self._batch_data(valid_df) + + print('Using keras standard fit') + + def _unpack(data): + return data['inputs'], data['outputs'], \ + self._get_active_locations(data['active_entries']) + + # Unpack without sample weights + data, labels, active_flags = _unpack(train_data) + val_data, val_labels, val_flags = _unpack(valid_data) + + all_callbacks = callbacks + + self.model.fit( + x=data, + y=np.concatenate([labels, labels, labels], axis=-1), + sample_weight=active_flags, + epochs=self.num_epochs, + batch_size=self.minibatch_size, + validation_data=(val_data, + np.concatenate([val_labels, val_labels, val_labels], + axis=-1), val_flags), + callbacks=all_callbacks, + shuffle=True, + use_multiprocessing=True, + workers=self.n_multiprocessing_workers) + + # Load best checkpoint again + tmp_checkpont = self.get_keras_saved_path(self._temp_folder) + if os.path.exists(tmp_checkpont): + self.load( + self._temp_folder, + use_keras_loadings=True) + + else: + print('Cannot load from {}, skipping ...'.format(self._temp_folder)) + + def evaluate(self, data=None, eval_metric='loss'): + """Applies evaluation metric to the training data. + + Args: + data: Dataframe for evaluation + eval_metric: Evaluation metic to return, based on model definition. + + Returns: + Computed evaluation loss. + """ + + if data is None: + print('Using cached validation data') + raw_data = TFTDataCache.get('valid') + else: + raw_data = self._batch_data(data) + + inputs = raw_data['inputs'] + outputs = raw_data['outputs'] + active_entries = self._get_active_locations(raw_data['active_entries']) + + metric_values = self.model.evaluate( + x=inputs, + y=np.concatenate([outputs, outputs, outputs], axis=-1), + sample_weight=active_entries, + workers=16, + use_multiprocessing=True) + + metrics = pd.Series(metric_values, self.model.metrics_names) + + return metrics[eval_metric] + + def predict(self, df, return_targets=False): + """Computes predictions for a given input dataset. + + Args: + df: Input dataframe + return_targets: Whether to also return outputs aligned with predictions to + faciliate evaluation + + Returns: + Input dataframe or tuple of (input dataframe, algined output dataframe). + """ + + data = self._batch_data(df) + + inputs = data['inputs'] + time = data['time'] + identifier = data['identifier'] + outputs = data['outputs'] + + combined = self.model.predict( + inputs, + workers=16, + use_multiprocessing=True, + batch_size=self.minibatch_size) + + # Format output_csv + if self.output_size != 1: + raise NotImplementedError('Current version only supports 1D targets!') + + def format_outputs(prediction): + """Returns formatted dataframes for prediction.""" + + flat_prediction = pd.DataFrame( + prediction[:, :, 0], + columns=[ + 't+{}'.format(i) + for i in range(self.time_steps - self.num_encoder_steps) + ]) + cols = list(flat_prediction.columns) + flat_prediction['forecast_time'] = time[:, self.num_encoder_steps - 1, 0] + flat_prediction['identifier'] = identifier[:, 0, 0] + + # Arrange in order + return flat_prediction[['forecast_time', 'identifier'] + cols] + + # Extract predictions for each quantile into different entries + process_map = { + 'p{}'.format(int(q * 100)): + combined[Ellipsis, i * self.output_size:(i + 1) * self.output_size] + for i, q in enumerate(self.quantiles) + } + + if return_targets: + # Add targets if relevant + process_map['targets'] = outputs + + return {k: format_outputs(process_map[k]) for k in process_map} + + def get_attention(self, df): + """Computes TFT attention weights for a given dataset. + + Args: + df: Input dataframe + + Returns: + Dictionary of numpy arrays for temporal attention weights and variable + selection weights, along with their identifiers and time indices + """ + + data = self._batch_data(df) + inputs = data['inputs'] + identifiers = data['identifier'] + time = data['time'] + + def get_batch_attention_weights(input_batch): + """Returns weights for a given minibatch of data.""" + input_placeholder = self._input_placeholder + attention_weights = {} + for k in self._attention_components: + attention_weight = tf.keras.backend.get_session().run( + self._attention_components[k], + {input_placeholder: input_batch.astype(np.float32)}) + attention_weights[k] = attention_weight + return attention_weights + + # Compute number of batches + batch_size = self.minibatch_size + n = inputs.shape[0] + num_batches = n // batch_size + if n - (num_batches * batch_size) > 0: + num_batches += 1 + + # Split up inputs into batches + batched_inputs = [ + inputs[i * batch_size:(i + 1) * batch_size, Ellipsis] + for i in range(num_batches) + ] + + # Get attention weights, while avoiding large memory increases + attention_by_batch = [ + get_batch_attention_weights(batch) for batch in batched_inputs + ] + attention_weights = {} + for k in self._attention_components: + attention_weights[k] = [] + for batch_weights in attention_by_batch: + attention_weights[k].append(batch_weights[k]) + + if len(attention_weights[k][0].shape) == 4: + tmp = np.concatenate(attention_weights[k], axis=1) + else: + tmp = np.concatenate(attention_weights[k], axis=0) + + del attention_weights[k] + gc.collect() + attention_weights[k] = tmp + + attention_weights['identifiers'] = identifiers[:, 0, 0] + attention_weights['time'] = time[:, :, 0] + + return attention_weights + + # Serialisation. + def reset_temp_folder(self): + """Deletes and recreates folder with temporary Keras training outputs.""" + print('Resetting temp folder...') + utils.create_folder_if_not_exist(self._temp_folder) + shutil.rmtree(self._temp_folder) + os.makedirs(self._temp_folder) + + def get_keras_saved_path(self, model_folder): + """Returns path to keras checkpoint.""" + return os.path.join(model_folder, '{}.check'.format(self.name)) + + def save(self, model_folder): + """Saves optimal TFT weights. + + Args: + model_folder: Location to serialze model. + """ + # Allows for direct serialisation of tensorflow variables to avoid spurious + # issue with Keras that leads to different performance evaluation results + # when model is reloaded (https://github.com/keras-team/keras/issues/4875). + + utils.save( + tf.keras.backend.get_session(), + model_folder, + cp_name=self.name, + scope=self.name) + + def load(self, model_folder, use_keras_loadings=False): + """Loads TFT weights. + + Args: + model_folder: Folder containing serialized models. + use_keras_loadings: Whether to load from Keras checkpoint. + + Returns: + + """ + if use_keras_loadings: + # Loads temporary Keras model saved during training. + serialisation_path = self.get_keras_saved_path(model_folder) + print('Loading model from {}'.format(serialisation_path)) + self.model.load_weights(serialisation_path) + else: + # Loads tensorflow graph for optimal models. + utils.load( + tf.keras.backend.get_session(), + model_folder, + cp_name=self.name, + scope=self.name) + + @classmethod + def get_hyperparm_choices(cls): + """Returns hyperparameter ranges for random search.""" + return { + 'dropout_rate': [0.1, 0.2, 0.3, 0.4, 0.5, 0.7, 0.9], + 'hidden_layer_size': [10, 20, 40, 80, 160, 240, 320], + 'minibatch_size': [64, 128, 256], + 'learning_rate': [1e-4, 1e-3, 1e-2], + 'max_gradient_norm': [0.01, 1.0, 100.0], + 'num_heads': [1, 4], + 'stack_size': [1], + } diff --git a/examples/benchmarks/TFT/libs/utils.py b/examples/benchmarks/TFT/libs/utils.py new file mode 100644 index 000000000..813d4b176 --- /dev/null +++ b/examples/benchmarks/TFT/libs/utils.py @@ -0,0 +1,236 @@ +# coding=utf-8 +# Copyright 2020 The Google Research Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Lint as: python3 +"""Generic helper functions used across codebase.""" + +import os +import pathlib + +import numpy as np +import tensorflow as tf +from tensorflow.python.tools.inspect_checkpoint import print_tensors_in_checkpoint_file + + +# Generic. +def get_single_col_by_input_type(input_type, column_definition): + """Returns name of single column. + + Args: + input_type: Input type of column to extract + column_definition: Column definition list for experiment + """ + + l = [tup[0] for tup in column_definition if tup[2] == input_type] + + if len(l) != 1: + raise ValueError('Invalid number of columns for {}'.format(input_type)) + + return l[0] + + +def extract_cols_from_data_type(data_type, column_definition, + excluded_input_types): + """Extracts the names of columns that correspond to a define data_type. + + Args: + data_type: DataType of columns to extract. + column_definition: Column definition to use. + excluded_input_types: Set of input types to exclude + + Returns: + List of names for columns with data type specified. + """ + return [ + tup[0] + for tup in column_definition + if tup[1] == data_type and tup[2] not in excluded_input_types + ] + + +# Loss functions. +def tensorflow_quantile_loss(y, y_pred, quantile): + """Computes quantile loss for tensorflow. + + Standard quantile loss as defined in the "Training Procedure" section of + the main TFT paper + + Args: + y: Targets + y_pred: Predictions + quantile: Quantile to use for loss calculations (between 0 & 1) + + Returns: + Tensor for quantile loss. + """ + + # Checks quantile + if quantile < 0 or quantile > 1: + raise ValueError( + 'Illegal quantile value={}! Values should be between 0 and 1.'.format( + quantile)) + + prediction_underflow = y - y_pred + q_loss = quantile * tf.maximum(prediction_underflow, 0.) + ( + 1. - quantile) * tf.maximum(-prediction_underflow, 0.) + + return tf.reduce_sum(q_loss, axis=-1) + + +def numpy_normalised_quantile_loss(y, y_pred, quantile): + """Computes normalised quantile loss for numpy arrays. + + Uses the q-Risk metric as defined in the "Training Procedure" section of the + main TFT paper. + + Args: + y: Targets + y_pred: Predictions + quantile: Quantile to use for loss calculations (between 0 & 1) + + Returns: + Float for normalised quantile loss. + """ + prediction_underflow = y - y_pred + weighted_errors = quantile * np.maximum(prediction_underflow, 0.) \ + + (1. - quantile) * np.maximum(-prediction_underflow, 0.) + + quantile_loss = weighted_errors.mean() + normaliser = y.abs().mean() + + return 2 * quantile_loss / normaliser + + +# OS related functions. +def create_folder_if_not_exist(directory): + """Creates folder if it doesn't exist. + + Args: + directory: Folder path to create. + """ + # Also creates directories recursively + pathlib.Path(directory).mkdir(parents=True, exist_ok=True) + + +# Tensorflow related functions. +def get_default_tensorflow_config(tf_device='gpu', gpu_id=0): + """Creates tensorflow config for graphs to run on CPU or GPU. + + Specifies whether to run graph on gpu or cpu and which GPU ID to use for multi + GPU machines. + + Args: + tf_device: 'cpu' or 'gpu' + gpu_id: GPU ID to use if relevant + + Returns: + Tensorflow config. + """ + + if tf_device == 'cpu': + os.environ['CUDA_VISIBLE_DEVICES'] = '-1' # for training on cpu + tf_config = tf.ConfigProto( + log_device_placement=False, device_count={'GPU': 0}) + + else: + os.environ['CUDA_DEVICE_ORDER'] = 'PCI_BUS_ID' + os.environ['CUDA_VISIBLE_DEVICES'] = str(gpu_id) + + print('Selecting GPU ID={}'.format(gpu_id)) + + tf_config = tf.ConfigProto(log_device_placement=False) + tf_config.gpu_options.allow_growth = True + + return tf_config + + +def save(tf_session, model_folder, cp_name, scope=None): + """Saves Tensorflow graph to checkpoint. + + Saves all trainiable variables under a given variable scope to checkpoint. + + Args: + tf_session: Session containing graph + model_folder: Folder to save models + cp_name: Name of Tensorflow checkpoint + scope: Variable scope containing variables to save + """ + # Save model + if scope is None: + saver = tf.train.Saver() + else: + var_list = tf.get_collection(tf.GraphKeys.TRAINABLE_VARIABLES, scope=scope) + saver = tf.train.Saver(var_list=var_list, max_to_keep=100000) + + save_path = saver.save(tf_session, + os.path.join(model_folder, '{0}.ckpt'.format(cp_name))) + print('Model saved to: {0}'.format(save_path)) + + +def load(tf_session, model_folder, cp_name, scope=None, verbose=False): + """Loads Tensorflow graph from checkpoint. + + Args: + tf_session: Session to load graph into + model_folder: Folder containing serialised model + cp_name: Name of Tensorflow checkpoint + scope: Variable scope to use. + verbose: Whether to print additional debugging information. + """ + # Load model proper + load_path = os.path.join(model_folder, '{0}.ckpt'.format(cp_name)) + + print('Loading model from {0}'.format(load_path)) + + print_weights_in_checkpoint(model_folder, cp_name) + + initial_vars = set( + [v.name for v in tf.get_default_graph().as_graph_def().node]) + + # Saver + if scope is None: + saver = tf.train.Saver() + else: + var_list = tf.get_collection(tf.GraphKeys.GLOBAL_VARIABLES, scope=scope) + saver = tf.train.Saver(var_list=var_list, max_to_keep=100000) + # Load + saver.restore(tf_session, load_path) + all_vars = set([v.name for v in tf.get_default_graph().as_graph_def().node]) + + if verbose: + print('Restored {0}'.format(','.join(initial_vars.difference(all_vars)))) + print('Existing {0}'.format(','.join(all_vars.difference(initial_vars)))) + print('All {0}'.format(','.join(all_vars))) + + print('Done.') + + +def print_weights_in_checkpoint(model_folder, cp_name): + """Prints all weights in Tensorflow checkpoint. + + Args: + model_folder: Folder containing checkpoint + cp_name: Name of checkpoint + + Returns: + + """ + load_path = os.path.join(model_folder, '{0}.ckpt'.format(cp_name)) + + print_tensors_in_checkpoint_file( + file_name=load_path, + tensor_name='', + all_tensors=True, + all_tensor_names=True) diff --git a/examples/benchmarks/TFT/tft.py b/examples/benchmarks/TFT/tft.py new file mode 100644 index 000000000..ee49a1eb7 --- /dev/null +++ b/examples/benchmarks/TFT/tft.py @@ -0,0 +1,246 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +import numpy as np +import pandas as pd +import tensorflow.compat.v1 as tf +import data_formatters.base +import expt_settings.configs +import libs.hyperparam_opt +import libs.tft_model +import libs.utils as utils +import os +import datetime as dte + + +from qlib.model.base import ModelFT +from qlib.data.dataset import DatasetH +from qlib.data.dataset.handler import DataHandlerLP + + + +# To register new datasets, please add them here. +ALLOW_DATASET = ['Alpha158'] +DATASET_SETTING = { + 'Alpha158': { + 'feature_col': ['RESI5', 'WVMA5', 'RSQR5', 'KLEN', 'RSQR10', 'CORR5', 'CORD5', 'CORR10', 'ROC60', 'RESI10'], + 'label_col': ['LABEL0'], + }, +} +# To register new datasets, please add their configurations here. + +def get_shifted_label(data_df, shifts=5, col_shift='LABEL0'): + return data_df[[col_shift]].groupby('instrument').apply(lambda df: df.shift(shifts)) + +def fill_test_na(test_df): + test_df_res = test_df.copy() + feature_cols = ~test_df_res.columns.str.contains('label', case=False) + test_feature_fna = test_df_res.loc[:, feature_cols].groupby('datetime').apply(lambda df: df.fillna(df.mean())) + test_df_res.loc[:, feature_cols] = test_feature_fna + return test_df_res + +def process_qlib_data(df, dataset, fillna=False): + """Prepare data to fit the TFT model. + + Args: + df: Original DataFrame. + fillna: Whether to fill the data with the mean values. + + Returns: + Transformed DataFrame. + + """ + # Several features selected manually + feature_col = DATASET_SETTING[dataset]['feature_col'] + label_col = DATASET_SETTING[dataset]['label_col'] + temp_df = df.loc[:, feature_col+label_col] + if fillna: + temp_df = fill_test_na(temp_df) + temp_df = temp_df.swaplevel() + temp_df = temp_df.sort_index() + temp_df = temp_df.reset_index(level=0) + dates = pd.to_datetime(temp_df.index) + temp_df['date'] = dates + temp_df['day_of_week'] = dates.dayofweek + temp_df['month'] = dates.month + temp_df['year'] = dates.year + temp_df['const'] = 1.0 + return temp_df + +def process_predicted(df, col_name): + """Transform the TFT predicted data into Qlib format. + + Args: + df: Original DataFrame. + fillna: New column name. + + Returns: + Transformed DataFrame. + + """ + df_res = df.copy() + df_res = df_res.rename(columns={"forecast_time": "datetime", "identifier": "instrument", "t+0": col_name}) + df_res = df_res.set_index(['datetime','instrument']).sort_index() + df_res = df_res[[col_name]] + return df_res + +def format_score(forecast_df, col_name='pred', label_shift=5): + pred = process_predicted(forecast_df, col_name=col_name) + pred = get_shifted_label(pred, shifts=-label_shift, col_shift=col_name) + pred = pred.dropna()[col_name] + return pred + +def transform_df(df, col_name='LABEL0'): + df_res = df['feature'] + df_res[col_name] = df['label'] + return df_res + +class TFTModel(ModelFT): + """TFT Model""" + + def __init__(self, **kwargs): + self.model = None + + def _prepare_data(self, dataset: DatasetH): + df_train, df_valid = dataset.prepare( + ["train", "valid"], col_set=["feature", "label"], data_key=DataHandlerLP.DK_L + ) + return transform_df(df_train), transform_df(df_valid) + + def fit( + self, + dataset: DatasetH, + DATASET = 'Alpha158', + MODEL_FOLDER = 'qlib_alpha158_model', + LABEL_COL = 'LABEL0', + LABEL_SHIFT = 5, + USE_GPU_ID = 0, + **kwargs + ): + + if DATASET not in ALLOW_DATASET: + raise AssertionError("The dataset is not supported, please make a new formatter to fit this dataset") + + dtrain, dvalid = self._prepare_data(dataset) + dtrain.loc[:, LABEL_COL] = get_shifted_label(dtrain, shifts=LABEL_SHIFT, col_shift=LABEL_COL) + dvalid.loc[:, LABEL_COL] = get_shifted_label(dvalid, shifts=LABEL_SHIFT, col_shift=LABEL_COL) + + + train = process_qlib_data(dtrain, DATASET, fillna=True).dropna() + valid = process_qlib_data(dvalid, DATASET, fillna=True).dropna() + + ExperimentConfig = expt_settings.configs.ExperimentConfig + config = ExperimentConfig(DATASET) + self.data_formatter = config.make_data_formatter() + self.model_folder = MODEL_FOLDER + self.gpu_id = USE_GPU_ID + self.label_shift = LABEL_SHIFT + self.expt_name = DATASET + self.label_col = LABEL_COL + + use_gpu = (True, self.gpu_id) + #===========================Training Process=========================== + ModelClass = libs.tft_model.TemporalFusionTransformer + if not isinstance(self.data_formatter, data_formatters.base.GenericDataFormatter): + raise ValueError( + "Data formatters should inherit from" + + "AbstractDataFormatter! Type={}".format(type(self.data_formatter))) + + default_keras_session = tf.keras.backend.get_session() + + if use_gpu[0]: + self.tf_config = utils.get_default_tensorflow_config(tf_device="gpu", gpu_id=use_gpu[1]) + else: + self.tf_config = utils.get_default_tensorflow_config(tf_device="cpu") + + self.data_formatter.set_scalers(train) + + # Sets up default params + fixed_params = self.data_formatter.get_experiment_params() + params = self.data_formatter.get_default_model_params() + + # Wendi: 合并调优的参数和非调优的参数 + params = {**params, **fixed_params} + + if not os.path.exists(self.model_folder): + os.makedirs(self.model_folder) + params['model_folder'] = self.model_folder + + print("*** Begin training ***") + best_loss = np.Inf + + tf.reset_default_graph() + + self.tf_graph = tf.Graph() + with self.tf_graph.as_default(): + self.sess = tf.Session(config=self.tf_config) + tf.keras.backend.set_session(self.sess) + self.model = ModelClass(params, use_cudnn=use_gpu[0]) + self.sess.run(tf.global_variables_initializer()) + self.model.fit(train_df=train, valid_df=valid) + print("*** Finished training ***") + saved_model_dir = self.model_folder+'/'+'saved_model' + if not os.path.exists(saved_model_dir): + os.makedirs(saved_model_dir) + self.model.save(saved_model_dir) + + def extract_numerical_data(data): + """Strips out forecast time and identifier columns.""" + return data[[ + col for col in data.columns + if col not in {"forecast_time", "identifier"} + ]] + + #p50_loss = utils.numpy_normalised_quantile_loss( + # extract_numerical_data(targets), extract_numerical_data(p50_forecast), + # 0.5) + #p90_loss = utils.numpy_normalised_quantile_loss( + # extract_numerical_data(targets), extract_numerical_data(p90_forecast), + # 0.9) + tf.keras.backend.set_session(default_keras_session) + print("Training completed.".format(dte.datetime.now())) + #===========================Training Process=========================== + + def predict(self, dataset): + if self.model is None: + raise ValueError("model is not fitted yet!") + d_test = dataset.prepare("test", col_set=["feature", "label"]) + d_test = transform_df(d_test) + d_test.loc[:, self.label_col] = get_shifted_label(d_test, shifts=self.label_shift, col_shift=self.label_col) + test = process_qlib_data(d_test, self.expt_name, fillna=True).dropna() + + use_gpu = (True, self.gpu_id) + #===========================Predicting Process=========================== + default_keras_session = tf.keras.backend.get_session() + + # Sets up default params + fixed_params = self.data_formatter.get_experiment_params() + params = self.data_formatter.get_default_model_params() + params = {**params, **fixed_params} + + + print("*** Begin predicting ***") + tf.reset_default_graph() + + with self.tf_graph.as_default(): + tf.keras.backend.set_session(self.sess) + output_map = self.model.predict(test, return_targets=True) + targets = self.data_formatter.format_predictions(output_map["targets"]) + p50_forecast = self.data_formatter.format_predictions(output_map["p50"]) + p90_forecast = self.data_formatter.format_predictions(output_map["p90"]) + tf.keras.backend.set_session(default_keras_session) + + predict = format_score(p90_forecast, 'pred', self.label_shift) + label = format_score(targets, 'label', self.label_shift) + #===========================Predicting Process=========================== + return predict, label + + def finetune(self, dataset: DatasetH): + """ + finetune model + Parameters + ---------- + dataset : DatasetH + dataset for finetuning + """ + pass diff --git a/examples/benchmarks/TFT/workflow_by_code_tft.py b/examples/benchmarks/TFT/workflow_by_code_tft.py new file mode 100644 index 000000000..593ac468f --- /dev/null +++ b/examples/benchmarks/TFT/workflow_by_code_tft.py @@ -0,0 +1,130 @@ + #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.pytorch_lstm import LSTM +from qlib.contrib.data.handler import ALPHA360_Denoise +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 + +# from qlib.model.learner import train_model +from qlib.utils import init_instance_by_config + +import pickle +from tft import TFTModel + +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_cn(target_dir=provider_uri) + + qlib.init(provider_uri=provider_uri, region=REG_CN) + + MARKET = "csi300" + BENCHMARK = "SH000300" + + ################################### + # train model + ################################### + DATA_HANDLER_CONFIG = { + "start_time": "2008-01-01", + "end_time": "2020-08-01", + "fit_start_time": "2008-01-01", + "fit_end_time": "2014-12-31", + "instruments": MARKET, + } + + TRAINER_CONFIG = { + "train_start_time": "2008-01-01", + "train_end_time": "2014-12-31", + "validate_start_time": "2015-01-01", + "validate_end_time": "2016-12-31", + "test_start_time": "2017-01-01", + "test_end_time": "2020-08-01", + } + + task = { + "dataset": { + "class": "DatasetH", + "module_path": "qlib.data.dataset", + "kwargs": { + 'handler': { + "class": "Alpha158", + "module_path": "qlib.contrib.data.handler", + "kwargs": DATA_HANDLER_CONFIG + }, + 'segments': { + 'train': ("2008-01-01", "2014-12-31"), + 'valid': ("2015-01-01", "2016-12-31",), + 'test': ("2017-01-01", "2020-08-01",), + } + } + } + # You shoud record the data in specific sequence + # "record": ['SignalRecord', 'SigAnaRecord', 'PortAnaRecord'], + } + + + model = TFTModel() + dataset = init_instance_by_config(task["dataset"]) + model.fit(dataset) + + pred_score, label_score = model.predict(dataset) + + # save pred_score to file + pred_score_path = Path("~/tmp/qlib/pred_score.pkl").expanduser() + pred_score_path.parent.mkdir(exist_ok=True, parents=True) + pred_score.to_pickle(pred_score_path) + + + ################################### + # backtest + ################################### + STRATEGY_CONFIG = { + "topk": 50, + "n_drop": 5, + } + BACKTEST_CONFIG = { + "verbose": False, + "limit_threshold": 0.095, + "account": 100000000, + "benchmark": BENCHMARK, + "deal_price": "close", + "open_cost": 0.0005, + "close_cost": 0.0015, + "min_cost": 5, + } + + # use default strategy + # custom Strategy, refer to: TODO: Strategy API url + strategy = TopkDropoutStrategy(**STRATEGY_CONFIG) + report_normal, positions_normal = normal_backtest(pred_score, strategy=strategy, **BACKTEST_CONFIG) + + ################################### + # analyze + # If need a more detailed analysis, refer to: examples/train_and_bakctest.ipynb + ################################### + 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 + print(analysis_df) + + From c2c96a817f3ccd59f100c7a9f6c4dba8a3f2959e Mon Sep 17 00:00:00 2001 From: Wendi Li Date: Mon, 23 Nov 2020 16:09:03 +0800 Subject: [PATCH 124/241] Format TFT --- .../TFT/data_formatters/__init__.py | 29 +- .../benchmarks/TFT/data_formatters/base.py | 458 ++- .../TFT/data_formatters/electricity.py | 515 ++-- .../TFT/data_formatters/favorita.py | 660 ++-- .../TFT/data_formatters/qlib_Alpha158.py | 439 ++- .../benchmarks/TFT/data_formatters/traffic.py | 234 +- .../TFT/data_formatters/volatility.py | 426 ++- .../benchmarks/TFT/expt_settings/__init__.py | 29 +- .../benchmarks/TFT/expt_settings/configs.py | 218 +- examples/benchmarks/TFT/libs/__init__.py | 29 +- .../benchmarks/TFT/libs/hyperparam_opt.py | 868 +++--- examples/benchmarks/TFT/libs/tft_model.py | 2671 ++++++++--------- examples/benchmarks/TFT/libs/utils.py | 460 ++- examples/benchmarks/TFT/tft.py | 494 +-- .../benchmarks/TFT/workflow_by_code_tft.py | 262 +- 15 files changed, 3821 insertions(+), 3971 deletions(-) diff --git a/examples/benchmarks/TFT/data_formatters/__init__.py b/examples/benchmarks/TFT/data_formatters/__init__.py index 9a1980462..87ec3284f 100644 --- a/examples/benchmarks/TFT/data_formatters/__init__.py +++ b/examples/benchmarks/TFT/data_formatters/__init__.py @@ -1,15 +1,14 @@ -# coding=utf-8 -# Copyright 2020 The Google Research Authors. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - +# coding=utf-8 +# Copyright 2020 The Google Research Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/examples/benchmarks/TFT/data_formatters/base.py b/examples/benchmarks/TFT/data_formatters/base.py index f4ce2764f..c68a192ba 100644 --- a/examples/benchmarks/TFT/data_formatters/base.py +++ b/examples/benchmarks/TFT/data_formatters/base.py @@ -1,235 +1,223 @@ -# coding=utf-8 -# Copyright 2020 The Google Research Authors. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# Lint as: python3 -"""Default data formatting functions for experiments. - -For new datasets, inherit form GenericDataFormatter and implement -all abstract functions. - -These dataset-specific methods: -1) Define the column and input types for tabular dataframes used by model -2) Perform the necessary input feature engineering & normalisation steps -3) Reverts the normalisation for predictions -4) Are responsible for train, validation and test splits - - -""" - -import abc -import enum - - -# Type defintions -class DataTypes(enum.IntEnum): - """Defines numerical types of each column.""" - REAL_VALUED = 0 - CATEGORICAL = 1 - DATE = 2 - - -class InputTypes(enum.IntEnum): - """Defines input types of each column.""" - TARGET = 0 - OBSERVED_INPUT = 1 - KNOWN_INPUT = 2 - STATIC_INPUT = 3 - ID = 4 # Single column used as an entity identifier - TIME = 5 # Single column exclusively used as a time index - - -class GenericDataFormatter(abc.ABC): - """Abstract base class for all data formatters. - - User can implement the abstract methods below to perform dataset-specific - manipulations. - - """ - - @abc.abstractmethod - def set_scalers(self, df): - """Calibrates scalers using the data supplied.""" - raise NotImplementedError() - - @abc.abstractmethod - def transform_inputs(self, df): - """Performs feature transformation.""" - raise NotImplementedError() - - @abc.abstractmethod - def format_predictions(self, df): - """Reverts any normalisation to give predictions in original scale.""" - raise NotImplementedError() - - @abc.abstractmethod - def split_data(self, df): - """Performs the default train, validation and test splits.""" - raise NotImplementedError() - - @property - @abc.abstractmethod - def _column_definition(self): - """Defines order, input type and data type of each column.""" - raise NotImplementedError() - - @abc.abstractmethod - def get_fixed_params(self): - """Defines the fixed parameters used by the model for training. - - Requires the following keys: - 'total_time_steps': Defines the total number of time steps used by TFT - 'num_encoder_steps': Determines length of LSTM encoder (i.e. history) - 'num_epochs': Maximum number of epochs for training - 'early_stopping_patience': Early stopping param for keras - 'multiprocessing_workers': # of cpus for data processing - - - Returns: - A dictionary of fixed parameters, e.g.: - - fixed_params = { - 'total_time_steps': 252 + 5, - 'num_encoder_steps': 252, - 'num_epochs': 100, - 'early_stopping_patience': 5, - 'multiprocessing_workers': 5, - } - """ - raise NotImplementedError - - # Shared functions across data-formatters - @property - def num_classes_per_cat_input(self): - """Returns number of categories per relevant input. - - This is seqeuently required for keras embedding layers. - """ - return self._num_classes_per_cat_input - - def get_num_samples_for_calibration(self): - """Gets the default number of training and validation samples. - - Use to sub-sample the data for network calibration and a value of -1 uses - all available samples. - - Returns: - Tuple of (training samples, validation samples) - """ - return -1, -1 - - def get_column_definition(self): - """"Returns formatted column definition in order expected by the TFT.""" - - column_definition = self._column_definition - - # Sanity checks first. - # Ensure only one ID and time column exist - def _check_single_column(input_type): - - length = len([tup for tup in column_definition if tup[2] == input_type]) - - if length != 1: - raise ValueError('Illegal number of inputs ({}) of type {}'.format( - length, input_type)) - - _check_single_column(InputTypes.ID) - _check_single_column(InputTypes.TIME) - - identifier = [tup for tup in column_definition if tup[2] == InputTypes.ID] - time = [tup for tup in column_definition if tup[2] == InputTypes.TIME] - real_inputs = [ - tup for tup in column_definition if tup[1] == DataTypes.REAL_VALUED and - tup[2] not in {InputTypes.ID, InputTypes.TIME} - ] - categorical_inputs = [ - tup for tup in column_definition if tup[1] == DataTypes.CATEGORICAL and - tup[2] not in {InputTypes.ID, InputTypes.TIME} - ] - - return identifier + time + real_inputs + categorical_inputs - - def _get_input_columns(self): - """Returns names of all input columns.""" - return [ - tup[0] - for tup in self.get_column_definition() - if tup[2] not in {InputTypes.ID, InputTypes.TIME} - ] - - def _get_tft_input_indices(self): - """Returns the relevant indexes and input sizes required by TFT.""" - - # Functions - def _extract_tuples_from_data_type(data_type, defn): - return [ - tup for tup in defn if tup[1] == data_type and - tup[2] not in {InputTypes.ID, InputTypes.TIME} - ] - - def _get_locations(input_types, defn): - return [i for i, tup in enumerate(defn) if tup[2] in input_types] - - # Start extraction - column_definition = [ - tup for tup in self.get_column_definition() - if tup[2] not in {InputTypes.ID, InputTypes.TIME} - ] - - categorical_inputs = _extract_tuples_from_data_type(DataTypes.CATEGORICAL, - column_definition) - real_inputs = _extract_tuples_from_data_type(DataTypes.REAL_VALUED, - column_definition) - - locations = { - 'input_size': - len(self._get_input_columns()), - 'output_size': - len(_get_locations({InputTypes.TARGET}, column_definition)), - 'category_counts': - self.num_classes_per_cat_input, - 'input_obs_loc': - _get_locations({InputTypes.TARGET}, column_definition), - 'static_input_loc': - _get_locations({InputTypes.STATIC_INPUT}, column_definition), - 'known_regular_inputs': - _get_locations({InputTypes.STATIC_INPUT, InputTypes.KNOWN_INPUT}, - real_inputs), - 'known_categorical_inputs': - _get_locations({InputTypes.STATIC_INPUT, InputTypes.KNOWN_INPUT}, - categorical_inputs), - } - - return locations - - def get_experiment_params(self): - """Returns fixed model parameters for experiments.""" - - required_keys = [ - 'total_time_steps', 'num_encoder_steps', 'num_epochs', - 'early_stopping_patience', 'multiprocessing_workers' - ] - - fixed_params = self.get_fixed_params() - - for k in required_keys: - if k not in fixed_params: - raise ValueError('Field {}'.format(k) + - ' missing from fixed parameter definitions!') - - fixed_params['column_definition'] = self.get_column_definition() - - fixed_params.update(self._get_tft_input_indices()) - - return fixed_params +# coding=utf-8 +# Copyright 2020 The Google Research Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Lint as: python3 +"""Default data formatting functions for experiments. + +For new datasets, inherit form GenericDataFormatter and implement +all abstract functions. + +These dataset-specific methods: +1) Define the column and input types for tabular dataframes used by model +2) Perform the necessary input feature engineering & normalisation steps +3) Reverts the normalisation for predictions +4) Are responsible for train, validation and test splits + + +""" + +import abc +import enum + + +# Type defintions +class DataTypes(enum.IntEnum): + """Defines numerical types of each column.""" + + REAL_VALUED = 0 + CATEGORICAL = 1 + DATE = 2 + + +class InputTypes(enum.IntEnum): + """Defines input types of each column.""" + + TARGET = 0 + OBSERVED_INPUT = 1 + KNOWN_INPUT = 2 + STATIC_INPUT = 3 + ID = 4 # Single column used as an entity identifier + TIME = 5 # Single column exclusively used as a time index + + +class GenericDataFormatter(abc.ABC): + """Abstract base class for all data formatters. + + User can implement the abstract methods below to perform dataset-specific + manipulations. + + """ + + @abc.abstractmethod + def set_scalers(self, df): + """Calibrates scalers using the data supplied.""" + raise NotImplementedError() + + @abc.abstractmethod + def transform_inputs(self, df): + """Performs feature transformation.""" + raise NotImplementedError() + + @abc.abstractmethod + def format_predictions(self, df): + """Reverts any normalisation to give predictions in original scale.""" + raise NotImplementedError() + + @abc.abstractmethod + def split_data(self, df): + """Performs the default train, validation and test splits.""" + raise NotImplementedError() + + @property + @abc.abstractmethod + def _column_definition(self): + """Defines order, input type and data type of each column.""" + raise NotImplementedError() + + @abc.abstractmethod + def get_fixed_params(self): + """Defines the fixed parameters used by the model for training. + + Requires the following keys: + 'total_time_steps': Defines the total number of time steps used by TFT + 'num_encoder_steps': Determines length of LSTM encoder (i.e. history) + 'num_epochs': Maximum number of epochs for training + 'early_stopping_patience': Early stopping param for keras + 'multiprocessing_workers': # of cpus for data processing + + + Returns: + A dictionary of fixed parameters, e.g.: + + fixed_params = { + 'total_time_steps': 252 + 5, + 'num_encoder_steps': 252, + 'num_epochs': 100, + 'early_stopping_patience': 5, + 'multiprocessing_workers': 5, + } + """ + raise NotImplementedError + + # Shared functions across data-formatters + @property + def num_classes_per_cat_input(self): + """Returns number of categories per relevant input. + + This is seqeuently required for keras embedding layers. + """ + return self._num_classes_per_cat_input + + def get_num_samples_for_calibration(self): + """Gets the default number of training and validation samples. + + Use to sub-sample the data for network calibration and a value of -1 uses + all available samples. + + Returns: + Tuple of (training samples, validation samples) + """ + return -1, -1 + + def get_column_definition(self): + """"Returns formatted column definition in order expected by the TFT.""" + + column_definition = self._column_definition + + # Sanity checks first. + # Ensure only one ID and time column exist + def _check_single_column(input_type): + + length = len([tup for tup in column_definition if tup[2] == input_type]) + + if length != 1: + raise ValueError("Illegal number of inputs ({}) of type {}".format(length, input_type)) + + _check_single_column(InputTypes.ID) + _check_single_column(InputTypes.TIME) + + identifier = [tup for tup in column_definition if tup[2] == InputTypes.ID] + time = [tup for tup in column_definition if tup[2] == InputTypes.TIME] + real_inputs = [ + tup + for tup in column_definition + if tup[1] == DataTypes.REAL_VALUED and tup[2] not in {InputTypes.ID, InputTypes.TIME} + ] + categorical_inputs = [ + tup + for tup in column_definition + if tup[1] == DataTypes.CATEGORICAL and tup[2] not in {InputTypes.ID, InputTypes.TIME} + ] + + return identifier + time + real_inputs + categorical_inputs + + def _get_input_columns(self): + """Returns names of all input columns.""" + return [tup[0] for tup in self.get_column_definition() if tup[2] not in {InputTypes.ID, InputTypes.TIME}] + + def _get_tft_input_indices(self): + """Returns the relevant indexes and input sizes required by TFT.""" + + # Functions + def _extract_tuples_from_data_type(data_type, defn): + return [tup for tup in defn if tup[1] == data_type and tup[2] not in {InputTypes.ID, InputTypes.TIME}] + + def _get_locations(input_types, defn): + return [i for i, tup in enumerate(defn) if tup[2] in input_types] + + # Start extraction + column_definition = [ + tup for tup in self.get_column_definition() if tup[2] not in {InputTypes.ID, InputTypes.TIME} + ] + + categorical_inputs = _extract_tuples_from_data_type(DataTypes.CATEGORICAL, column_definition) + real_inputs = _extract_tuples_from_data_type(DataTypes.REAL_VALUED, column_definition) + + locations = { + "input_size": len(self._get_input_columns()), + "output_size": len(_get_locations({InputTypes.TARGET}, column_definition)), + "category_counts": self.num_classes_per_cat_input, + "input_obs_loc": _get_locations({InputTypes.TARGET}, column_definition), + "static_input_loc": _get_locations({InputTypes.STATIC_INPUT}, column_definition), + "known_regular_inputs": _get_locations({InputTypes.STATIC_INPUT, InputTypes.KNOWN_INPUT}, real_inputs), + "known_categorical_inputs": _get_locations( + {InputTypes.STATIC_INPUT, InputTypes.KNOWN_INPUT}, categorical_inputs + ), + } + + return locations + + def get_experiment_params(self): + """Returns fixed model parameters for experiments.""" + + required_keys = [ + "total_time_steps", + "num_encoder_steps", + "num_epochs", + "early_stopping_patience", + "multiprocessing_workers", + ] + + fixed_params = self.get_fixed_params() + + for k in required_keys: + if k not in fixed_params: + raise ValueError("Field {}".format(k) + " missing from fixed parameter definitions!") + + fixed_params["column_definition"] = self.get_column_definition() + + fixed_params.update(self._get_tft_input_indices()) + + return fixed_params diff --git a/examples/benchmarks/TFT/data_formatters/electricity.py b/examples/benchmarks/TFT/data_formatters/electricity.py index 062a77eb2..366954a71 100644 --- a/examples/benchmarks/TFT/data_formatters/electricity.py +++ b/examples/benchmarks/TFT/data_formatters/electricity.py @@ -1,261 +1,254 @@ -# coding=utf-8 -# Copyright 2020 The Google Research Authors. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# Lint as: python3 -"""Custom formatting functions for Electricity dataset. - -Defines dataset specific column definitions and data transformations. Uses -entity specific z-score normalization. -""" - -import data_formatters.base -import libs.utils as utils -import pandas as pd -import sklearn.preprocessing - -GenericDataFormatter = data_formatters.base.GenericDataFormatter -DataTypes = data_formatters.base.DataTypes -InputTypes = data_formatters.base.InputTypes - - -class ElectricityFormatter(GenericDataFormatter): - """Defines and formats data for the electricity dataset. - - Note that per-entity z-score normalization is used here, and is implemented - across functions. - - Attributes: - column_definition: Defines input and data type of column used in the - experiment. - identifiers: Entity identifiers used in experiments. - """ - - _column_definition = [ - ('id', DataTypes.REAL_VALUED, InputTypes.ID), - ('hours_from_start', DataTypes.REAL_VALUED, InputTypes.TIME), - ('power_usage', DataTypes.REAL_VALUED, InputTypes.TARGET), - ('hour', DataTypes.REAL_VALUED, InputTypes.KNOWN_INPUT), - ('day_of_week', DataTypes.REAL_VALUED, InputTypes.KNOWN_INPUT), - ('hours_from_start', DataTypes.REAL_VALUED, InputTypes.KNOWN_INPUT), - ('categorical_id', DataTypes.CATEGORICAL, InputTypes.STATIC_INPUT), - ] - - def __init__(self): - """Initialises formatter.""" - - self.identifiers = None - self._real_scalers = None - self._cat_scalers = None - self._target_scaler = None - self._num_classes_per_cat_input = None - self._time_steps = self.get_fixed_params()['total_time_steps'] - - def split_data(self, df, valid_boundary=1315, test_boundary=1339): - """Splits data frame into training-validation-test data frames. - - This also calibrates scaling object, and transforms data for each split. - - Args: - df: Source data frame to split. - valid_boundary: Starting year for validation data - test_boundary: Starting year for test data - - Returns: - Tuple of transformed (train, valid, test) data. - """ - - print('Formatting train-valid-test splits.') - - index = df['days_from_start'] - train = df.loc[index < valid_boundary] - valid = df.loc[(index >= valid_boundary - 7) & (index < test_boundary)] - test = df.loc[index >= test_boundary - 7] - - self.set_scalers(train) - - return (self.transform_inputs(data) for data in [train, valid, test]) - - def set_scalers(self, df): - """Calibrates scalers using the data supplied. - - Args: - df: Data to use to calibrate scalers. - """ - print('Setting scalers with training data...') - - column_definitions = self.get_column_definition() - id_column = utils.get_single_col_by_input_type(InputTypes.ID, - column_definitions) - target_column = utils.get_single_col_by_input_type(InputTypes.TARGET, - column_definitions) - - # Format real scalers - real_inputs = utils.extract_cols_from_data_type( - DataTypes.REAL_VALUED, column_definitions, - {InputTypes.ID, InputTypes.TIME}) - - # Initialise scaler caches - self._real_scalers = {} - self._target_scaler = {} - identifiers = [] - for identifier, sliced in df.groupby(id_column): - - if len(sliced) >= self._time_steps: - - data = sliced[real_inputs].values - targets = sliced[[target_column]].values - self._real_scalers[identifier] \ - = sklearn.preprocessing.StandardScaler().fit(data) - - self._target_scaler[identifier] \ - = sklearn.preprocessing.StandardScaler().fit(targets) - identifiers.append(identifier) - - # Format categorical scalers - categorical_inputs = utils.extract_cols_from_data_type( - DataTypes.CATEGORICAL, column_definitions, - {InputTypes.ID, InputTypes.TIME}) - - categorical_scalers = {} - num_classes = [] - for col in categorical_inputs: - # Set all to str so that we don't have mixed integer/string columns - srs = df[col].apply(str) - categorical_scalers[col] = sklearn.preprocessing.LabelEncoder().fit( - srs.values) - num_classes.append(srs.nunique()) - - # Set categorical scaler outputs - self._cat_scalers = categorical_scalers - self._num_classes_per_cat_input = num_classes - - # Extract identifiers in case required - self.identifiers = identifiers - - def transform_inputs(self, df): - """Performs feature transformations. - - This includes both feature engineering, preprocessing and normalisation. - - Args: - df: Data frame to transform. - - Returns: - Transformed data frame. - - """ - - if self._real_scalers is None and self._cat_scalers is None: - raise ValueError('Scalers have not been set!') - - # Extract relevant columns - column_definitions = self.get_column_definition() - id_col = utils.get_single_col_by_input_type(InputTypes.ID, - column_definitions) - real_inputs = utils.extract_cols_from_data_type( - DataTypes.REAL_VALUED, column_definitions, - {InputTypes.ID, InputTypes.TIME}) - categorical_inputs = utils.extract_cols_from_data_type( - DataTypes.CATEGORICAL, column_definitions, - {InputTypes.ID, InputTypes.TIME}) - - # Transform real inputs per entity - df_list = [] - for identifier, sliced in df.groupby(id_col): - - # Filter out any trajectories that are too short - if len(sliced) >= self._time_steps: - sliced_copy = sliced.copy() - sliced_copy[real_inputs] = self._real_scalers[identifier].transform( - sliced_copy[real_inputs].values) - df_list.append(sliced_copy) - - output = pd.concat(df_list, axis=0) - - # Format categorical inputs - for col in categorical_inputs: - string_df = df[col].apply(str) - output[col] = self._cat_scalers[col].transform(string_df) - - return output - - def format_predictions(self, predictions): - """Reverts any normalisation to give predictions in original scale. - - Args: - predictions: Dataframe of model predictions. - - Returns: - Data frame of unnormalised predictions. - """ - - if self._target_scaler is None: - raise ValueError('Scalers have not been set!') - - column_names = predictions.columns - - df_list = [] - for identifier, sliced in predictions.groupby('identifier'): - sliced_copy = sliced.copy() - target_scaler = self._target_scaler[identifier] - - for col in column_names: - if col not in {'forecast_time', 'identifier'}: - sliced_copy[col] = target_scaler.inverse_transform(sliced_copy[col]) - df_list.append(sliced_copy) - - output = pd.concat(df_list, axis=0) - - return output - - # Default params - def get_fixed_params(self): - """Returns fixed model parameters for experiments.""" - - fixed_params = { - 'total_time_steps': 8 * 24, - 'num_encoder_steps': 7 * 24, - 'num_epochs': 100, - 'early_stopping_patience': 5, - 'multiprocessing_workers': 5 - } - - return fixed_params - - def get_default_model_params(self): - """Returns default optimised model parameters.""" - - model_params = { - 'dropout_rate': 0.1, - 'hidden_layer_size': 160, - 'learning_rate': 0.001, - 'minibatch_size': 64, - 'max_gradient_norm': 0.01, - 'num_heads': 4, - 'stack_size': 1 - } - - return model_params - - def get_num_samples_for_calibration(self): - """Gets the default number of training and validation samples. - - Use to sub-sample the data for network calibration and a value of -1 uses - all available samples. - - Returns: - Tuple of (training samples, validation samples) - """ - return 450000, 50000 +# coding=utf-8 +# Copyright 2020 The Google Research Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Lint as: python3 +"""Custom formatting functions for Electricity dataset. + +Defines dataset specific column definitions and data transformations. Uses +entity specific z-score normalization. +""" + +import data_formatters.base +import libs.utils as utils +import pandas as pd +import sklearn.preprocessing + +GenericDataFormatter = data_formatters.base.GenericDataFormatter +DataTypes = data_formatters.base.DataTypes +InputTypes = data_formatters.base.InputTypes + + +class ElectricityFormatter(GenericDataFormatter): + """Defines and formats data for the electricity dataset. + + Note that per-entity z-score normalization is used here, and is implemented + across functions. + + Attributes: + column_definition: Defines input and data type of column used in the + experiment. + identifiers: Entity identifiers used in experiments. + """ + + _column_definition = [ + ("id", DataTypes.REAL_VALUED, InputTypes.ID), + ("hours_from_start", DataTypes.REAL_VALUED, InputTypes.TIME), + ("power_usage", DataTypes.REAL_VALUED, InputTypes.TARGET), + ("hour", DataTypes.REAL_VALUED, InputTypes.KNOWN_INPUT), + ("day_of_week", DataTypes.REAL_VALUED, InputTypes.KNOWN_INPUT), + ("hours_from_start", DataTypes.REAL_VALUED, InputTypes.KNOWN_INPUT), + ("categorical_id", DataTypes.CATEGORICAL, InputTypes.STATIC_INPUT), + ] + + def __init__(self): + """Initialises formatter.""" + + self.identifiers = None + self._real_scalers = None + self._cat_scalers = None + self._target_scaler = None + self._num_classes_per_cat_input = None + self._time_steps = self.get_fixed_params()["total_time_steps"] + + def split_data(self, df, valid_boundary=1315, test_boundary=1339): + """Splits data frame into training-validation-test data frames. + + This also calibrates scaling object, and transforms data for each split. + + Args: + df: Source data frame to split. + valid_boundary: Starting year for validation data + test_boundary: Starting year for test data + + Returns: + Tuple of transformed (train, valid, test) data. + """ + + print("Formatting train-valid-test splits.") + + index = df["days_from_start"] + train = df.loc[index < valid_boundary] + valid = df.loc[(index >= valid_boundary - 7) & (index < test_boundary)] + test = df.loc[index >= test_boundary - 7] + + self.set_scalers(train) + + return (self.transform_inputs(data) for data in [train, valid, test]) + + def set_scalers(self, df): + """Calibrates scalers using the data supplied. + + Args: + df: Data to use to calibrate scalers. + """ + print("Setting scalers with training data...") + + column_definitions = self.get_column_definition() + id_column = utils.get_single_col_by_input_type(InputTypes.ID, column_definitions) + target_column = utils.get_single_col_by_input_type(InputTypes.TARGET, column_definitions) + + # Format real scalers + real_inputs = utils.extract_cols_from_data_type( + DataTypes.REAL_VALUED, column_definitions, {InputTypes.ID, InputTypes.TIME} + ) + + # Initialise scaler caches + self._real_scalers = {} + self._target_scaler = {} + identifiers = [] + for identifier, sliced in df.groupby(id_column): + + if len(sliced) >= self._time_steps: + + data = sliced[real_inputs].values + targets = sliced[[target_column]].values + self._real_scalers[identifier] = sklearn.preprocessing.StandardScaler().fit(data) + + self._target_scaler[identifier] = sklearn.preprocessing.StandardScaler().fit(targets) + identifiers.append(identifier) + + # Format categorical scalers + categorical_inputs = utils.extract_cols_from_data_type( + DataTypes.CATEGORICAL, column_definitions, {InputTypes.ID, InputTypes.TIME} + ) + + categorical_scalers = {} + num_classes = [] + for col in categorical_inputs: + # Set all to str so that we don't have mixed integer/string columns + srs = df[col].apply(str) + categorical_scalers[col] = sklearn.preprocessing.LabelEncoder().fit(srs.values) + num_classes.append(srs.nunique()) + + # Set categorical scaler outputs + self._cat_scalers = categorical_scalers + self._num_classes_per_cat_input = num_classes + + # Extract identifiers in case required + self.identifiers = identifiers + + def transform_inputs(self, df): + """Performs feature transformations. + + This includes both feature engineering, preprocessing and normalisation. + + Args: + df: Data frame to transform. + + Returns: + Transformed data frame. + + """ + + if self._real_scalers is None and self._cat_scalers is None: + raise ValueError("Scalers have not been set!") + + # Extract relevant columns + column_definitions = self.get_column_definition() + id_col = utils.get_single_col_by_input_type(InputTypes.ID, column_definitions) + real_inputs = utils.extract_cols_from_data_type( + DataTypes.REAL_VALUED, column_definitions, {InputTypes.ID, InputTypes.TIME} + ) + categorical_inputs = utils.extract_cols_from_data_type( + DataTypes.CATEGORICAL, column_definitions, {InputTypes.ID, InputTypes.TIME} + ) + + # Transform real inputs per entity + df_list = [] + for identifier, sliced in df.groupby(id_col): + + # Filter out any trajectories that are too short + if len(sliced) >= self._time_steps: + sliced_copy = sliced.copy() + sliced_copy[real_inputs] = self._real_scalers[identifier].transform(sliced_copy[real_inputs].values) + df_list.append(sliced_copy) + + output = pd.concat(df_list, axis=0) + + # Format categorical inputs + for col in categorical_inputs: + string_df = df[col].apply(str) + output[col] = self._cat_scalers[col].transform(string_df) + + return output + + def format_predictions(self, predictions): + """Reverts any normalisation to give predictions in original scale. + + Args: + predictions: Dataframe of model predictions. + + Returns: + Data frame of unnormalised predictions. + """ + + if self._target_scaler is None: + raise ValueError("Scalers have not been set!") + + column_names = predictions.columns + + df_list = [] + for identifier, sliced in predictions.groupby("identifier"): + sliced_copy = sliced.copy() + target_scaler = self._target_scaler[identifier] + + for col in column_names: + if col not in {"forecast_time", "identifier"}: + sliced_copy[col] = target_scaler.inverse_transform(sliced_copy[col]) + df_list.append(sliced_copy) + + output = pd.concat(df_list, axis=0) + + return output + + # Default params + def get_fixed_params(self): + """Returns fixed model parameters for experiments.""" + + fixed_params = { + "total_time_steps": 8 * 24, + "num_encoder_steps": 7 * 24, + "num_epochs": 100, + "early_stopping_patience": 5, + "multiprocessing_workers": 5, + } + + return fixed_params + + def get_default_model_params(self): + """Returns default optimised model parameters.""" + + model_params = { + "dropout_rate": 0.1, + "hidden_layer_size": 160, + "learning_rate": 0.001, + "minibatch_size": 64, + "max_gradient_norm": 0.01, + "num_heads": 4, + "stack_size": 1, + } + + return model_params + + def get_num_samples_for_calibration(self): + """Gets the default number of training and validation samples. + + Use to sub-sample the data for network calibration and a value of -1 uses + all available samples. + + Returns: + Tuple of (training samples, validation samples) + """ + return 450000, 50000 diff --git a/examples/benchmarks/TFT/data_formatters/favorita.py b/examples/benchmarks/TFT/data_formatters/favorita.py index 26fae632c..bc7a24140 100644 --- a/examples/benchmarks/TFT/data_formatters/favorita.py +++ b/examples/benchmarks/TFT/data_formatters/favorita.py @@ -1,327 +1,333 @@ -# coding=utf-8 -# Copyright 2020 The Google Research Authors. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# Lint as: python3 -"""Custom formatting functions for Favorita dataset. - -Defines dataset specific column definitions and data transformations. -""" - -import data_formatters.base -import libs.utils as utils -import pandas as pd -import sklearn.preprocessing - -DataTypes = data_formatters.base.DataTypes -InputTypes = data_formatters.base.InputTypes - - -class FavoritaFormatter(data_formatters.base.GenericDataFormatter): - """Defines and formats data for the Favorita dataset. - - Attributes: - column_definition: Defines input and data type of column used in the - experiment. - identifiers: Entity identifiers used in experiments. - """ - - _column_definition = [ - ('traj_id', DataTypes.REAL_VALUED, InputTypes.ID), - ('date', DataTypes.DATE, InputTypes.TIME), - ('log_sales', DataTypes.REAL_VALUED, InputTypes.TARGET), - ('onpromotion', DataTypes.CATEGORICAL, InputTypes.KNOWN_INPUT), - ('transactions', DataTypes.REAL_VALUED, InputTypes.OBSERVED_INPUT), - ('oil', DataTypes.REAL_VALUED, InputTypes.OBSERVED_INPUT), - ('day_of_week', DataTypes.CATEGORICAL, InputTypes.KNOWN_INPUT), - ('day_of_month', DataTypes.REAL_VALUED, InputTypes.KNOWN_INPUT), - ('month', DataTypes.REAL_VALUED, InputTypes.KNOWN_INPUT), - ('national_hol', DataTypes.CATEGORICAL, InputTypes.KNOWN_INPUT), - ('regional_hol', DataTypes.CATEGORICAL, InputTypes.KNOWN_INPUT), - ('local_hol', DataTypes.CATEGORICAL, InputTypes.KNOWN_INPUT), - ('open', DataTypes.REAL_VALUED, InputTypes.KNOWN_INPUT), - ('item_nbr', DataTypes.CATEGORICAL, InputTypes.STATIC_INPUT), - ('store_nbr', DataTypes.CATEGORICAL, InputTypes.STATIC_INPUT), - ('city', DataTypes.CATEGORICAL, InputTypes.STATIC_INPUT), - ('state', DataTypes.CATEGORICAL, InputTypes.STATIC_INPUT), - ('type', DataTypes.CATEGORICAL, InputTypes.STATIC_INPUT), - ('cluster', DataTypes.CATEGORICAL, InputTypes.STATIC_INPUT), - ('family', DataTypes.CATEGORICAL, InputTypes.STATIC_INPUT), - ('class', DataTypes.CATEGORICAL, InputTypes.STATIC_INPUT), - ('perishable', DataTypes.CATEGORICAL, InputTypes.STATIC_INPUT) - ] - - def __init__(self): - """Initialises formatter.""" - - self.identifiers = None - self._real_scalers = None - self._cat_scalers = None - self._target_scaler = None - self._num_classes_per_cat_input = None - - def split_data(self, df, valid_boundary=None, test_boundary=None): - """Splits data frame into training-validation-test data frames. - - This also calibrates scaling object, and transforms data for each split. - - Args: - df: Source data frame to split. - valid_boundary: Starting year for validation data - test_boundary: Starting year for test data - - Returns: - Tuple of transformed (train, valid, test) data. - """ - - print('Formatting train-valid-test splits.') - - if valid_boundary is None: - valid_boundary = pd.datetime(2015, 12, 1) - - fixed_params = self.get_fixed_params() - time_steps = fixed_params['total_time_steps'] - lookback = fixed_params['num_encoder_steps'] - forecast_horizon = time_steps - lookback - - df['date'] = pd.to_datetime(df['date']) - df_lists = {'train': [], 'valid': [], 'test': []} - for _, sliced in df.groupby('traj_id'): - index = sliced['date'] - train = sliced.loc[index < valid_boundary] - train_len = len(train) - valid_len = train_len + forecast_horizon - valid = sliced.iloc[train_len - lookback:valid_len, :] - test = sliced.iloc[valid_len - lookback:valid_len + forecast_horizon, :] - - sliced_map = {'train': train, 'valid': valid, 'test': test} - - for k in sliced_map: - item = sliced_map[k] - - if len(item) >= time_steps: - df_lists[k].append(item) - - dfs = {k: pd.concat(df_lists[k], axis=0) for k in df_lists} - - train = dfs['train'] - self.set_scalers(train, set_real=True) - - # Use all data for label encoding to handle labels not present in training. - self.set_scalers(df, set_real=False) - - # Filter out identifiers not present in training (i.e. cold-started items). - def filter_ids(frame): - identifiers = set(self.identifiers) - index = frame['traj_id'] - return frame.loc[index.apply(lambda x: x in identifiers)] - - valid = filter_ids(dfs['valid']) - test = filter_ids(dfs['test']) - - return (self.transform_inputs(data) for data in [train, valid, test]) - - def set_scalers(self, df, set_real=True): - """Calibrates scalers using the data supplied. - - Label encoding is applied to the entire dataset (i.e. including test), - so that unseen labels can be handled at run-time. - - Args: - df: Data to use to calibrate scalers. - set_real: Whether to fit set real-valued or categorical scalers - """ - print('Setting scalers with training data...') - - column_definitions = self.get_column_definition() - id_column = utils.get_single_col_by_input_type(InputTypes.ID, - column_definitions) - target_column = utils.get_single_col_by_input_type(InputTypes.TARGET, - column_definitions) - - if set_real: - - # Extract identifiers in case required - self.identifiers = list(df[id_column].unique()) - - # Format real scalers - self._real_scalers = {} - for col in ['oil', 'transactions', 'log_sales']: - self._real_scalers[col] = (df[col].mean(), df[col].std()) - - self._target_scaler = (df[target_column].mean(), df[target_column].std()) - - else: - # Format categorical scalers - categorical_inputs = utils.extract_cols_from_data_type( - DataTypes.CATEGORICAL, column_definitions, - {InputTypes.ID, InputTypes.TIME}) - - categorical_scalers = {} - num_classes = [] - if self.identifiers is None: - raise ValueError('Scale real-valued inputs first!') - id_set = set(self.identifiers) - valid_idx = df['traj_id'].apply(lambda x: x in id_set) - for col in categorical_inputs: - # Set all to str so that we don't have mixed integer/string columns - srs = df[col].apply(str).loc[valid_idx] - categorical_scalers[col] = sklearn.preprocessing.LabelEncoder().fit( - srs.values) - - num_classes.append(srs.nunique()) - - # Set categorical scaler outputs - self._cat_scalers = categorical_scalers - self._num_classes_per_cat_input = num_classes - - def transform_inputs(self, df): - """Performs feature transformations. - - This includes both feature engineering, preprocessing and normalisation. - - Args: - df: Data frame to transform. - - Returns: - Transformed data frame. - - """ - output = df.copy() - - if self._real_scalers is None and self._cat_scalers is None: - raise ValueError('Scalers have not been set!') - - column_definitions = self.get_column_definition() - - categorical_inputs = utils.extract_cols_from_data_type( - DataTypes.CATEGORICAL, column_definitions, - {InputTypes.ID, InputTypes.TIME}) - - # Format real inputs - for col in ['log_sales', 'oil', 'transactions']: - mean, std = self._real_scalers[col] - output[col] = (df[col] - mean) / std - - if col == 'log_sales': - output[col] = output[col].fillna(0.) # mean imputation - - # Format categorical inputs - for col in categorical_inputs: - string_df = df[col].apply(str) - output[col] = self._cat_scalers[col].transform(string_df) - - return output - - def format_predictions(self, predictions): - """Reverts any normalisation to give predictions in original scale. - - Args: - predictions: Dataframe of model predictions. - - Returns: - Data frame of unnormalised predictions. - """ - output = predictions.copy() - - column_names = predictions.columns - mean, std = self._target_scaler - for col in column_names: - if col not in {'forecast_time', 'identifier'}: - output[col] = (predictions[col] * std) + mean - - return output - - # Default params - def get_fixed_params(self): - """Returns fixed model parameters for experiments.""" - - fixed_params = { - 'total_time_steps': 120, - 'num_encoder_steps': 90, - 'num_epochs': 100, - 'early_stopping_patience': 5, - 'multiprocessing_workers': 5 - } - - return fixed_params - - def get_default_model_params(self): - """Returns default optimised model parameters.""" - - model_params = { - 'dropout_rate': 0.1, - 'hidden_layer_size': 240, - 'learning_rate': 0.001, - 'minibatch_size': 128, - 'max_gradient_norm': 100., - 'num_heads': 4, - 'stack_size': 1 - } - - return model_params - - def get_num_samples_for_calibration(self): - """Gets the default number of training and validation samples. - - Use to sub-sample the data for network calibration and a value of -1 uses - all available samples. - - Returns: - Tuple of (training samples, validation samples) - """ - return 450000, 50000 - - def get_column_definition(self): - """"Formats column definition in order expected by the TFT. - - Modified for Favorita to match column order of original experiment. - - Returns: - Favorita-specific column definition - """ - - column_definition = self._column_definition - - # Sanity checks first. - # Ensure only one ID and time column exist - def _check_single_column(input_type): - - length = len([tup for tup in column_definition if tup[2] == input_type]) - - if length != 1: - raise ValueError('Illegal number of inputs ({}) of type {}'.format( - length, input_type)) - - _check_single_column(InputTypes.ID) - _check_single_column(InputTypes.TIME) - - identifier = [tup for tup in column_definition if tup[2] == InputTypes.ID] - time = [tup for tup in column_definition if tup[2] == InputTypes.TIME] - real_inputs = [ - tup for tup in column_definition if tup[1] == DataTypes.REAL_VALUED and - tup[2] not in {InputTypes.ID, InputTypes.TIME} - ] - - col_definition_map = {tup[0]: tup for tup in column_definition} - col_order = [ - 'item_nbr', 'store_nbr', 'city', 'state', 'type', 'cluster', 'family', - 'class', 'perishable', 'onpromotion', 'day_of_week', 'national_hol', - 'regional_hol', 'local_hol' - ] - categorical_inputs = [ - col_definition_map[k] for k in col_order if k in col_definition_map - ] - - return identifier + time + real_inputs + categorical_inputs +# coding=utf-8 +# Copyright 2020 The Google Research Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Lint as: python3 +"""Custom formatting functions for Favorita dataset. + +Defines dataset specific column definitions and data transformations. +""" + +import data_formatters.base +import libs.utils as utils +import pandas as pd +import sklearn.preprocessing + +DataTypes = data_formatters.base.DataTypes +InputTypes = data_formatters.base.InputTypes + + +class FavoritaFormatter(data_formatters.base.GenericDataFormatter): + """Defines and formats data for the Favorita dataset. + + Attributes: + column_definition: Defines input and data type of column used in the + experiment. + identifiers: Entity identifiers used in experiments. + """ + + _column_definition = [ + ("traj_id", DataTypes.REAL_VALUED, InputTypes.ID), + ("date", DataTypes.DATE, InputTypes.TIME), + ("log_sales", DataTypes.REAL_VALUED, InputTypes.TARGET), + ("onpromotion", DataTypes.CATEGORICAL, InputTypes.KNOWN_INPUT), + ("transactions", DataTypes.REAL_VALUED, InputTypes.OBSERVED_INPUT), + ("oil", DataTypes.REAL_VALUED, InputTypes.OBSERVED_INPUT), + ("day_of_week", DataTypes.CATEGORICAL, InputTypes.KNOWN_INPUT), + ("day_of_month", DataTypes.REAL_VALUED, InputTypes.KNOWN_INPUT), + ("month", DataTypes.REAL_VALUED, InputTypes.KNOWN_INPUT), + ("national_hol", DataTypes.CATEGORICAL, InputTypes.KNOWN_INPUT), + ("regional_hol", DataTypes.CATEGORICAL, InputTypes.KNOWN_INPUT), + ("local_hol", DataTypes.CATEGORICAL, InputTypes.KNOWN_INPUT), + ("open", DataTypes.REAL_VALUED, InputTypes.KNOWN_INPUT), + ("item_nbr", DataTypes.CATEGORICAL, InputTypes.STATIC_INPUT), + ("store_nbr", DataTypes.CATEGORICAL, InputTypes.STATIC_INPUT), + ("city", DataTypes.CATEGORICAL, InputTypes.STATIC_INPUT), + ("state", DataTypes.CATEGORICAL, InputTypes.STATIC_INPUT), + ("type", DataTypes.CATEGORICAL, InputTypes.STATIC_INPUT), + ("cluster", DataTypes.CATEGORICAL, InputTypes.STATIC_INPUT), + ("family", DataTypes.CATEGORICAL, InputTypes.STATIC_INPUT), + ("class", DataTypes.CATEGORICAL, InputTypes.STATIC_INPUT), + ("perishable", DataTypes.CATEGORICAL, InputTypes.STATIC_INPUT), + ] + + def __init__(self): + """Initialises formatter.""" + + self.identifiers = None + self._real_scalers = None + self._cat_scalers = None + self._target_scaler = None + self._num_classes_per_cat_input = None + + def split_data(self, df, valid_boundary=None, test_boundary=None): + """Splits data frame into training-validation-test data frames. + + This also calibrates scaling object, and transforms data for each split. + + Args: + df: Source data frame to split. + valid_boundary: Starting year for validation data + test_boundary: Starting year for test data + + Returns: + Tuple of transformed (train, valid, test) data. + """ + + print("Formatting train-valid-test splits.") + + if valid_boundary is None: + valid_boundary = pd.datetime(2015, 12, 1) + + fixed_params = self.get_fixed_params() + time_steps = fixed_params["total_time_steps"] + lookback = fixed_params["num_encoder_steps"] + forecast_horizon = time_steps - lookback + + df["date"] = pd.to_datetime(df["date"]) + df_lists = {"train": [], "valid": [], "test": []} + for _, sliced in df.groupby("traj_id"): + index = sliced["date"] + train = sliced.loc[index < valid_boundary] + train_len = len(train) + valid_len = train_len + forecast_horizon + valid = sliced.iloc[train_len - lookback : valid_len, :] + test = sliced.iloc[valid_len - lookback : valid_len + forecast_horizon, :] + + sliced_map = {"train": train, "valid": valid, "test": test} + + for k in sliced_map: + item = sliced_map[k] + + if len(item) >= time_steps: + df_lists[k].append(item) + + dfs = {k: pd.concat(df_lists[k], axis=0) for k in df_lists} + + train = dfs["train"] + self.set_scalers(train, set_real=True) + + # Use all data for label encoding to handle labels not present in training. + self.set_scalers(df, set_real=False) + + # Filter out identifiers not present in training (i.e. cold-started items). + def filter_ids(frame): + identifiers = set(self.identifiers) + index = frame["traj_id"] + return frame.loc[index.apply(lambda x: x in identifiers)] + + valid = filter_ids(dfs["valid"]) + test = filter_ids(dfs["test"]) + + return (self.transform_inputs(data) for data in [train, valid, test]) + + def set_scalers(self, df, set_real=True): + """Calibrates scalers using the data supplied. + + Label encoding is applied to the entire dataset (i.e. including test), + so that unseen labels can be handled at run-time. + + Args: + df: Data to use to calibrate scalers. + set_real: Whether to fit set real-valued or categorical scalers + """ + print("Setting scalers with training data...") + + column_definitions = self.get_column_definition() + id_column = utils.get_single_col_by_input_type(InputTypes.ID, column_definitions) + target_column = utils.get_single_col_by_input_type(InputTypes.TARGET, column_definitions) + + if set_real: + + # Extract identifiers in case required + self.identifiers = list(df[id_column].unique()) + + # Format real scalers + self._real_scalers = {} + for col in ["oil", "transactions", "log_sales"]: + self._real_scalers[col] = (df[col].mean(), df[col].std()) + + self._target_scaler = (df[target_column].mean(), df[target_column].std()) + + else: + # Format categorical scalers + categorical_inputs = utils.extract_cols_from_data_type( + DataTypes.CATEGORICAL, column_definitions, {InputTypes.ID, InputTypes.TIME} + ) + + categorical_scalers = {} + num_classes = [] + if self.identifiers is None: + raise ValueError("Scale real-valued inputs first!") + id_set = set(self.identifiers) + valid_idx = df["traj_id"].apply(lambda x: x in id_set) + for col in categorical_inputs: + # Set all to str so that we don't have mixed integer/string columns + srs = df[col].apply(str).loc[valid_idx] + categorical_scalers[col] = sklearn.preprocessing.LabelEncoder().fit(srs.values) + + num_classes.append(srs.nunique()) + + # Set categorical scaler outputs + self._cat_scalers = categorical_scalers + self._num_classes_per_cat_input = num_classes + + def transform_inputs(self, df): + """Performs feature transformations. + + This includes both feature engineering, preprocessing and normalisation. + + Args: + df: Data frame to transform. + + Returns: + Transformed data frame. + + """ + output = df.copy() + + if self._real_scalers is None and self._cat_scalers is None: + raise ValueError("Scalers have not been set!") + + column_definitions = self.get_column_definition() + + categorical_inputs = utils.extract_cols_from_data_type( + DataTypes.CATEGORICAL, column_definitions, {InputTypes.ID, InputTypes.TIME} + ) + + # Format real inputs + for col in ["log_sales", "oil", "transactions"]: + mean, std = self._real_scalers[col] + output[col] = (df[col] - mean) / std + + if col == "log_sales": + output[col] = output[col].fillna(0.0) # mean imputation + + # Format categorical inputs + for col in categorical_inputs: + string_df = df[col].apply(str) + output[col] = self._cat_scalers[col].transform(string_df) + + return output + + def format_predictions(self, predictions): + """Reverts any normalisation to give predictions in original scale. + + Args: + predictions: Dataframe of model predictions. + + Returns: + Data frame of unnormalised predictions. + """ + output = predictions.copy() + + column_names = predictions.columns + mean, std = self._target_scaler + for col in column_names: + if col not in {"forecast_time", "identifier"}: + output[col] = (predictions[col] * std) + mean + + return output + + # Default params + def get_fixed_params(self): + """Returns fixed model parameters for experiments.""" + + fixed_params = { + "total_time_steps": 120, + "num_encoder_steps": 90, + "num_epochs": 100, + "early_stopping_patience": 5, + "multiprocessing_workers": 5, + } + + return fixed_params + + def get_default_model_params(self): + """Returns default optimised model parameters.""" + + model_params = { + "dropout_rate": 0.1, + "hidden_layer_size": 240, + "learning_rate": 0.001, + "minibatch_size": 128, + "max_gradient_norm": 100.0, + "num_heads": 4, + "stack_size": 1, + } + + return model_params + + def get_num_samples_for_calibration(self): + """Gets the default number of training and validation samples. + + Use to sub-sample the data for network calibration and a value of -1 uses + all available samples. + + Returns: + Tuple of (training samples, validation samples) + """ + return 450000, 50000 + + def get_column_definition(self): + """ "Formats column definition in order expected by the TFT. + + Modified for Favorita to match column order of original experiment. + + Returns: + Favorita-specific column definition + """ + + column_definition = self._column_definition + + # Sanity checks first. + # Ensure only one ID and time column exist + def _check_single_column(input_type): + + length = len([tup for tup in column_definition if tup[2] == input_type]) + + if length != 1: + raise ValueError("Illegal number of inputs ({}) of type {}".format(length, input_type)) + + _check_single_column(InputTypes.ID) + _check_single_column(InputTypes.TIME) + + identifier = [tup for tup in column_definition if tup[2] == InputTypes.ID] + time = [tup for tup in column_definition if tup[2] == InputTypes.TIME] + real_inputs = [ + tup + for tup in column_definition + if tup[1] == DataTypes.REAL_VALUED and tup[2] not in {InputTypes.ID, InputTypes.TIME} + ] + + col_definition_map = {tup[0]: tup for tup in column_definition} + col_order = [ + "item_nbr", + "store_nbr", + "city", + "state", + "type", + "cluster", + "family", + "class", + "perishable", + "onpromotion", + "day_of_week", + "national_hol", + "regional_hol", + "local_hol", + ] + categorical_inputs = [col_definition_map[k] for k in col_order if k in col_definition_map] + + return identifier + time + real_inputs + categorical_inputs diff --git a/examples/benchmarks/TFT/data_formatters/qlib_Alpha158.py b/examples/benchmarks/TFT/data_formatters/qlib_Alpha158.py index aa081fb17..e9236d041 100644 --- a/examples/benchmarks/TFT/data_formatters/qlib_Alpha158.py +++ b/examples/benchmarks/TFT/data_formatters/qlib_Alpha158.py @@ -1,220 +1,219 @@ -# coding=utf-8 -# Copyright 2020 The Google Research Authors. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# Lint as: python3 -"""Custom formatting functions for Alpha158 dataset. - -Defines dataset specific column definitions and data transformations. -""" - -import data_formatters.base -import libs.utils as utils -import sklearn.preprocessing - -GenericDataFormatter = data_formatters.base.GenericDataFormatter -DataTypes = data_formatters.base.DataTypes -InputTypes = data_formatters.base.InputTypes - -class Alpha158Formatter(GenericDataFormatter): - """Defines and formats data for the Alpha158 dataset. - - Attributes: - column_definition: Defines input and data type of column used in the - experiment. - identifiers: Entity identifiers used in experiments. - """ - - _column_definition = [ - ('instrument', DataTypes.CATEGORICAL, InputTypes.ID), - ('LABEL0', DataTypes.REAL_VALUED, InputTypes.TARGET), - ('date', DataTypes.DATE, InputTypes.TIME), - ('month', DataTypes.CATEGORICAL, InputTypes.KNOWN_INPUT), - ('day_of_week', DataTypes.CATEGORICAL, InputTypes.KNOWN_INPUT), - # Selected 10 features - ('RESI5', DataTypes.REAL_VALUED, InputTypes.OBSERVED_INPUT), - ('WVMA5', DataTypes.REAL_VALUED, InputTypes.OBSERVED_INPUT), - ('RSQR5', DataTypes.REAL_VALUED, InputTypes.OBSERVED_INPUT), - ('KLEN', DataTypes.REAL_VALUED, InputTypes.OBSERVED_INPUT), - ('RSQR10', DataTypes.REAL_VALUED, InputTypes.OBSERVED_INPUT), - ('CORR5', DataTypes.REAL_VALUED, InputTypes.OBSERVED_INPUT), - ('CORD5', DataTypes.REAL_VALUED, InputTypes.OBSERVED_INPUT), - ('CORR10', DataTypes.REAL_VALUED, InputTypes.OBSERVED_INPUT), - ('ROC60', DataTypes.REAL_VALUED, InputTypes.OBSERVED_INPUT), - ('RESI10', DataTypes.REAL_VALUED, InputTypes.OBSERVED_INPUT), - ('const', DataTypes.CATEGORICAL, InputTypes.STATIC_INPUT), - ] - - def __init__(self): - """Initialises formatter.""" - - self.identifiers = None - self._real_scalers = None - self._cat_scalers = None - self._target_scaler = None - self._num_classes_per_cat_input = None - - def split_data(self, df, valid_boundary=2016, test_boundary=2018): - """Splits data frame into training-validation-test data frames. - - This also calibrates scaling object, and transforms data for each split. - - Args: - df: Source data frame to split. - valid_boundary: Starting year for validation data - test_boundary: Starting year for test data - - Returns: - Tuple of transformed (train, valid, test) data. - """ - - print('Formatting train-valid-test splits.') - - index = df['year'] - train = df.loc[index < valid_boundary] - valid = df.loc[(index >= valid_boundary) & (index < test_boundary)] - test = df.loc[index >= test_boundary] - - self.set_scalers(train) - - return (self.transform_inputs(data) for data in [train, valid, test]) - - def set_scalers(self, df): - """Calibrates scalers using the data supplied. - - Args: - df: Data to use to calibrate scalers. - """ - print('Setting scalers with training data...') - - column_definitions = self.get_column_definition() - id_column = utils.get_single_col_by_input_type(InputTypes.ID, - column_definitions) - target_column = utils.get_single_col_by_input_type(InputTypes.TARGET, - column_definitions) - - # Extract identifiers in case required - self.identifiers = list(df[id_column].unique()) - - # Format real scalers - real_inputs = utils.extract_cols_from_data_type( - DataTypes.REAL_VALUED, column_definitions, - {InputTypes.ID, InputTypes.TIME}) - - data = df[real_inputs].values - self._real_scalers = sklearn.preprocessing.StandardScaler().fit(data) - self._target_scaler = sklearn.preprocessing.StandardScaler().fit( - df[[target_column]].values) # used for predictions - - # Format categorical scalers - categorical_inputs = utils.extract_cols_from_data_type( - DataTypes.CATEGORICAL, column_definitions, - {InputTypes.ID, InputTypes.TIME}) - - categorical_scalers = {} - num_classes = [] - for col in categorical_inputs: - # Set all to str so that we don't have mixed integer/string columns - srs = df[col].apply(str) - categorical_scalers[col] = sklearn.preprocessing.LabelEncoder().fit( - srs.values) - num_classes.append(srs.nunique()) - - # Set categorical scaler outputs - self._cat_scalers = categorical_scalers - self._num_classes_per_cat_input = num_classes - - def transform_inputs(self, df): - """Performs feature transformations. - - This includes both feature engineering, preprocessing and normalisation. - - Args: - df: Data frame to transform. - - Returns: - Transformed data frame. - - """ - output = df.copy() - - if self._real_scalers is None and self._cat_scalers is None: - raise ValueError('Scalers have not been set!') - - column_definitions = self.get_column_definition() - - real_inputs = utils.extract_cols_from_data_type( - DataTypes.REAL_VALUED, column_definitions, - {InputTypes.ID, InputTypes.TIME}) - categorical_inputs = utils.extract_cols_from_data_type( - DataTypes.CATEGORICAL, column_definitions, - {InputTypes.ID, InputTypes.TIME}) - - # Format real inputs - output[real_inputs] = self._real_scalers.transform(df[real_inputs].values) - - # Format categorical inputs - for col in categorical_inputs: - string_df = df[col].apply(str) - output[col] = self._cat_scalers[col].transform(string_df) - - return output - - def format_predictions(self, predictions): - """Reverts any normalisation to give predictions in original scale. - - Args: - predictions: Dataframe of model predictions. - - Returns: - Data frame of unnormalised predictions. - """ - output = predictions.copy() - - column_names = predictions.columns - - for col in column_names: - if col not in {'forecast_time', 'identifier'}: - output[col] = self._target_scaler.inverse_transform(predictions[col]) - - return output - - # Default params - def get_fixed_params(self): - """Returns fixed model parameters for experiments.""" - - fixed_params = { - 'total_time_steps': 16 + 6, - 'num_encoder_steps': 16, - 'num_epochs': 100, - 'early_stopping_patience': 5, - 'multiprocessing_workers': 5, - } - - return fixed_params - - def get_default_model_params(self): - """Returns default optimised model parameters.""" - - model_params = { - 'dropout_rate': 0.3, - 'hidden_layer_size': 160, - 'learning_rate': 0.01, - 'minibatch_size': 64, - 'max_gradient_norm': 0.01, - 'num_heads': 1, - 'stack_size': 1 - } - - return model_params +# coding=utf-8 +# Copyright 2020 The Google Research Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Lint as: python3 +"""Custom formatting functions for Alpha158 dataset. + +Defines dataset specific column definitions and data transformations. +""" + +import data_formatters.base +import libs.utils as utils +import sklearn.preprocessing + +GenericDataFormatter = data_formatters.base.GenericDataFormatter +DataTypes = data_formatters.base.DataTypes +InputTypes = data_formatters.base.InputTypes + + +class Alpha158Formatter(GenericDataFormatter): + """Defines and formats data for the Alpha158 dataset. + + Attributes: + column_definition: Defines input and data type of column used in the + experiment. + identifiers: Entity identifiers used in experiments. + """ + + _column_definition = [ + ("instrument", DataTypes.CATEGORICAL, InputTypes.ID), + ("LABEL0", DataTypes.REAL_VALUED, InputTypes.TARGET), + ("date", DataTypes.DATE, InputTypes.TIME), + ("month", DataTypes.CATEGORICAL, InputTypes.KNOWN_INPUT), + ("day_of_week", DataTypes.CATEGORICAL, InputTypes.KNOWN_INPUT), + # Selected 10 features + ("RESI5", DataTypes.REAL_VALUED, InputTypes.OBSERVED_INPUT), + ("WVMA5", DataTypes.REAL_VALUED, InputTypes.OBSERVED_INPUT), + ("RSQR5", DataTypes.REAL_VALUED, InputTypes.OBSERVED_INPUT), + ("KLEN", DataTypes.REAL_VALUED, InputTypes.OBSERVED_INPUT), + ("RSQR10", DataTypes.REAL_VALUED, InputTypes.OBSERVED_INPUT), + ("CORR5", DataTypes.REAL_VALUED, InputTypes.OBSERVED_INPUT), + ("CORD5", DataTypes.REAL_VALUED, InputTypes.OBSERVED_INPUT), + ("CORR10", DataTypes.REAL_VALUED, InputTypes.OBSERVED_INPUT), + ("ROC60", DataTypes.REAL_VALUED, InputTypes.OBSERVED_INPUT), + ("RESI10", DataTypes.REAL_VALUED, InputTypes.OBSERVED_INPUT), + ("const", DataTypes.CATEGORICAL, InputTypes.STATIC_INPUT), + ] + + def __init__(self): + """Initialises formatter.""" + + self.identifiers = None + self._real_scalers = None + self._cat_scalers = None + self._target_scaler = None + self._num_classes_per_cat_input = None + + def split_data(self, df, valid_boundary=2016, test_boundary=2018): + """Splits data frame into training-validation-test data frames. + + This also calibrates scaling object, and transforms data for each split. + + Args: + df: Source data frame to split. + valid_boundary: Starting year for validation data + test_boundary: Starting year for test data + + Returns: + Tuple of transformed (train, valid, test) data. + """ + + print("Formatting train-valid-test splits.") + + index = df["year"] + train = df.loc[index < valid_boundary] + valid = df.loc[(index >= valid_boundary) & (index < test_boundary)] + test = df.loc[index >= test_boundary] + + self.set_scalers(train) + + return (self.transform_inputs(data) for data in [train, valid, test]) + + def set_scalers(self, df): + """Calibrates scalers using the data supplied. + + Args: + df: Data to use to calibrate scalers. + """ + print("Setting scalers with training data...") + + column_definitions = self.get_column_definition() + id_column = utils.get_single_col_by_input_type(InputTypes.ID, column_definitions) + target_column = utils.get_single_col_by_input_type(InputTypes.TARGET, column_definitions) + + # Extract identifiers in case required + self.identifiers = list(df[id_column].unique()) + + # Format real scalers + real_inputs = utils.extract_cols_from_data_type( + DataTypes.REAL_VALUED, column_definitions, {InputTypes.ID, InputTypes.TIME} + ) + + data = df[real_inputs].values + self._real_scalers = sklearn.preprocessing.StandardScaler().fit(data) + self._target_scaler = sklearn.preprocessing.StandardScaler().fit( + df[[target_column]].values + ) # used for predictions + + # Format categorical scalers + categorical_inputs = utils.extract_cols_from_data_type( + DataTypes.CATEGORICAL, column_definitions, {InputTypes.ID, InputTypes.TIME} + ) + + categorical_scalers = {} + num_classes = [] + for col in categorical_inputs: + # Set all to str so that we don't have mixed integer/string columns + srs = df[col].apply(str) + categorical_scalers[col] = sklearn.preprocessing.LabelEncoder().fit(srs.values) + num_classes.append(srs.nunique()) + + # Set categorical scaler outputs + self._cat_scalers = categorical_scalers + self._num_classes_per_cat_input = num_classes + + def transform_inputs(self, df): + """Performs feature transformations. + + This includes both feature engineering, preprocessing and normalisation. + + Args: + df: Data frame to transform. + + Returns: + Transformed data frame. + + """ + output = df.copy() + + if self._real_scalers is None and self._cat_scalers is None: + raise ValueError("Scalers have not been set!") + + column_definitions = self.get_column_definition() + + real_inputs = utils.extract_cols_from_data_type( + DataTypes.REAL_VALUED, column_definitions, {InputTypes.ID, InputTypes.TIME} + ) + categorical_inputs = utils.extract_cols_from_data_type( + DataTypes.CATEGORICAL, column_definitions, {InputTypes.ID, InputTypes.TIME} + ) + + # Format real inputs + output[real_inputs] = self._real_scalers.transform(df[real_inputs].values) + + # Format categorical inputs + for col in categorical_inputs: + string_df = df[col].apply(str) + output[col] = self._cat_scalers[col].transform(string_df) + + return output + + def format_predictions(self, predictions): + """Reverts any normalisation to give predictions in original scale. + + Args: + predictions: Dataframe of model predictions. + + Returns: + Data frame of unnormalised predictions. + """ + output = predictions.copy() + + column_names = predictions.columns + + for col in column_names: + if col not in {"forecast_time", "identifier"}: + output[col] = self._target_scaler.inverse_transform(predictions[col]) + + return output + + # Default params + def get_fixed_params(self): + """Returns fixed model parameters for experiments.""" + + fixed_params = { + "total_time_steps": 16 + 6, + "num_encoder_steps": 16, + "num_epochs": 100, + "early_stopping_patience": 5, + "multiprocessing_workers": 5, + } + + return fixed_params + + def get_default_model_params(self): + """Returns default optimised model parameters.""" + + model_params = { + "dropout_rate": 0.3, + "hidden_layer_size": 160, + "learning_rate": 0.01, + "minibatch_size": 64, + "max_gradient_norm": 0.01, + "num_heads": 1, + "stack_size": 1, + } + + return model_params diff --git a/examples/benchmarks/TFT/data_formatters/traffic.py b/examples/benchmarks/TFT/data_formatters/traffic.py index 49401e5cc..ee8ef2e5d 100644 --- a/examples/benchmarks/TFT/data_formatters/traffic.py +++ b/examples/benchmarks/TFT/data_formatters/traffic.py @@ -1,117 +1,117 @@ -# coding=utf-8 -# Copyright 2020 The Google Research Authors. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# Lint as: python3 -"""Custom formatting functions for Traffic dataset. - -Defines dataset specific column definitions and data transformations. This also -performs z-score normalization across the entire dataset, hence re-uses most of -the same functions as volatility. -""" - -import data_formatters.base -import data_formatters.volatility - -VolatilityFormatter = data_formatters.volatility.VolatilityFormatter -DataTypes = data_formatters.base.DataTypes -InputTypes = data_formatters.base.InputTypes - - -class TrafficFormatter(VolatilityFormatter): - """Defines and formats data for the traffic dataset. - - This also performs z-score normalization across the entire dataset, hence - re-uses most of the same functions as volatility. - - Attributes: - column_definition: Defines input and data type of column used in the - experiment. - identifiers: Entity identifiers used in experiments. - """ - - _column_definition = [ - ('id', DataTypes.REAL_VALUED, InputTypes.ID), - ('hours_from_start', DataTypes.REAL_VALUED, InputTypes.TIME), - ('values', DataTypes.REAL_VALUED, InputTypes.TARGET), - ('time_on_day', DataTypes.REAL_VALUED, InputTypes.KNOWN_INPUT), - ('day_of_week', DataTypes.REAL_VALUED, InputTypes.KNOWN_INPUT), - ('hours_from_start', DataTypes.REAL_VALUED, InputTypes.KNOWN_INPUT), - ('categorical_id', DataTypes.CATEGORICAL, InputTypes.STATIC_INPUT), - ] - - def split_data(self, df, valid_boundary=151, test_boundary=166): - """Splits data frame into training-validation-test data frames. - - This also calibrates scaling object, and transforms data for each split. - - Args: - df: Source data frame to split. - valid_boundary: Starting year for validation data - test_boundary: Starting year for test data - - Returns: - Tuple of transformed (train, valid, test) data. - """ - - print('Formatting train-valid-test splits.') - - index = df['sensor_day'] - train = df.loc[index < valid_boundary] - valid = df.loc[(index >= valid_boundary - 7) & (index < test_boundary)] - test = df.loc[index >= test_boundary - 7] - - self.set_scalers(train) - - return (self.transform_inputs(data) for data in [train, valid, test]) - - # Default params - def get_fixed_params(self): - """Returns fixed model parameters for experiments.""" - - fixed_params = { - 'total_time_steps': 8 * 24, - 'num_encoder_steps': 7 * 24, - 'num_epochs': 100, - 'early_stopping_patience': 5, - 'multiprocessing_workers': 5 - } - - return fixed_params - - def get_default_model_params(self): - """Returns default optimised model parameters.""" - - model_params = { - 'dropout_rate': 0.3, - 'hidden_layer_size': 320, - 'learning_rate': 0.001, - 'minibatch_size': 128, - 'max_gradient_norm': 100., - 'num_heads': 4, - 'stack_size': 1 - } - - return model_params - - def get_num_samples_for_calibration(self): - """Gets the default number of training and validation samples. - - Use to sub-sample the data for network calibration and a value of -1 uses - all available samples. - - Returns: - Tuple of (training samples, validation samples) - """ - return 450000, 50000 +# coding=utf-8 +# Copyright 2020 The Google Research Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Lint as: python3 +"""Custom formatting functions for Traffic dataset. + +Defines dataset specific column definitions and data transformations. This also +performs z-score normalization across the entire dataset, hence re-uses most of +the same functions as volatility. +""" + +import data_formatters.base +import data_formatters.volatility + +VolatilityFormatter = data_formatters.volatility.VolatilityFormatter +DataTypes = data_formatters.base.DataTypes +InputTypes = data_formatters.base.InputTypes + + +class TrafficFormatter(VolatilityFormatter): + """Defines and formats data for the traffic dataset. + + This also performs z-score normalization across the entire dataset, hence + re-uses most of the same functions as volatility. + + Attributes: + column_definition: Defines input and data type of column used in the + experiment. + identifiers: Entity identifiers used in experiments. + """ + + _column_definition = [ + ("id", DataTypes.REAL_VALUED, InputTypes.ID), + ("hours_from_start", DataTypes.REAL_VALUED, InputTypes.TIME), + ("values", DataTypes.REAL_VALUED, InputTypes.TARGET), + ("time_on_day", DataTypes.REAL_VALUED, InputTypes.KNOWN_INPUT), + ("day_of_week", DataTypes.REAL_VALUED, InputTypes.KNOWN_INPUT), + ("hours_from_start", DataTypes.REAL_VALUED, InputTypes.KNOWN_INPUT), + ("categorical_id", DataTypes.CATEGORICAL, InputTypes.STATIC_INPUT), + ] + + def split_data(self, df, valid_boundary=151, test_boundary=166): + """Splits data frame into training-validation-test data frames. + + This also calibrates scaling object, and transforms data for each split. + + Args: + df: Source data frame to split. + valid_boundary: Starting year for validation data + test_boundary: Starting year for test data + + Returns: + Tuple of transformed (train, valid, test) data. + """ + + print("Formatting train-valid-test splits.") + + index = df["sensor_day"] + train = df.loc[index < valid_boundary] + valid = df.loc[(index >= valid_boundary - 7) & (index < test_boundary)] + test = df.loc[index >= test_boundary - 7] + + self.set_scalers(train) + + return (self.transform_inputs(data) for data in [train, valid, test]) + + # Default params + def get_fixed_params(self): + """Returns fixed model parameters for experiments.""" + + fixed_params = { + "total_time_steps": 8 * 24, + "num_encoder_steps": 7 * 24, + "num_epochs": 100, + "early_stopping_patience": 5, + "multiprocessing_workers": 5, + } + + return fixed_params + + def get_default_model_params(self): + """Returns default optimised model parameters.""" + + model_params = { + "dropout_rate": 0.3, + "hidden_layer_size": 320, + "learning_rate": 0.001, + "minibatch_size": 128, + "max_gradient_norm": 100.0, + "num_heads": 4, + "stack_size": 1, + } + + return model_params + + def get_num_samples_for_calibration(self): + """Gets the default number of training and validation samples. + + Use to sub-sample the data for network calibration and a value of -1 uses + all available samples. + + Returns: + Tuple of (training samples, validation samples) + """ + return 450000, 50000 diff --git a/examples/benchmarks/TFT/data_formatters/volatility.py b/examples/benchmarks/TFT/data_formatters/volatility.py index 37923a275..b3ddf09fd 100644 --- a/examples/benchmarks/TFT/data_formatters/volatility.py +++ b/examples/benchmarks/TFT/data_formatters/volatility.py @@ -1,214 +1,212 @@ -# coding=utf-8 -# Copyright 2020 The Google Research Authors. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# Lint as: python3 -"""Custom formatting functions for Volatility dataset. - -Defines dataset specific column definitions and data transformations. -""" - -import data_formatters.base -import libs.utils as utils -import sklearn.preprocessing - -GenericDataFormatter = data_formatters.base.GenericDataFormatter -DataTypes = data_formatters.base.DataTypes -InputTypes = data_formatters.base.InputTypes - - -class VolatilityFormatter(GenericDataFormatter): - """Defines and formats data for the volatility dataset. - - Attributes: - column_definition: Defines input and data type of column used in the - experiment. - identifiers: Entity identifiers used in experiments. - """ - - _column_definition = [ - ('Symbol', DataTypes.CATEGORICAL, InputTypes.ID), - ('date', DataTypes.DATE, InputTypes.TIME), - ('log_vol', DataTypes.REAL_VALUED, InputTypes.TARGET), - ('open_to_close', DataTypes.REAL_VALUED, InputTypes.OBSERVED_INPUT), - ('days_from_start', DataTypes.REAL_VALUED, InputTypes.KNOWN_INPUT), - ('day_of_week', DataTypes.CATEGORICAL, InputTypes.KNOWN_INPUT), - ('day_of_month', DataTypes.CATEGORICAL, InputTypes.KNOWN_INPUT), - ('week_of_year', DataTypes.CATEGORICAL, InputTypes.KNOWN_INPUT), - ('month', DataTypes.CATEGORICAL, InputTypes.KNOWN_INPUT), - ('Region', DataTypes.CATEGORICAL, InputTypes.STATIC_INPUT), - ] - - def __init__(self): - """Initialises formatter.""" - - self.identifiers = None - self._real_scalers = None - self._cat_scalers = None - self._target_scaler = None - self._num_classes_per_cat_input = None - - def split_data(self, df, valid_boundary=2016, test_boundary=2018): - """Splits data frame into training-validation-test data frames. - - This also calibrates scaling object, and transforms data for each split. - - Args: - df: Source data frame to split. - valid_boundary: Starting year for validation data - test_boundary: Starting year for test data - - Returns: - Tuple of transformed (train, valid, test) data. - """ - - print('Formatting train-valid-test splits.') - - index = df['year'] - train = df.loc[index < valid_boundary] - valid = df.loc[(index >= valid_boundary) & (index < test_boundary)] - test = df.loc[index >= test_boundary] - - self.set_scalers(train) - - return (self.transform_inputs(data) for data in [train, valid, test]) - - def set_scalers(self, df): - """Calibrates scalers using the data supplied. - - Args: - df: Data to use to calibrate scalers. - """ - print('Setting scalers with training data...') - - column_definitions = self.get_column_definition() - id_column = utils.get_single_col_by_input_type(InputTypes.ID, - column_definitions) - target_column = utils.get_single_col_by_input_type(InputTypes.TARGET, - column_definitions) - - # Extract identifiers in case required - self.identifiers = list(df[id_column].unique()) - - # Format real scalers - real_inputs = utils.extract_cols_from_data_type( - DataTypes.REAL_VALUED, column_definitions, - {InputTypes.ID, InputTypes.TIME}) - - data = df[real_inputs].values - self._real_scalers = sklearn.preprocessing.StandardScaler().fit(data) - self._target_scaler = sklearn.preprocessing.StandardScaler().fit( - df[[target_column]].values) # used for predictions - - # Format categorical scalers - categorical_inputs = utils.extract_cols_from_data_type( - DataTypes.CATEGORICAL, column_definitions, - {InputTypes.ID, InputTypes.TIME}) - - categorical_scalers = {} - num_classes = [] - for col in categorical_inputs: - # Set all to str so that we don't have mixed integer/string columns - srs = df[col].apply(str) - categorical_scalers[col] = sklearn.preprocessing.LabelEncoder().fit( - srs.values) - num_classes.append(srs.nunique()) - - # Set categorical scaler outputs - self._cat_scalers = categorical_scalers - self._num_classes_per_cat_input = num_classes - - def transform_inputs(self, df): - """Performs feature transformations. - - This includes both feature engineering, preprocessing and normalisation. - - Args: - df: Data frame to transform. - - Returns: - Transformed data frame. - - """ - output = df.copy() - - if self._real_scalers is None and self._cat_scalers is None: - raise ValueError('Scalers have not been set!') - - column_definitions = self.get_column_definition() - - real_inputs = utils.extract_cols_from_data_type( - DataTypes.REAL_VALUED, column_definitions, - {InputTypes.ID, InputTypes.TIME}) - categorical_inputs = utils.extract_cols_from_data_type( - DataTypes.CATEGORICAL, column_definitions, - {InputTypes.ID, InputTypes.TIME}) - - # Format real inputs - output[real_inputs] = self._real_scalers.transform(df[real_inputs].values) - - # Format categorical inputs - for col in categorical_inputs: - string_df = df[col].apply(str) - output[col] = self._cat_scalers[col].transform(string_df) - - return output - - def format_predictions(self, predictions): - """Reverts any normalisation to give predictions in original scale. - - Args: - predictions: Dataframe of model predictions. - - Returns: - Data frame of unnormalised predictions. - """ - output = predictions.copy() - - column_names = predictions.columns - - for col in column_names: - if col not in {'forecast_time', 'identifier'}: - output[col] = self._target_scaler.inverse_transform(predictions[col]) - - return output - - # Default params - def get_fixed_params(self): - """Returns fixed model parameters for experiments.""" - - fixed_params = { - 'total_time_steps': 252 + 5, - 'num_encoder_steps': 252, - 'num_epochs': 100, - 'early_stopping_patience': 5, - 'multiprocessing_workers': 5, - } - - return fixed_params - - def get_default_model_params(self): - """Returns default optimised model parameters.""" - - model_params = { - 'dropout_rate': 0.3, - 'hidden_layer_size': 160, - 'learning_rate': 0.01, - 'minibatch_size': 64, - 'max_gradient_norm': 0.01, - 'num_heads': 1, - 'stack_size': 1 - } - - return model_params +# coding=utf-8 +# Copyright 2020 The Google Research Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Lint as: python3 +"""Custom formatting functions for Volatility dataset. + +Defines dataset specific column definitions and data transformations. +""" + +import data_formatters.base +import libs.utils as utils +import sklearn.preprocessing + +GenericDataFormatter = data_formatters.base.GenericDataFormatter +DataTypes = data_formatters.base.DataTypes +InputTypes = data_formatters.base.InputTypes + + +class VolatilityFormatter(GenericDataFormatter): + """Defines and formats data for the volatility dataset. + + Attributes: + column_definition: Defines input and data type of column used in the + experiment. + identifiers: Entity identifiers used in experiments. + """ + + _column_definition = [ + ("Symbol", DataTypes.CATEGORICAL, InputTypes.ID), + ("date", DataTypes.DATE, InputTypes.TIME), + ("log_vol", DataTypes.REAL_VALUED, InputTypes.TARGET), + ("open_to_close", DataTypes.REAL_VALUED, InputTypes.OBSERVED_INPUT), + ("days_from_start", DataTypes.REAL_VALUED, InputTypes.KNOWN_INPUT), + ("day_of_week", DataTypes.CATEGORICAL, InputTypes.KNOWN_INPUT), + ("day_of_month", DataTypes.CATEGORICAL, InputTypes.KNOWN_INPUT), + ("week_of_year", DataTypes.CATEGORICAL, InputTypes.KNOWN_INPUT), + ("month", DataTypes.CATEGORICAL, InputTypes.KNOWN_INPUT), + ("Region", DataTypes.CATEGORICAL, InputTypes.STATIC_INPUT), + ] + + def __init__(self): + """Initialises formatter.""" + + self.identifiers = None + self._real_scalers = None + self._cat_scalers = None + self._target_scaler = None + self._num_classes_per_cat_input = None + + def split_data(self, df, valid_boundary=2016, test_boundary=2018): + """Splits data frame into training-validation-test data frames. + + This also calibrates scaling object, and transforms data for each split. + + Args: + df: Source data frame to split. + valid_boundary: Starting year for validation data + test_boundary: Starting year for test data + + Returns: + Tuple of transformed (train, valid, test) data. + """ + + print("Formatting train-valid-test splits.") + + index = df["year"] + train = df.loc[index < valid_boundary] + valid = df.loc[(index >= valid_boundary) & (index < test_boundary)] + test = df.loc[index >= test_boundary] + + self.set_scalers(train) + + return (self.transform_inputs(data) for data in [train, valid, test]) + + def set_scalers(self, df): + """Calibrates scalers using the data supplied. + + Args: + df: Data to use to calibrate scalers. + """ + print("Setting scalers with training data...") + + column_definitions = self.get_column_definition() + id_column = utils.get_single_col_by_input_type(InputTypes.ID, column_definitions) + target_column = utils.get_single_col_by_input_type(InputTypes.TARGET, column_definitions) + + # Extract identifiers in case required + self.identifiers = list(df[id_column].unique()) + + # Format real scalers + real_inputs = utils.extract_cols_from_data_type( + DataTypes.REAL_VALUED, column_definitions, {InputTypes.ID, InputTypes.TIME} + ) + + data = df[real_inputs].values + self._real_scalers = sklearn.preprocessing.StandardScaler().fit(data) + self._target_scaler = sklearn.preprocessing.StandardScaler().fit( + df[[target_column]].values + ) # used for predictions + + # Format categorical scalers + categorical_inputs = utils.extract_cols_from_data_type( + DataTypes.CATEGORICAL, column_definitions, {InputTypes.ID, InputTypes.TIME} + ) + + categorical_scalers = {} + num_classes = [] + for col in categorical_inputs: + # Set all to str so that we don't have mixed integer/string columns + srs = df[col].apply(str) + categorical_scalers[col] = sklearn.preprocessing.LabelEncoder().fit(srs.values) + num_classes.append(srs.nunique()) + + # Set categorical scaler outputs + self._cat_scalers = categorical_scalers + self._num_classes_per_cat_input = num_classes + + def transform_inputs(self, df): + """Performs feature transformations. + + This includes both feature engineering, preprocessing and normalisation. + + Args: + df: Data frame to transform. + + Returns: + Transformed data frame. + + """ + output = df.copy() + + if self._real_scalers is None and self._cat_scalers is None: + raise ValueError("Scalers have not been set!") + + column_definitions = self.get_column_definition() + + real_inputs = utils.extract_cols_from_data_type( + DataTypes.REAL_VALUED, column_definitions, {InputTypes.ID, InputTypes.TIME} + ) + categorical_inputs = utils.extract_cols_from_data_type( + DataTypes.CATEGORICAL, column_definitions, {InputTypes.ID, InputTypes.TIME} + ) + + # Format real inputs + output[real_inputs] = self._real_scalers.transform(df[real_inputs].values) + + # Format categorical inputs + for col in categorical_inputs: + string_df = df[col].apply(str) + output[col] = self._cat_scalers[col].transform(string_df) + + return output + + def format_predictions(self, predictions): + """Reverts any normalisation to give predictions in original scale. + + Args: + predictions: Dataframe of model predictions. + + Returns: + Data frame of unnormalised predictions. + """ + output = predictions.copy() + + column_names = predictions.columns + + for col in column_names: + if col not in {"forecast_time", "identifier"}: + output[col] = self._target_scaler.inverse_transform(predictions[col]) + + return output + + # Default params + def get_fixed_params(self): + """Returns fixed model parameters for experiments.""" + + fixed_params = { + "total_time_steps": 252 + 5, + "num_encoder_steps": 252, + "num_epochs": 100, + "early_stopping_patience": 5, + "multiprocessing_workers": 5, + } + + return fixed_params + + def get_default_model_params(self): + """Returns default optimised model parameters.""" + + model_params = { + "dropout_rate": 0.3, + "hidden_layer_size": 160, + "learning_rate": 0.01, + "minibatch_size": 64, + "max_gradient_norm": 0.01, + "num_heads": 1, + "stack_size": 1, + } + + return model_params diff --git a/examples/benchmarks/TFT/expt_settings/__init__.py b/examples/benchmarks/TFT/expt_settings/__init__.py index 9a1980462..87ec3284f 100644 --- a/examples/benchmarks/TFT/expt_settings/__init__.py +++ b/examples/benchmarks/TFT/expt_settings/__init__.py @@ -1,15 +1,14 @@ -# coding=utf-8 -# Copyright 2020 The Google Research Authors. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - +# coding=utf-8 +# Copyright 2020 The Google Research Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/examples/benchmarks/TFT/expt_settings/configs.py b/examples/benchmarks/TFT/expt_settings/configs.py index d28a39bb0..d1891a002 100644 --- a/examples/benchmarks/TFT/expt_settings/configs.py +++ b/examples/benchmarks/TFT/expt_settings/configs.py @@ -1,111 +1,107 @@ -# coding=utf-8 -# Copyright 2020 The Google Research Authors. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# Lint as: python3 -"""Default configs for TFT experiments. - -Contains the default output paths for data, serialised models and predictions -for the main experiments used in the publication. -""" - -import os - -import data_formatters.electricity -import data_formatters.favorita -import data_formatters.traffic -import data_formatters.volatility -import data_formatters.qlib_Alpha158 - - -class ExperimentConfig(object): - """Defines experiment configs and paths to outputs. - - Attributes: - root_folder: Root folder to contain all experimental outputs. - experiment: Name of experiment to run. - data_folder: Folder to store data for experiment. - model_folder: Folder to store serialised models. - results_folder: Folder to store results. - data_csv_path: Path to primary data csv file used in experiment. - hyperparam_iterations: Default number of random search iterations for - experiment. - """ - - default_experiments = ['volatility', 'electricity', 'traffic', 'favorita', 'Alpha158'] - - def __init__(self, experiment='volatility', root_folder=None): - """Creates configs based on default experiment chosen. - - Args: - experiment: Name of experiment. - root_folder: Root folder to save all outputs of training. - """ - - if experiment not in self.default_experiments: - raise ValueError('Unrecognised experiment={}'.format(experiment)) - - # Defines all relevant paths - if root_folder is None: - root_folder = os.path.join( - os.path.dirname(os.path.realpath(__file__)), '..', 'outputs') - print('Using root folder {}'.format(root_folder)) - - self.root_folder = root_folder - self.experiment = experiment - self.data_folder = os.path.join(root_folder, 'data', experiment) - self.model_folder = os.path.join(root_folder, 'saved_models', experiment) - self.results_folder = os.path.join(root_folder, 'results', experiment) - - # Creates folders if they don't exist - for relevant_directory in [ - self.root_folder, self.data_folder, self.model_folder, - self.results_folder - ]: - if not os.path.exists(relevant_directory): - os.makedirs(relevant_directory) - - @property - def data_csv_path(self): - csv_map = { - 'volatility': 'formatted_omi_vol.csv', - 'electricity': 'hourly_electricity.csv', - 'traffic': 'hourly_data.csv', - 'favorita': 'favorita_consolidated.csv', - 'Alpha158': 'Alpha158.csv', - } - - return os.path.join(self.data_folder, csv_map[self.experiment]) - - @property - def hyperparam_iterations(self): - - return 240 if self.experiment == 'volatility' else 60 - - def make_data_formatter(self): - """Gets a data formatter object for experiment. - - Returns: - Default DataFormatter per experiment. - """ - - data_formatter_class = { - 'volatility': data_formatters.volatility.VolatilityFormatter, - 'electricity': data_formatters.electricity.ElectricityFormatter, - 'traffic': data_formatters.traffic.TrafficFormatter, - 'favorita': data_formatters.favorita.FavoritaFormatter, - 'Alpha158': data_formatters.qlib_Alpha158.Alpha158Formatter, - } - - return data_formatter_class[self.experiment]() +# coding=utf-8 +# Copyright 2020 The Google Research Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Lint as: python3 +"""Default configs for TFT experiments. + +Contains the default output paths for data, serialised models and predictions +for the main experiments used in the publication. +""" + +import os + +import data_formatters.electricity +import data_formatters.favorita +import data_formatters.traffic +import data_formatters.volatility +import data_formatters.qlib_Alpha158 + + +class ExperimentConfig(object): + """Defines experiment configs and paths to outputs. + + Attributes: + root_folder: Root folder to contain all experimental outputs. + experiment: Name of experiment to run. + data_folder: Folder to store data for experiment. + model_folder: Folder to store serialised models. + results_folder: Folder to store results. + data_csv_path: Path to primary data csv file used in experiment. + hyperparam_iterations: Default number of random search iterations for + experiment. + """ + + default_experiments = ["volatility", "electricity", "traffic", "favorita", "Alpha158"] + + def __init__(self, experiment="volatility", root_folder=None): + """Creates configs based on default experiment chosen. + + Args: + experiment: Name of experiment. + root_folder: Root folder to save all outputs of training. + """ + + if experiment not in self.default_experiments: + raise ValueError("Unrecognised experiment={}".format(experiment)) + + # Defines all relevant paths + if root_folder is None: + root_folder = os.path.join(os.path.dirname(os.path.realpath(__file__)), "..", "outputs") + print("Using root folder {}".format(root_folder)) + + self.root_folder = root_folder + self.experiment = experiment + self.data_folder = os.path.join(root_folder, "data", experiment) + self.model_folder = os.path.join(root_folder, "saved_models", experiment) + self.results_folder = os.path.join(root_folder, "results", experiment) + + # Creates folders if they don't exist + for relevant_directory in [self.root_folder, self.data_folder, self.model_folder, self.results_folder]: + if not os.path.exists(relevant_directory): + os.makedirs(relevant_directory) + + @property + def data_csv_path(self): + csv_map = { + "volatility": "formatted_omi_vol.csv", + "electricity": "hourly_electricity.csv", + "traffic": "hourly_data.csv", + "favorita": "favorita_consolidated.csv", + "Alpha158": "Alpha158.csv", + } + + return os.path.join(self.data_folder, csv_map[self.experiment]) + + @property + def hyperparam_iterations(self): + + return 240 if self.experiment == "volatility" else 60 + + def make_data_formatter(self): + """Gets a data formatter object for experiment. + + Returns: + Default DataFormatter per experiment. + """ + + data_formatter_class = { + "volatility": data_formatters.volatility.VolatilityFormatter, + "electricity": data_formatters.electricity.ElectricityFormatter, + "traffic": data_formatters.traffic.TrafficFormatter, + "favorita": data_formatters.favorita.FavoritaFormatter, + "Alpha158": data_formatters.qlib_Alpha158.Alpha158Formatter, + } + + return data_formatter_class[self.experiment]() diff --git a/examples/benchmarks/TFT/libs/__init__.py b/examples/benchmarks/TFT/libs/__init__.py index 9a1980462..87ec3284f 100644 --- a/examples/benchmarks/TFT/libs/__init__.py +++ b/examples/benchmarks/TFT/libs/__init__.py @@ -1,15 +1,14 @@ -# coding=utf-8 -# Copyright 2020 The Google Research Authors. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - +# coding=utf-8 +# Copyright 2020 The Google Research Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/examples/benchmarks/TFT/libs/hyperparam_opt.py b/examples/benchmarks/TFT/libs/hyperparam_opt.py index c9bc19e7c..750fdf2c1 100644 --- a/examples/benchmarks/TFT/libs/hyperparam_opt.py +++ b/examples/benchmarks/TFT/libs/hyperparam_opt.py @@ -1,438 +1,430 @@ -# coding=utf-8 -# Copyright 2020 The Google Research Authors. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# Lint as: python3 -"""Classes used for hyperparameter optimisation. - -Two main classes exist: -1) HyperparamOptManager used for optimisation on a single machine/GPU. -2) DistributedHyperparamOptManager for multiple GPUs on different machines. -""" - -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function - -import collections -import os -import shutil -import libs.utils as utils -import numpy as np -import pandas as pd - -Deque = collections.deque - - -class HyperparamOptManager: - """Manages hyperparameter optimisation using random search for a single GPU. - - Attributes: - param_ranges: Discrete hyperparameter range for random search. - results: Dataframe of validation results. - fixed_params: Fixed model parameters per experiment. - saved_params: Dataframe of parameters trained. - best_score: Minimum validation loss observed thus far. - optimal_name: Key to best configuration. - hyperparam_folder: Where to save optimisation outputs. - """ - - def __init__(self, - param_ranges, - fixed_params, - model_folder, - override_w_fixed_params=True): - """Instantiates model. - - Args: - param_ranges: Discrete hyperparameter range for random search. - fixed_params: Fixed model parameters per experiment. - model_folder: Folder to store optimisation artifacts. - override_w_fixed_params: Whether to override serialsed fixed model - parameters with new supplied values. - """ - - self.param_ranges = param_ranges - - self._max_tries = 1000 - self.results = pd.DataFrame() - self.fixed_params = fixed_params - self.saved_params = pd.DataFrame() - - self.best_score = np.Inf - self.optimal_name = "" - - # Setup - # Create folder for saving if its not there - self.hyperparam_folder = model_folder - utils.create_folder_if_not_exist(self.hyperparam_folder) - - self._override_w_fixed_params = override_w_fixed_params - - def load_results(self): - """Loads results from previous hyperparameter optimisation. - - Returns: - A boolean indicating if previous results can be loaded. - """ - print("Loading results from", self.hyperparam_folder) - - results_file = os.path.join(self.hyperparam_folder, "results.csv") - params_file = os.path.join(self.hyperparam_folder, "params.csv") - - if os.path.exists(results_file) and os.path.exists(params_file): - - self.results = pd.read_csv(results_file, index_col=0) - self.saved_params = pd.read_csv(params_file, index_col=0) - - if not self.results.empty: - self.results.at["loss"] = self.results.loc["loss"].apply(float) - self.best_score = self.results.loc["loss"].min() - - is_optimal = self.results.loc["loss"] == self.best_score - self.optimal_name = self.results.T[is_optimal].index[0] - - return True - - return False - - def _get_params_from_name(self, name): - """Returns previously saved parameters given a key.""" - params = self.saved_params - - selected_params = dict(params[name]) - - if self._override_w_fixed_params: - for k in self.fixed_params: - selected_params[k] = self.fixed_params[k] - - return selected_params - - def get_best_params(self): - """Returns the optimal hyperparameters thus far.""" - - optimal_name = self.optimal_name - - return self._get_params_from_name(optimal_name) - - def clear(self): - """Clears all previous results and saved parameters.""" - shutil.rmtree(self.hyperparam_folder) - os.makedirs(self.hyperparam_folder) - self.results = pd.DataFrame() - self.saved_params = pd.DataFrame() - - def _check_params(self, params): - """Checks that parameter map is properly defined.""" - - valid_fields = list(self.param_ranges.keys()) + list( - self.fixed_params.keys()) - invalid_fields = [k for k in params if k not in valid_fields] - missing_fields = [k for k in valid_fields if k not in params] - - if invalid_fields: - raise ValueError("Invalid Fields Found {} - Valid ones are {}".format( - invalid_fields, valid_fields)) - if missing_fields: - raise ValueError("Missing Fields Found {} - Valid ones are {}".format( - missing_fields, valid_fields)) - - def _get_name(self, params): - """Returns a unique key for the supplied set of params.""" - - self._check_params(params) - - fields = list(params.keys()) - fields.sort() - - return "_".join([str(params[k]) for k in fields]) - - def get_next_parameters(self, ranges_to_skip=None): - """Returns the next set of parameters to optimise. - - Args: - ranges_to_skip: Explicitly defines a set of keys to skip. - """ - if ranges_to_skip is None: - ranges_to_skip = set(self.results.index) - - if not isinstance(self.param_ranges, dict): - raise ValueError("Only works for random search!") - - param_range_keys = list(self.param_ranges.keys()) - param_range_keys.sort() - - def _get_next(): - """Returns next hyperparameter set per try.""" - - parameters = { - k: np.random.choice(self.param_ranges[k]) for k in param_range_keys - } - - # Adds fixed params - for k in self.fixed_params: - parameters[k] = self.fixed_params[k] - - return parameters - - for _ in range(self._max_tries): - - parameters = _get_next() - name = self._get_name(parameters) - - if name not in ranges_to_skip: - return parameters - - raise ValueError("Exceeded max number of hyperparameter searches!!") - - def update_score(self, parameters, loss, model, info=""): - """Updates the results from last optimisation run. - - Args: - parameters: Hyperparameters used in optimisation. - loss: Validation loss obtained. - model: Model to serialised if required. - info: Any ancillary information to tag on to results. - - Returns: - Boolean flag indicating if the model is the best seen so far. - """ - - if np.isnan(loss): - loss = np.Inf - - if not os.path.isdir(self.hyperparam_folder): - os.makedirs(self.hyperparam_folder) - - name = self._get_name(parameters) - - is_optimal = self.results.empty or loss < self.best_score - - # save the first model - if is_optimal: - # Try saving first, before updating info - if model is not None: - print("Optimal model found, updating") - model.save(self.hyperparam_folder) - self.best_score = loss - self.optimal_name = name - - self.results[name] = pd.Series({"loss": loss, "info": info}) - self.saved_params[name] = pd.Series(parameters) - - self.results.to_csv(os.path.join(self.hyperparam_folder, "results.csv")) - self.saved_params.to_csv(os.path.join(self.hyperparam_folder, "params.csv")) - - return is_optimal - - -class DistributedHyperparamOptManager(HyperparamOptManager): - """Manages distributed hyperparameter optimisation across many gpus.""" - - def __init__(self, - param_ranges, - fixed_params, - root_model_folder, - worker_number, - search_iterations=1000, - num_iterations_per_worker=5, - clear_serialised_params=False): - """Instantiates optimisation manager. - - This hyperparameter optimisation pre-generates #search_iterations - hyperparameter combinations and serialises them - at the start. At runtime, each worker goes through their own set of - parameter ranges. The pregeneration - allows for multiple workers to run in parallel on different machines without - resulting in parameter overlaps. - - Args: - param_ranges: Discrete hyperparameter range for random search. - fixed_params: Fixed model parameters per experiment. - root_model_folder: Folder to store optimisation artifacts. - worker_number: Worker index definining which set of hyperparameters to - test. - search_iterations: Maximum numer of random search iterations. - num_iterations_per_worker: How many iterations are handled per worker. - clear_serialised_params: Whether to regenerate hyperparameter - combinations. - """ - - max_workers = int(np.ceil(search_iterations / num_iterations_per_worker)) - - # Sanity checks - if worker_number > max_workers: - raise ValueError( - "Worker number ({}) cannot be larger than the total number of workers!" - .format(max_workers)) - if worker_number > search_iterations: - raise ValueError( - "Worker number ({}) cannot be larger than the max search iterations ({})!" - .format(worker_number, search_iterations)) - - print("*** Creating hyperparameter manager for worker {} ***".format( - worker_number)) - - hyperparam_folder = os.path.join(root_model_folder, str(worker_number)) - super().__init__( - param_ranges, - fixed_params, - hyperparam_folder, - override_w_fixed_params=True) - - serialised_ranges_folder = os.path.join(root_model_folder, "hyperparams") - if clear_serialised_params: - print("Regenerating hyperparameter list") - if os.path.exists(serialised_ranges_folder): - shutil.rmtree(serialised_ranges_folder) - - utils.create_folder_if_not_exist(serialised_ranges_folder) - - self.serialised_ranges_path = os.path.join( - serialised_ranges_folder, "ranges_{}.csv".format(search_iterations)) - self.hyperparam_folder = hyperparam_folder # override - self.worker_num = worker_number - self.total_search_iterations = search_iterations - self.num_iterations_per_worker = num_iterations_per_worker - self.global_hyperparam_df = self.load_serialised_hyperparam_df() - self.worker_search_queue = self._get_worker_search_queue() - - @property - def optimisation_completed(self): - return False if self.worker_search_queue else True - - def get_next_parameters(self): - """Returns next dictionary of hyperparameters to optimise.""" - param_name = self.worker_search_queue.pop() - - params = self.global_hyperparam_df.loc[param_name, :].to_dict() - - # Always override! - for k in self.fixed_params: - print("Overriding saved {}: {}".format(k, self.fixed_params[k])) - - params[k] = self.fixed_params[k] - - return params - - def load_serialised_hyperparam_df(self): - """Loads serialsed hyperparameter ranges from file. - - Returns: - DataFrame containing hyperparameter combinations. - """ - print("Loading params for {} search iterations form {}".format( - self.total_search_iterations, self.serialised_ranges_path)) - - if os.path.exists(self.serialised_ranges_folder): - df = pd.read_csv(self.serialised_ranges_path, index_col=0) - else: - print("Unable to load - regenerating serach ranges instead") - df = self.update_serialised_hyperparam_df() - - return df - - def update_serialised_hyperparam_df(self): - """Regenerates hyperparameter combinations and saves to file. - - Returns: - DataFrame containing hyperparameter combinations. - """ - search_df = self._generate_full_hyperparam_df() - - print("Serialising params for {} search iterations to {}".format( - self.total_search_iterations, self.serialised_ranges_path)) - - search_df.to_csv(self.serialised_ranges_path) - - return search_df - - def _generate_full_hyperparam_df(self): - """Generates actual hyperparameter combinations. - - Returns: - DataFrame containing hyperparameter combinations. - """ - - np.random.seed(131) # for reproducibility of hyperparam list - - name_list = [] - param_list = [] - for _ in range(self.total_search_iterations): - params = super().get_next_parameters(name_list) - - name = self._get_name(params) - - name_list.append(name) - param_list.append(params) - - full_search_df = pd.DataFrame(param_list, index=name_list) - - return full_search_df - - def clear(self): # reset when cleared - """Clears results for hyperparameter manager and resets.""" - super().clear() - self.worker_search_queue = self._get_worker_search_queue() - - def load_results(self): - """Load results from file and queue parameter combinations to try. - - Returns: - Boolean indicating if results were successfully loaded. - """ - success = super().load_results() - - if success: - self.worker_search_queue = self._get_worker_search_queue() - - return success - - def _get_worker_search_queue(self): - """Generates the queue of param combinations for current worker. - - Returns: - Queue of hyperparameter combinations outstanding. - """ - global_df = self.assign_worker_numbers(self.global_hyperparam_df) - worker_df = global_df[global_df["worker"] == self.worker_num] - - left_overs = [s for s in worker_df.index if s not in self.results.columns] - - return Deque(left_overs) - - def assign_worker_numbers(self, df): - """Updates parameter combinations with the index of the worker used. - - Args: - df: DataFrame of parameter combinations. - - Returns: - Updated DataFrame with worker number. - """ - output = df.copy() - - n = self.total_search_iterations - batch_size = self.num_iterations_per_worker - - max_worker_num = int(np.ceil(n / batch_size)) - - worker_idx = np.concatenate([ - np.tile(i + 1, self.num_iterations_per_worker) - for i in range(max_worker_num) - ]) - - output["worker"] = worker_idx[:len(output)] - - return output +# coding=utf-8 +# Copyright 2020 The Google Research Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Lint as: python3 +"""Classes used for hyperparameter optimisation. + +Two main classes exist: +1) HyperparamOptManager used for optimisation on a single machine/GPU. +2) DistributedHyperparamOptManager for multiple GPUs on different machines. +""" + +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +import collections +import os +import shutil +import libs.utils as utils +import numpy as np +import pandas as pd + +Deque = collections.deque + + +class HyperparamOptManager: + """Manages hyperparameter optimisation using random search for a single GPU. + + Attributes: + param_ranges: Discrete hyperparameter range for random search. + results: Dataframe of validation results. + fixed_params: Fixed model parameters per experiment. + saved_params: Dataframe of parameters trained. + best_score: Minimum validation loss observed thus far. + optimal_name: Key to best configuration. + hyperparam_folder: Where to save optimisation outputs. + """ + + def __init__(self, param_ranges, fixed_params, model_folder, override_w_fixed_params=True): + """Instantiates model. + + Args: + param_ranges: Discrete hyperparameter range for random search. + fixed_params: Fixed model parameters per experiment. + model_folder: Folder to store optimisation artifacts. + override_w_fixed_params: Whether to override serialsed fixed model + parameters with new supplied values. + """ + + self.param_ranges = param_ranges + + self._max_tries = 1000 + self.results = pd.DataFrame() + self.fixed_params = fixed_params + self.saved_params = pd.DataFrame() + + self.best_score = np.Inf + self.optimal_name = "" + + # Setup + # Create folder for saving if its not there + self.hyperparam_folder = model_folder + utils.create_folder_if_not_exist(self.hyperparam_folder) + + self._override_w_fixed_params = override_w_fixed_params + + def load_results(self): + """Loads results from previous hyperparameter optimisation. + + Returns: + A boolean indicating if previous results can be loaded. + """ + print("Loading results from", self.hyperparam_folder) + + results_file = os.path.join(self.hyperparam_folder, "results.csv") + params_file = os.path.join(self.hyperparam_folder, "params.csv") + + if os.path.exists(results_file) and os.path.exists(params_file): + + self.results = pd.read_csv(results_file, index_col=0) + self.saved_params = pd.read_csv(params_file, index_col=0) + + if not self.results.empty: + self.results.at["loss"] = self.results.loc["loss"].apply(float) + self.best_score = self.results.loc["loss"].min() + + is_optimal = self.results.loc["loss"] == self.best_score + self.optimal_name = self.results.T[is_optimal].index[0] + + return True + + return False + + def _get_params_from_name(self, name): + """Returns previously saved parameters given a key.""" + params = self.saved_params + + selected_params = dict(params[name]) + + if self._override_w_fixed_params: + for k in self.fixed_params: + selected_params[k] = self.fixed_params[k] + + return selected_params + + def get_best_params(self): + """Returns the optimal hyperparameters thus far.""" + + optimal_name = self.optimal_name + + return self._get_params_from_name(optimal_name) + + def clear(self): + """Clears all previous results and saved parameters.""" + shutil.rmtree(self.hyperparam_folder) + os.makedirs(self.hyperparam_folder) + self.results = pd.DataFrame() + self.saved_params = pd.DataFrame() + + def _check_params(self, params): + """Checks that parameter map is properly defined.""" + + valid_fields = list(self.param_ranges.keys()) + list(self.fixed_params.keys()) + invalid_fields = [k for k in params if k not in valid_fields] + missing_fields = [k for k in valid_fields if k not in params] + + if invalid_fields: + raise ValueError("Invalid Fields Found {} - Valid ones are {}".format(invalid_fields, valid_fields)) + if missing_fields: + raise ValueError("Missing Fields Found {} - Valid ones are {}".format(missing_fields, valid_fields)) + + def _get_name(self, params): + """Returns a unique key for the supplied set of params.""" + + self._check_params(params) + + fields = list(params.keys()) + fields.sort() + + return "_".join([str(params[k]) for k in fields]) + + def get_next_parameters(self, ranges_to_skip=None): + """Returns the next set of parameters to optimise. + + Args: + ranges_to_skip: Explicitly defines a set of keys to skip. + """ + if ranges_to_skip is None: + ranges_to_skip = set(self.results.index) + + if not isinstance(self.param_ranges, dict): + raise ValueError("Only works for random search!") + + param_range_keys = list(self.param_ranges.keys()) + param_range_keys.sort() + + def _get_next(): + """Returns next hyperparameter set per try.""" + + parameters = {k: np.random.choice(self.param_ranges[k]) for k in param_range_keys} + + # Adds fixed params + for k in self.fixed_params: + parameters[k] = self.fixed_params[k] + + return parameters + + for _ in range(self._max_tries): + + parameters = _get_next() + name = self._get_name(parameters) + + if name not in ranges_to_skip: + return parameters + + raise ValueError("Exceeded max number of hyperparameter searches!!") + + def update_score(self, parameters, loss, model, info=""): + """Updates the results from last optimisation run. + + Args: + parameters: Hyperparameters used in optimisation. + loss: Validation loss obtained. + model: Model to serialised if required. + info: Any ancillary information to tag on to results. + + Returns: + Boolean flag indicating if the model is the best seen so far. + """ + + if np.isnan(loss): + loss = np.Inf + + if not os.path.isdir(self.hyperparam_folder): + os.makedirs(self.hyperparam_folder) + + name = self._get_name(parameters) + + is_optimal = self.results.empty or loss < self.best_score + + # save the first model + if is_optimal: + # Try saving first, before updating info + if model is not None: + print("Optimal model found, updating") + model.save(self.hyperparam_folder) + self.best_score = loss + self.optimal_name = name + + self.results[name] = pd.Series({"loss": loss, "info": info}) + self.saved_params[name] = pd.Series(parameters) + + self.results.to_csv(os.path.join(self.hyperparam_folder, "results.csv")) + self.saved_params.to_csv(os.path.join(self.hyperparam_folder, "params.csv")) + + return is_optimal + + +class DistributedHyperparamOptManager(HyperparamOptManager): + """Manages distributed hyperparameter optimisation across many gpus.""" + + def __init__( + self, + param_ranges, + fixed_params, + root_model_folder, + worker_number, + search_iterations=1000, + num_iterations_per_worker=5, + clear_serialised_params=False, + ): + """Instantiates optimisation manager. + + This hyperparameter optimisation pre-generates #search_iterations + hyperparameter combinations and serialises them + at the start. At runtime, each worker goes through their own set of + parameter ranges. The pregeneration + allows for multiple workers to run in parallel on different machines without + resulting in parameter overlaps. + + Args: + param_ranges: Discrete hyperparameter range for random search. + fixed_params: Fixed model parameters per experiment. + root_model_folder: Folder to store optimisation artifacts. + worker_number: Worker index definining which set of hyperparameters to + test. + search_iterations: Maximum numer of random search iterations. + num_iterations_per_worker: How many iterations are handled per worker. + clear_serialised_params: Whether to regenerate hyperparameter + combinations. + """ + + max_workers = int(np.ceil(search_iterations / num_iterations_per_worker)) + + # Sanity checks + if worker_number > max_workers: + raise ValueError( + "Worker number ({}) cannot be larger than the total number of workers!".format(max_workers) + ) + if worker_number > search_iterations: + raise ValueError( + "Worker number ({}) cannot be larger than the max search iterations ({})!".format( + worker_number, search_iterations + ) + ) + + print("*** Creating hyperparameter manager for worker {} ***".format(worker_number)) + + hyperparam_folder = os.path.join(root_model_folder, str(worker_number)) + super().__init__(param_ranges, fixed_params, hyperparam_folder, override_w_fixed_params=True) + + serialised_ranges_folder = os.path.join(root_model_folder, "hyperparams") + if clear_serialised_params: + print("Regenerating hyperparameter list") + if os.path.exists(serialised_ranges_folder): + shutil.rmtree(serialised_ranges_folder) + + utils.create_folder_if_not_exist(serialised_ranges_folder) + + self.serialised_ranges_path = os.path.join(serialised_ranges_folder, "ranges_{}.csv".format(search_iterations)) + self.hyperparam_folder = hyperparam_folder # override + self.worker_num = worker_number + self.total_search_iterations = search_iterations + self.num_iterations_per_worker = num_iterations_per_worker + self.global_hyperparam_df = self.load_serialised_hyperparam_df() + self.worker_search_queue = self._get_worker_search_queue() + + @property + def optimisation_completed(self): + return False if self.worker_search_queue else True + + def get_next_parameters(self): + """Returns next dictionary of hyperparameters to optimise.""" + param_name = self.worker_search_queue.pop() + + params = self.global_hyperparam_df.loc[param_name, :].to_dict() + + # Always override! + for k in self.fixed_params: + print("Overriding saved {}: {}".format(k, self.fixed_params[k])) + + params[k] = self.fixed_params[k] + + return params + + def load_serialised_hyperparam_df(self): + """Loads serialsed hyperparameter ranges from file. + + Returns: + DataFrame containing hyperparameter combinations. + """ + print( + "Loading params for {} search iterations form {}".format( + self.total_search_iterations, self.serialised_ranges_path + ) + ) + + if os.path.exists(self.serialised_ranges_folder): + df = pd.read_csv(self.serialised_ranges_path, index_col=0) + else: + print("Unable to load - regenerating serach ranges instead") + df = self.update_serialised_hyperparam_df() + + return df + + def update_serialised_hyperparam_df(self): + """Regenerates hyperparameter combinations and saves to file. + + Returns: + DataFrame containing hyperparameter combinations. + """ + search_df = self._generate_full_hyperparam_df() + + print( + "Serialising params for {} search iterations to {}".format( + self.total_search_iterations, self.serialised_ranges_path + ) + ) + + search_df.to_csv(self.serialised_ranges_path) + + return search_df + + def _generate_full_hyperparam_df(self): + """Generates actual hyperparameter combinations. + + Returns: + DataFrame containing hyperparameter combinations. + """ + + np.random.seed(131) # for reproducibility of hyperparam list + + name_list = [] + param_list = [] + for _ in range(self.total_search_iterations): + params = super().get_next_parameters(name_list) + + name = self._get_name(params) + + name_list.append(name) + param_list.append(params) + + full_search_df = pd.DataFrame(param_list, index=name_list) + + return full_search_df + + def clear(self): # reset when cleared + """Clears results for hyperparameter manager and resets.""" + super().clear() + self.worker_search_queue = self._get_worker_search_queue() + + def load_results(self): + """Load results from file and queue parameter combinations to try. + + Returns: + Boolean indicating if results were successfully loaded. + """ + success = super().load_results() + + if success: + self.worker_search_queue = self._get_worker_search_queue() + + return success + + def _get_worker_search_queue(self): + """Generates the queue of param combinations for current worker. + + Returns: + Queue of hyperparameter combinations outstanding. + """ + global_df = self.assign_worker_numbers(self.global_hyperparam_df) + worker_df = global_df[global_df["worker"] == self.worker_num] + + left_overs = [s for s in worker_df.index if s not in self.results.columns] + + return Deque(left_overs) + + def assign_worker_numbers(self, df): + """Updates parameter combinations with the index of the worker used. + + Args: + df: DataFrame of parameter combinations. + + Returns: + Updated DataFrame with worker number. + """ + output = df.copy() + + n = self.total_search_iterations + batch_size = self.num_iterations_per_worker + + max_worker_num = int(np.ceil(n / batch_size)) + + worker_idx = np.concatenate([np.tile(i + 1, self.num_iterations_per_worker) for i in range(max_worker_num)]) + + output["worker"] = worker_idx[: len(output)] + + return output diff --git a/examples/benchmarks/TFT/libs/tft_model.py b/examples/benchmarks/TFT/libs/tft_model.py index 2a41f4566..658bae60f 100644 --- a/examples/benchmarks/TFT/libs/tft_model.py +++ b/examples/benchmarks/TFT/libs/tft_model.py @@ -1,1391 +1,1280 @@ -# coding=utf-8 -# Copyright 2020 The Google Research Authors. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# Lint as: python3 -"""Temporal Fusion Transformer Model. - -Contains the full TFT architecture and associated components. Defines functions -for training, evaluation and prediction using simple Pandas Dataframe inputs. -""" - -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function - -import gc -import json -import os -import shutil - -import data_formatters.base -import libs.utils as utils -import numpy as np -import pandas as pd -import tensorflow as tf - -# Layer definitions. -concat = tf.keras.backend.concatenate -stack = tf.keras.backend.stack -K = tf.keras.backend -Add = tf.keras.layers.Add -LayerNorm = tf.keras.layers.LayerNormalization -Dense = tf.keras.layers.Dense -Multiply = tf.keras.layers.Multiply -Dropout = tf.keras.layers.Dropout -Activation = tf.keras.layers.Activation -Lambda = tf.keras.layers.Lambda - -# Default input types. -InputTypes = data_formatters.base.InputTypes - - -# Layer utility functions. -def linear_layer(size, - activation=None, - use_time_distributed=False, - use_bias=True): - """Returns simple Keras linear layer. - - Args: - size: Output size - activation: Activation function to apply if required - use_time_distributed: Whether to apply layer across time - use_bias: Whether bias should be included in layer - """ - linear = tf.keras.layers.Dense(size, activation=activation, use_bias=use_bias) - if use_time_distributed: - linear = tf.keras.layers.TimeDistributed(linear) - return linear - - -def apply_mlp(inputs, - hidden_size, - output_size, - output_activation=None, - hidden_activation='tanh', - use_time_distributed=False): - """Applies simple feed-forward network to an input. - - Args: - inputs: MLP inputs - hidden_size: Hidden state size - output_size: Output size of MLP - output_activation: Activation function to apply on output - hidden_activation: Activation function to apply on input - use_time_distributed: Whether to apply across time - - Returns: - Tensor for MLP outputs. - """ - if use_time_distributed: - hidden = tf.keras.layers.TimeDistributed( - tf.keras.layers.Dense(hidden_size, activation=hidden_activation))( - inputs) - return tf.keras.layers.TimeDistributed( - tf.keras.layers.Dense(output_size, activation=output_activation))( - hidden) - else: - hidden = tf.keras.layers.Dense( - hidden_size, activation=hidden_activation)( - inputs) - return tf.keras.layers.Dense( - output_size, activation=output_activation)( - hidden) - - -def apply_gating_layer(x, - hidden_layer_size, - dropout_rate=None, - use_time_distributed=True, - activation=None): - """Applies a Gated Linear Unit (GLU) to an input. - - Args: - x: Input to gating layer - hidden_layer_size: Dimension of GLU - dropout_rate: Dropout rate to apply if any - use_time_distributed: Whether to apply across time - activation: Activation function to apply to the linear feature transform if - necessary - - Returns: - Tuple of tensors for: (GLU output, gate) - """ - - if dropout_rate is not None: - x = tf.keras.layers.Dropout(dropout_rate)(x) - - if use_time_distributed: - activation_layer = tf.keras.layers.TimeDistributed( - tf.keras.layers.Dense(hidden_layer_size, activation=activation))( - x) - gated_layer = tf.keras.layers.TimeDistributed( - tf.keras.layers.Dense(hidden_layer_size, activation='sigmoid'))( - x) - else: - activation_layer = tf.keras.layers.Dense( - hidden_layer_size, activation=activation)( - x) - gated_layer = tf.keras.layers.Dense( - hidden_layer_size, activation='sigmoid')( - x) - - return tf.keras.layers.Multiply()([activation_layer, - gated_layer]), gated_layer - - -def add_and_norm(x_list): - """Applies skip connection followed by layer normalisation. - - Args: - x_list: List of inputs to sum for skip connection - - Returns: - Tensor output from layer. - """ - tmp = Add()(x_list) - tmp = LayerNorm()(tmp) - return tmp - - -def gated_residual_network(x, - hidden_layer_size, - output_size=None, - dropout_rate=None, - use_time_distributed=True, - additional_context=None, - return_gate=False): - """Applies the gated residual network (GRN) as defined in paper. - - Args: - x: Network inputs - hidden_layer_size: Internal state size - output_size: Size of output layer - dropout_rate: Dropout rate if dropout is applied - use_time_distributed: Whether to apply network across time dimension - additional_context: Additional context vector to use if relevant - return_gate: Whether to return GLU gate for diagnostic purposes - - Returns: - Tuple of tensors for: (GRN output, GLU gate) - """ - - # Setup skip connection - if output_size is None: - output_size = hidden_layer_size - skip = x - else: - linear = Dense(output_size) - if use_time_distributed: - linear = tf.keras.layers.TimeDistributed(linear) - skip = linear(x) - - # Apply feedforward network - hidden = linear_layer( - hidden_layer_size, - activation=None, - use_time_distributed=use_time_distributed)( - x) - if additional_context is not None: - hidden = hidden + linear_layer( - hidden_layer_size, - activation=None, - use_time_distributed=use_time_distributed, - use_bias=False)( - additional_context) - hidden = tf.keras.layers.Activation('elu')(hidden) - hidden = linear_layer( - hidden_layer_size, - activation=None, - use_time_distributed=use_time_distributed)( - hidden) - - gating_layer, gate = apply_gating_layer( - hidden, - output_size, - dropout_rate=dropout_rate, - use_time_distributed=use_time_distributed, - activation=None) - - if return_gate: - return add_and_norm([skip, gating_layer]), gate - else: - return add_and_norm([skip, gating_layer]) - - -# Attention Components. -def get_decoder_mask(self_attn_inputs): - """Returns causal mask to apply for self-attention layer. - - Args: - self_attn_inputs: Inputs to self attention layer to determine mask shape - """ - len_s = tf.shape(self_attn_inputs)[1] - bs = tf.shape(self_attn_inputs)[:1] - mask = K.cumsum(tf.eye(len_s, batch_shape=bs), 1) - return mask - - -class ScaledDotProductAttention(): - """Defines scaled dot product attention layer. - - Attributes: - dropout: Dropout rate to use - activation: Normalisation function for scaled dot product attention (e.g. - softmax by default) - """ - - def __init__(self, attn_dropout=0.0): - self.dropout = Dropout(attn_dropout) - self.activation = Activation('softmax') - - def __call__(self, q, k, v, mask): - """Applies scaled dot product attention. - - Args: - q: Queries - k: Keys - v: Values - mask: Masking if required -- sets softmax to very large value - - Returns: - Tuple of (layer outputs, attention weights) - """ - temper = tf.sqrt(tf.cast(tf.shape(k)[-1], dtype='float32')) - attn = Lambda(lambda x: K.batch_dot(x[0], x[1], axes=[2, 2]) / temper)( - [q, k]) # shape=(batch, q, k) - if mask is not None: - mmask = Lambda(lambda x: (-1e+9) * (1. - K.cast(x, 'float32')))( - mask) # setting to infinity - attn = Add()([attn, mmask]) - attn = self.activation(attn) - attn = self.dropout(attn) - output = Lambda(lambda x: K.batch_dot(x[0], x[1]))([attn, v]) - return output, attn - - -class InterpretableMultiHeadAttention(): - """Defines interpretable multi-head attention layer. - - Attributes: - n_head: Number of heads - d_k: Key/query dimensionality per head - d_v: Value dimensionality - dropout: Dropout rate to apply - qs_layers: List of queries across heads - ks_layers: List of keys across heads - vs_layers: List of values across heads - attention: Scaled dot product attention layer - w_o: Output weight matrix to project internal state to the original TFT - state size - """ - - def __init__(self, n_head, d_model, dropout): - """Initialises layer. - - Args: - n_head: Number of heads - d_model: TFT state dimensionality - dropout: Dropout discard rate - """ - - self.n_head = n_head - self.d_k = self.d_v = d_k = d_v = d_model // n_head - self.dropout = dropout - - self.qs_layers = [] - self.ks_layers = [] - self.vs_layers = [] - - # Use same value layer to facilitate interp - vs_layer = Dense(d_v, use_bias=False) - - for _ in range(n_head): - self.qs_layers.append(Dense(d_k, use_bias=False)) - self.ks_layers.append(Dense(d_k, use_bias=False)) - self.vs_layers.append(vs_layer) # use same vs_layer - - self.attention = ScaledDotProductAttention() - self.w_o = Dense(d_model, use_bias=False) - - def __call__(self, q, k, v, mask=None): - """Applies interpretable multihead attention. - - Using T to denote the number of time steps fed into the transformer. - - Args: - q: Query tensor of shape=(?, T, d_model) - k: Key of shape=(?, T, d_model) - v: Values of shape=(?, T, d_model) - mask: Masking if required with shape=(?, T, T) - - Returns: - Tuple of (layer outputs, attention weights) - """ - n_head = self.n_head - - heads = [] - attns = [] - for i in range(n_head): - qs = self.qs_layers[i](q) - ks = self.ks_layers[i](k) - vs = self.vs_layers[i](v) - head, attn = self.attention(qs, ks, vs, mask) - - head_dropout = Dropout(self.dropout)(head) - heads.append(head_dropout) - attns.append(attn) - head = K.stack(heads) if n_head > 1 else heads[0] - attn = K.stack(attns) - - outputs = K.mean(head, axis=0) if n_head > 1 else head - outputs = self.w_o(outputs) - outputs = Dropout(self.dropout)(outputs) # output dropout - - return outputs, attn - - -class TFTDataCache(object): - """Caches data for the TFT.""" - - _data_cache = {} - - @classmethod - def update(cls, data, key): - """Updates cached data. - - Args: - data: Source to update - key: Key to dictionary location - """ - cls._data_cache[key] = data - - @classmethod - def get(cls, key): - """Returns data stored at key location.""" - return cls._data_cache[key].copy() - - @classmethod - def contains(cls, key): - """Retuns boolean indicating whether key is present in cache.""" - - return key in cls._data_cache - - -# TFT model definitions. -class TemporalFusionTransformer(object): - """Defines Temporal Fusion Transformer. - - Attributes: - name: Name of model - time_steps: Total number of input time steps per forecast date (i.e. Width - of Temporal fusion decoder N) - input_size: Total number of inputs - output_size: Total number of outputs - category_counts: Number of categories per categorical variable - n_multiprocessing_workers: Number of workers to use for parallel - computations - column_definition: List of tuples of (string, DataType, InputType) that - define each column - quantiles: Quantiles to forecast for TFT - use_cudnn: Whether to use Keras CuDNNLSTM or standard LSTM layers - hidden_layer_size: Internal state size of TFT - dropout_rate: Dropout discard rate - max_gradient_norm: Maximum norm for gradient clipping - learning_rate: Initial learning rate of ADAM optimizer - minibatch_size: Size of minibatches for training - num_epochs: Maximum number of epochs for training - early_stopping_patience: Maximum number of iterations of non-improvement - before early stopping kicks in - num_encoder_steps: Size of LSTM encoder -- i.e. number of past time steps - before forecast date to use - num_stacks: Number of self-attention layers to apply (default is 1 for basic - TFT) - num_heads: Number of heads for interpretable mulit-head attention - model: Keras model for TFT - """ - - def __init__(self, raw_params, use_cudnn=False): - """Builds TFT from parameters. - - Args: - raw_params: Parameters to define TFT - use_cudnn: Whether to use CUDNN GPU optimised LSTM - """ - - self.name = self.__class__.__name__ - - params = dict(raw_params) # copy locally - - # Data parameters - self.time_steps = int(params['total_time_steps']) - self.input_size = int(params['input_size']) - self.output_size = int(params['output_size']) - self.category_counts = json.loads(str(params['category_counts'])) - self.n_multiprocessing_workers = int(params['multiprocessing_workers']) - - # Relevant indices for TFT - self._input_obs_loc = json.loads(str(params['input_obs_loc'])) - self._static_input_loc = json.loads(str(params['static_input_loc'])) - self._known_regular_input_idx = json.loads( - str(params['known_regular_inputs'])) - self._known_categorical_input_idx = json.loads( - str(params['known_categorical_inputs'])) - - self.column_definition = params['column_definition'] - - # Network params - self.quantiles = [0.1, 0.5, 0.9] - self.use_cudnn = use_cudnn # Whether to use GPU optimised LSTM - self.hidden_layer_size = int(params['hidden_layer_size']) - self.dropout_rate = float(params['dropout_rate']) - self.max_gradient_norm = float(params['max_gradient_norm']) - self.learning_rate = float(params['learning_rate']) - self.minibatch_size = int(params['minibatch_size']) - self.num_epochs = int(params['num_epochs']) - self.early_stopping_patience = int(params['early_stopping_patience']) - - self.num_encoder_steps = int(params['num_encoder_steps']) - self.num_stacks = int(params['stack_size']) - self.num_heads = int(params['num_heads']) - - # Serialisation options - self._temp_folder = os.path.join(params['model_folder'], 'tmp') - self.reset_temp_folder() - - # Extra components to store Tensorflow nodes for attention computations - self._input_placeholder = None - self._attention_components = None - self._prediction_parts = None - - print('*** {} params ***'.format(self.name)) - for k in params: - print('# {} = {}'.format(k, params[k])) - - # Build model - self.model = self.build_model() - - def get_tft_embeddings(self, all_inputs): - """Transforms raw inputs to embeddings. - - Applies linear transformation onto continuous variables and uses embeddings - for categorical variables. - - Args: - all_inputs: Inputs to transform - - Returns: - Tensors for transformed inputs. - """ - - time_steps = self.time_steps - - # Sanity checks - for i in self._known_regular_input_idx: - if i in self._input_obs_loc: - raise ValueError('Observation cannot be known a priori!') - for i in self._input_obs_loc: - if i in self._static_input_loc: - raise ValueError('Observation cannot be static!') - - if all_inputs.get_shape().as_list()[-1] != self.input_size: - raise ValueError( - 'Illegal number of inputs! Inputs observed={}, expected={}'.format( - all_inputs.get_shape().as_list()[-1], self.input_size)) - - num_categorical_variables = len(self.category_counts) - num_regular_variables = self.input_size - num_categorical_variables - - embedding_sizes = [ - self.hidden_layer_size for i, size in enumerate(self.category_counts) - ] - - embeddings = [] - for i in range(num_categorical_variables): - - embedding = tf.keras.Sequential([ - tf.keras.layers.InputLayer([time_steps]), - tf.keras.layers.Embedding( - self.category_counts[i], - embedding_sizes[i], - input_length=time_steps, - dtype=tf.float32) - ]) - embeddings.append(embedding) - - regular_inputs, categorical_inputs \ - = all_inputs[:, :, :num_regular_variables], \ - all_inputs[:, :, num_regular_variables:] - - embedded_inputs = [ - embeddings[i](categorical_inputs[Ellipsis, i]) - for i in range(num_categorical_variables) - ] - - # Static inputs - if self._static_input_loc: - static_inputs = [tf.keras.layers.Dense(self.hidden_layer_size)( - regular_inputs[:, 0, i:i + 1]) for i in range(num_regular_variables) - if i in self._static_input_loc] \ - + [embedded_inputs[i][:, 0, :] - for i in range(num_categorical_variables) - if i + num_regular_variables in self._static_input_loc] - static_inputs = tf.keras.backend.stack(static_inputs, axis=1) - - else: - static_inputs = None - - def convert_real_to_embedding(x): - """Applies linear transformation for time-varying inputs.""" - return tf.keras.layers.TimeDistributed( - tf.keras.layers.Dense(self.hidden_layer_size))( - x) - - # Targets - obs_inputs = tf.keras.backend.stack([ - convert_real_to_embedding(regular_inputs[Ellipsis, i:i + 1]) - for i in self._input_obs_loc - ], - axis=-1) - - # Observed (a prioir unknown) inputs - wired_embeddings = [] - for i in range(num_categorical_variables): - if i not in self._known_categorical_input_idx \ - and i + num_regular_variables not in self._input_obs_loc: - e = embeddings[i](categorical_inputs[:, :, i]) - wired_embeddings.append(e) - - unknown_inputs = [] - for i in range(regular_inputs.shape[-1]): - if i not in self._known_regular_input_idx \ - and i not in self._input_obs_loc: - e = convert_real_to_embedding(regular_inputs[Ellipsis, i:i + 1]) - unknown_inputs.append(e) - - if unknown_inputs + wired_embeddings: - unknown_inputs = tf.keras.backend.stack( - unknown_inputs + wired_embeddings, axis=-1) - else: - unknown_inputs = None - - # A priori known inputs - known_regular_inputs = [ - convert_real_to_embedding(regular_inputs[Ellipsis, i:i + 1]) - for i in self._known_regular_input_idx - if i not in self._static_input_loc - ] - known_categorical_inputs = [ - embedded_inputs[i] - for i in self._known_categorical_input_idx - if i + num_regular_variables not in self._static_input_loc - ] - - known_combined_layer = tf.keras.backend.stack( - known_regular_inputs + known_categorical_inputs, axis=-1) - - return unknown_inputs, known_combined_layer, obs_inputs, static_inputs - - def _get_single_col_by_type(self, input_type): - """Returns name of single column for input type.""" - - return utils.get_single_col_by_input_type(input_type, - self.column_definition) - - def training_data_cached(self): - """Returns boolean indicating if training data has been cached.""" - - return TFTDataCache.contains('train') and TFTDataCache.contains('valid') - - def cache_batched_data(self, data, cache_key, num_samples=-1): - """Batches and caches data once for using during training. - - Args: - data: Data to batch and cache - cache_key: Key used for cache - num_samples: Maximum number of samples to extract (-1 to use all data) - """ - - if num_samples > 0: - TFTDataCache.update( - self._batch_sampled_data(data, max_samples=num_samples), cache_key) - else: - TFTDataCache.update(self._batch_data(data), cache_key) - - print('Cached data "{}" updated'.format(cache_key)) - - def _batch_sampled_data(self, data, max_samples): - """Samples segments into a compatible format. - - Args: - data: Sources data to sample and batch - max_samples: Maximum number of samples in batch - - Returns: - Dictionary of batched data with the maximum samples specified. - """ - - if max_samples < 1: - raise ValueError( - 'Illegal number of samples specified! samples={}'.format(max_samples)) - - id_col = self._get_single_col_by_type(InputTypes.ID) - time_col = self._get_single_col_by_type(InputTypes.TIME) - - data.sort_values(by=[id_col, time_col], inplace=True) - - print('Getting valid sampling locations.') - valid_sampling_locations = [] - split_data_map = {} - for identifier, df in data.groupby(id_col): - print('Getting locations for {}'.format(identifier)) - num_entries = len(df) - if num_entries >= self.time_steps: - valid_sampling_locations += [ - (identifier, self.time_steps + i) - for i in range(num_entries - self.time_steps + 1) - ] - split_data_map[identifier] = df - - inputs = np.zeros((max_samples, self.time_steps, self.input_size)) - outputs = np.zeros((max_samples, self.time_steps, self.output_size)) - time = np.empty((max_samples, self.time_steps, 1), dtype=object) - identifiers = np.empty((max_samples, self.time_steps, 1), dtype=object) - - if max_samples > 0 and len(valid_sampling_locations) > max_samples: - print('Extracting {} samples...'.format(max_samples)) - ranges = [ - valid_sampling_locations[i] for i in np.random.choice( - len(valid_sampling_locations), max_samples, replace=False) - ] - else: - print('Max samples={} exceeds # available segments={}'.format( - max_samples, len(valid_sampling_locations))) - ranges = valid_sampling_locations - - id_col = self._get_single_col_by_type(InputTypes.ID) - time_col = self._get_single_col_by_type(InputTypes.TIME) - target_col = self._get_single_col_by_type(InputTypes.TARGET) - input_cols = [ - tup[0] - for tup in self.column_definition - if tup[2] not in {InputTypes.ID, InputTypes.TIME} - ] - - for i, tup in enumerate(ranges): - if (i + 1 % 1000) == 0: - print(i + 1, 'of', max_samples, 'samples done...') - identifier, start_idx = tup - sliced = split_data_map[identifier].iloc[start_idx - - self.time_steps:start_idx] - inputs[i, :, :] = sliced[input_cols] - outputs[i, :, :] = sliced[[target_col]] - time[i, :, 0] = sliced[time_col] - identifiers[i, :, 0] = sliced[id_col] - - sampled_data = { - 'inputs': inputs, - 'outputs': outputs[:, self.num_encoder_steps:, :], - 'active_entries': np.ones_like(outputs[:, self.num_encoder_steps:, :]), - 'time': time, - 'identifier': identifiers - } - - return sampled_data - - def _batch_data(self, data): - """Batches data for training. - - Converts raw dataframe from a 2-D tabular format to a batched 3-D array - to feed into Keras model. - - Args: - data: DataFrame to batch - - Returns: - Batched Numpy array with shape=(?, self.time_steps, self.input_size) - """ - - # Functions. - def _batch_single_entity(input_data): - time_steps = len(input_data) - lags = self.time_steps - x = input_data.values - if time_steps >= lags: - return np.stack( - [x[i:time_steps - (lags - 1) + i, :] for i in range(lags)], axis=1) - - else: - return None - - id_col = self._get_single_col_by_type(InputTypes.ID) - time_col = self._get_single_col_by_type(InputTypes.TIME) - target_col = self._get_single_col_by_type(InputTypes.TARGET) - input_cols = [ - tup[0] - for tup in self.column_definition - if tup[2] not in {InputTypes.ID, InputTypes.TIME} - ] - - data_map = {} - for _, sliced in data.groupby(id_col): - - col_mappings = { - 'identifier': [id_col], - 'time': [time_col], - 'outputs': [target_col], - 'inputs': input_cols - } - - for k in col_mappings: - cols = col_mappings[k] - arr = _batch_single_entity(sliced[cols].copy()) - - if k not in data_map: - data_map[k] = [arr] - else: - data_map[k].append(arr) - - # Combine all data - for k in data_map: - # Wendi: Avoid returning None when the length is not enough - data_map[k] = np.concatenate([i for i in data_map[k] if i is not None], axis=0) - - # Shorten target so we only get decoder steps - data_map['outputs'] = data_map['outputs'][:, self.num_encoder_steps:, :] - - active_entries = np.ones_like(data_map['outputs']) - if 'active_entries' not in data_map: - data_map['active_entries'] = active_entries - else: - data_map['active_entries'].append(active_entries) - - return data_map - - def _get_active_locations(self, x): - """Formats sample weights for Keras training.""" - return (np.sum(x, axis=-1) > 0.0) * 1.0 - - def _build_base_graph(self): - """Returns graph defining layers of the TFT.""" - - # Size definitions. - time_steps = self.time_steps - combined_input_size = self.input_size - encoder_steps = self.num_encoder_steps - - # Inputs. - all_inputs = tf.keras.layers.Input( - shape=( - time_steps, - combined_input_size, - )) - - unknown_inputs, known_combined_layer, obs_inputs, static_inputs \ - = self.get_tft_embeddings(all_inputs) - - # Isolate known and observed historical inputs. - if unknown_inputs is not None: - historical_inputs = concat([ - unknown_inputs[:, :encoder_steps, :], - known_combined_layer[:, :encoder_steps, :], - obs_inputs[:, :encoder_steps, :] - ], - axis=-1) - else: - historical_inputs = concat([ - known_combined_layer[:, :encoder_steps, :], - obs_inputs[:, :encoder_steps, :] - ], - axis=-1) - - # Isolate only known future inputs. - future_inputs = known_combined_layer[:, encoder_steps:, :] - - def static_combine_and_mask(embedding): - """Applies variable selection network to static inputs. - - Args: - embedding: Transformed static inputs - - Returns: - Tensor output for variable selection network - """ - - # Add temporal features - _, num_static, _ = embedding.get_shape().as_list() - - flatten = tf.keras.layers.Flatten()(embedding) - - # Nonlinear transformation with gated residual network. - mlp_outputs = gated_residual_network( - flatten, - self.hidden_layer_size, - output_size=num_static, - dropout_rate=self.dropout_rate, - use_time_distributed=False, - additional_context=None) - - sparse_weights = tf.keras.layers.Activation('softmax')(mlp_outputs) - sparse_weights = K.expand_dims(sparse_weights, axis=-1) - - trans_emb_list = [] - for i in range(num_static): - e = gated_residual_network( - embedding[:, i:i + 1, :], - self.hidden_layer_size, - dropout_rate=self.dropout_rate, - use_time_distributed=False) - trans_emb_list.append(e) - - transformed_embedding = concat(trans_emb_list, axis=1) - - combined = tf.keras.layers.Multiply()( - [sparse_weights, transformed_embedding]) - - static_vec = K.sum(combined, axis=1) - - return static_vec, sparse_weights - - static_encoder, static_weights = static_combine_and_mask(static_inputs) - - static_context_variable_selection = gated_residual_network( - static_encoder, - self.hidden_layer_size, - dropout_rate=self.dropout_rate, - use_time_distributed=False) - static_context_enrichment = gated_residual_network( - static_encoder, - self.hidden_layer_size, - dropout_rate=self.dropout_rate, - use_time_distributed=False) - static_context_state_h = gated_residual_network( - static_encoder, - self.hidden_layer_size, - dropout_rate=self.dropout_rate, - use_time_distributed=False) - static_context_state_c = gated_residual_network( - static_encoder, - self.hidden_layer_size, - dropout_rate=self.dropout_rate, - use_time_distributed=False) - - def lstm_combine_and_mask(embedding): - """Apply temporal variable selection networks. - - Args: - embedding: Transformed inputs. - - Returns: - Processed tensor outputs. - """ - - # Add temporal features - _, time_steps, embedding_dim, num_inputs = embedding.get_shape().as_list() - - flatten = K.reshape(embedding, - [-1, time_steps, embedding_dim * num_inputs]) - - expanded_static_context = K.expand_dims( - static_context_variable_selection, axis=1) - - # Variable selection weights - mlp_outputs, static_gate = gated_residual_network( - flatten, - self.hidden_layer_size, - output_size=num_inputs, - dropout_rate=self.dropout_rate, - use_time_distributed=True, - additional_context=expanded_static_context, - return_gate=True) - - sparse_weights = tf.keras.layers.Activation('softmax')(mlp_outputs) - sparse_weights = tf.expand_dims(sparse_weights, axis=2) - - # Non-linear Processing & weight application - trans_emb_list = [] - for i in range(num_inputs): - grn_output = gated_residual_network( - embedding[Ellipsis, i], - self.hidden_layer_size, - dropout_rate=self.dropout_rate, - use_time_distributed=True) - trans_emb_list.append(grn_output) - - transformed_embedding = stack(trans_emb_list, axis=-1) - - combined = tf.keras.layers.Multiply()( - [sparse_weights, transformed_embedding]) - temporal_ctx = K.sum(combined, axis=-1) - - return temporal_ctx, sparse_weights, static_gate - - historical_features, historical_flags, _ = lstm_combine_and_mask( - historical_inputs) - future_features, future_flags, _ = lstm_combine_and_mask(future_inputs) - - # LSTM layer - def get_lstm(return_state): - """Returns LSTM cell initialized with default parameters.""" - if self.use_cudnn: - lstm = tf.keras.layers.CuDNNLSTM( - self.hidden_layer_size, - return_sequences=True, - return_state=return_state, - stateful=False, - ) - else: - lstm = tf.keras.layers.LSTM( - self.hidden_layer_size, - return_sequences=True, - return_state=return_state, - stateful=False, - # Additional params to ensure LSTM matches CuDNN, See TF 2.0 : - # (https://www.tensorflow.org/api_docs/python/tf/keras/layers/LSTM) - activation='tanh', - recurrent_activation='sigmoid', - recurrent_dropout=0, - unroll=False, - use_bias=True) - return lstm - - history_lstm, state_h, state_c \ - = get_lstm(return_state=True)(historical_features, - initial_state=[static_context_state_h, - static_context_state_c]) - - future_lstm = get_lstm(return_state=False)( - future_features, initial_state=[state_h, state_c]) - - lstm_layer = concat([history_lstm, future_lstm], axis=1) - - # Apply gated skip connection - input_embeddings = concat([historical_features, future_features], axis=1) - - lstm_layer, _ = apply_gating_layer( - lstm_layer, self.hidden_layer_size, self.dropout_rate, activation=None) - temporal_feature_layer = add_and_norm([lstm_layer, input_embeddings]) - - # Static enrichment layers - expanded_static_context = K.expand_dims(static_context_enrichment, axis=1) - enriched, _ = gated_residual_network( - temporal_feature_layer, - self.hidden_layer_size, - dropout_rate=self.dropout_rate, - use_time_distributed=True, - additional_context=expanded_static_context, - return_gate=True) - - # Decoder self attention - self_attn_layer = InterpretableMultiHeadAttention( - self.num_heads, self.hidden_layer_size, dropout=self.dropout_rate) - - mask = get_decoder_mask(enriched) - x, self_att \ - = self_attn_layer(enriched, enriched, enriched, - mask=mask) - - x, _ = apply_gating_layer( - x, - self.hidden_layer_size, - dropout_rate=self.dropout_rate, - activation=None) - x = add_and_norm([x, enriched]) - - # Nonlinear processing on outputs - decoder = gated_residual_network( - x, - self.hidden_layer_size, - dropout_rate=self.dropout_rate, - use_time_distributed=True) - - # Final skip connection - decoder, _ = apply_gating_layer( - decoder, self.hidden_layer_size, activation=None) - transformer_layer = add_and_norm([decoder, temporal_feature_layer]) - - # Attention components for explainability - attention_components = { - # Temporal attention weights - 'decoder_self_attn': self_att, - # Static variable selection weights - 'static_flags': static_weights[Ellipsis, 0], - # Variable selection weights of past inputs - 'historical_flags': historical_flags[Ellipsis, 0, :], - # Variable selection weights of future inputs - 'future_flags': future_flags[Ellipsis, 0, :] - } - - return transformer_layer, all_inputs, attention_components - - def build_model(self): - """Build model and defines training losses. - - Returns: - Fully defined Keras model. - """ - - with tf.variable_scope(self.name): - - transformer_layer, all_inputs, attention_components \ - = self._build_base_graph() - - outputs = tf.keras.layers.TimeDistributed( - tf.keras.layers.Dense(self.output_size * len(self.quantiles))) \ - (transformer_layer[Ellipsis, self.num_encoder_steps:, :]) - - self._attention_components = attention_components - - adam = tf.keras.optimizers.Adam( - lr=self.learning_rate, clipnorm=self.max_gradient_norm) - - model = tf.keras.Model(inputs=all_inputs, outputs=outputs) - - print(model.summary()) - - valid_quantiles = self.quantiles - output_size = self.output_size - - class QuantileLossCalculator(object): - """Computes the combined quantile loss for prespecified quantiles. - - Attributes: - quantiles: Quantiles to compute losses - """ - - def __init__(self, quantiles): - """Initializes computer with quantiles for loss calculations. - - Args: - quantiles: Quantiles to use for computations. - """ - self.quantiles = quantiles - - def quantile_loss(self, a, b): - """Returns quantile loss for specified quantiles. - - Args: - a: Targets - b: Predictions - """ - quantiles_used = set(self.quantiles) - - loss = 0. - for i, quantile in enumerate(valid_quantiles): - if quantile in quantiles_used: - loss += utils.tensorflow_quantile_loss( - a[Ellipsis, output_size * i:output_size * (i + 1)], - b[Ellipsis, output_size * i:output_size * (i + 1)], quantile) - return loss - - quantile_loss = QuantileLossCalculator(valid_quantiles).quantile_loss - - model.compile( - loss=quantile_loss, optimizer=adam, sample_weight_mode='temporal') - - self._input_placeholder = all_inputs - - return model - - def fit(self, train_df=None, valid_df=None): - """Fits deep neural network for given training and validation data. - - Args: - train_df: DataFrame for training data - valid_df: DataFrame for validation data - """ - - print('*** Fitting {} ***'.format(self.name)) - - # Add relevant callbacks - callbacks = [ - tf.keras.callbacks.EarlyStopping( - monitor='val_loss', - patience=self.early_stopping_patience, - min_delta=1e-4), - tf.keras.callbacks.ModelCheckpoint( - filepath=self.get_keras_saved_path(self._temp_folder), - monitor='val_loss', - save_best_only=True, - save_weights_only=True), - tf.keras.callbacks.TerminateOnNaN() - ] - - print('Getting batched_data') - if train_df is None: - print('Using cached training data') - train_data = TFTDataCache.get('train') - else: - train_data = self._batch_data(train_df) - - if valid_df is None: - print('Using cached validation data') - valid_data = TFTDataCache.get('valid') - else: - valid_data = self._batch_data(valid_df) - - print('Using keras standard fit') - - def _unpack(data): - return data['inputs'], data['outputs'], \ - self._get_active_locations(data['active_entries']) - - # Unpack without sample weights - data, labels, active_flags = _unpack(train_data) - val_data, val_labels, val_flags = _unpack(valid_data) - - all_callbacks = callbacks - - self.model.fit( - x=data, - y=np.concatenate([labels, labels, labels], axis=-1), - sample_weight=active_flags, - epochs=self.num_epochs, - batch_size=self.minibatch_size, - validation_data=(val_data, - np.concatenate([val_labels, val_labels, val_labels], - axis=-1), val_flags), - callbacks=all_callbacks, - shuffle=True, - use_multiprocessing=True, - workers=self.n_multiprocessing_workers) - - # Load best checkpoint again - tmp_checkpont = self.get_keras_saved_path(self._temp_folder) - if os.path.exists(tmp_checkpont): - self.load( - self._temp_folder, - use_keras_loadings=True) - - else: - print('Cannot load from {}, skipping ...'.format(self._temp_folder)) - - def evaluate(self, data=None, eval_metric='loss'): - """Applies evaluation metric to the training data. - - Args: - data: Dataframe for evaluation - eval_metric: Evaluation metic to return, based on model definition. - - Returns: - Computed evaluation loss. - """ - - if data is None: - print('Using cached validation data') - raw_data = TFTDataCache.get('valid') - else: - raw_data = self._batch_data(data) - - inputs = raw_data['inputs'] - outputs = raw_data['outputs'] - active_entries = self._get_active_locations(raw_data['active_entries']) - - metric_values = self.model.evaluate( - x=inputs, - y=np.concatenate([outputs, outputs, outputs], axis=-1), - sample_weight=active_entries, - workers=16, - use_multiprocessing=True) - - metrics = pd.Series(metric_values, self.model.metrics_names) - - return metrics[eval_metric] - - def predict(self, df, return_targets=False): - """Computes predictions for a given input dataset. - - Args: - df: Input dataframe - return_targets: Whether to also return outputs aligned with predictions to - faciliate evaluation - - Returns: - Input dataframe or tuple of (input dataframe, algined output dataframe). - """ - - data = self._batch_data(df) - - inputs = data['inputs'] - time = data['time'] - identifier = data['identifier'] - outputs = data['outputs'] - - combined = self.model.predict( - inputs, - workers=16, - use_multiprocessing=True, - batch_size=self.minibatch_size) - - # Format output_csv - if self.output_size != 1: - raise NotImplementedError('Current version only supports 1D targets!') - - def format_outputs(prediction): - """Returns formatted dataframes for prediction.""" - - flat_prediction = pd.DataFrame( - prediction[:, :, 0], - columns=[ - 't+{}'.format(i) - for i in range(self.time_steps - self.num_encoder_steps) - ]) - cols = list(flat_prediction.columns) - flat_prediction['forecast_time'] = time[:, self.num_encoder_steps - 1, 0] - flat_prediction['identifier'] = identifier[:, 0, 0] - - # Arrange in order - return flat_prediction[['forecast_time', 'identifier'] + cols] - - # Extract predictions for each quantile into different entries - process_map = { - 'p{}'.format(int(q * 100)): - combined[Ellipsis, i * self.output_size:(i + 1) * self.output_size] - for i, q in enumerate(self.quantiles) - } - - if return_targets: - # Add targets if relevant - process_map['targets'] = outputs - - return {k: format_outputs(process_map[k]) for k in process_map} - - def get_attention(self, df): - """Computes TFT attention weights for a given dataset. - - Args: - df: Input dataframe - - Returns: - Dictionary of numpy arrays for temporal attention weights and variable - selection weights, along with their identifiers and time indices - """ - - data = self._batch_data(df) - inputs = data['inputs'] - identifiers = data['identifier'] - time = data['time'] - - def get_batch_attention_weights(input_batch): - """Returns weights for a given minibatch of data.""" - input_placeholder = self._input_placeholder - attention_weights = {} - for k in self._attention_components: - attention_weight = tf.keras.backend.get_session().run( - self._attention_components[k], - {input_placeholder: input_batch.astype(np.float32)}) - attention_weights[k] = attention_weight - return attention_weights - - # Compute number of batches - batch_size = self.minibatch_size - n = inputs.shape[0] - num_batches = n // batch_size - if n - (num_batches * batch_size) > 0: - num_batches += 1 - - # Split up inputs into batches - batched_inputs = [ - inputs[i * batch_size:(i + 1) * batch_size, Ellipsis] - for i in range(num_batches) - ] - - # Get attention weights, while avoiding large memory increases - attention_by_batch = [ - get_batch_attention_weights(batch) for batch in batched_inputs - ] - attention_weights = {} - for k in self._attention_components: - attention_weights[k] = [] - for batch_weights in attention_by_batch: - attention_weights[k].append(batch_weights[k]) - - if len(attention_weights[k][0].shape) == 4: - tmp = np.concatenate(attention_weights[k], axis=1) - else: - tmp = np.concatenate(attention_weights[k], axis=0) - - del attention_weights[k] - gc.collect() - attention_weights[k] = tmp - - attention_weights['identifiers'] = identifiers[:, 0, 0] - attention_weights['time'] = time[:, :, 0] - - return attention_weights - - # Serialisation. - def reset_temp_folder(self): - """Deletes and recreates folder with temporary Keras training outputs.""" - print('Resetting temp folder...') - utils.create_folder_if_not_exist(self._temp_folder) - shutil.rmtree(self._temp_folder) - os.makedirs(self._temp_folder) - - def get_keras_saved_path(self, model_folder): - """Returns path to keras checkpoint.""" - return os.path.join(model_folder, '{}.check'.format(self.name)) - - def save(self, model_folder): - """Saves optimal TFT weights. - - Args: - model_folder: Location to serialze model. - """ - # Allows for direct serialisation of tensorflow variables to avoid spurious - # issue with Keras that leads to different performance evaluation results - # when model is reloaded (https://github.com/keras-team/keras/issues/4875). - - utils.save( - tf.keras.backend.get_session(), - model_folder, - cp_name=self.name, - scope=self.name) - - def load(self, model_folder, use_keras_loadings=False): - """Loads TFT weights. - - Args: - model_folder: Folder containing serialized models. - use_keras_loadings: Whether to load from Keras checkpoint. - - Returns: - - """ - if use_keras_loadings: - # Loads temporary Keras model saved during training. - serialisation_path = self.get_keras_saved_path(model_folder) - print('Loading model from {}'.format(serialisation_path)) - self.model.load_weights(serialisation_path) - else: - # Loads tensorflow graph for optimal models. - utils.load( - tf.keras.backend.get_session(), - model_folder, - cp_name=self.name, - scope=self.name) - - @classmethod - def get_hyperparm_choices(cls): - """Returns hyperparameter ranges for random search.""" - return { - 'dropout_rate': [0.1, 0.2, 0.3, 0.4, 0.5, 0.7, 0.9], - 'hidden_layer_size': [10, 20, 40, 80, 160, 240, 320], - 'minibatch_size': [64, 128, 256], - 'learning_rate': [1e-4, 1e-3, 1e-2], - 'max_gradient_norm': [0.01, 1.0, 100.0], - 'num_heads': [1, 4], - 'stack_size': [1], - } +# coding=utf-8 +# Copyright 2020 The Google Research Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Lint as: python3 +"""Temporal Fusion Transformer Model. + +Contains the full TFT architecture and associated components. Defines functions +for training, evaluation and prediction using simple Pandas Dataframe inputs. +""" + +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +import gc +import json +import os +import shutil + +import data_formatters.base +import libs.utils as utils +import numpy as np +import pandas as pd +import tensorflow as tf + +# Layer definitions. +concat = tf.keras.backend.concatenate +stack = tf.keras.backend.stack +K = tf.keras.backend +Add = tf.keras.layers.Add +LayerNorm = tf.keras.layers.LayerNormalization +Dense = tf.keras.layers.Dense +Multiply = tf.keras.layers.Multiply +Dropout = tf.keras.layers.Dropout +Activation = tf.keras.layers.Activation +Lambda = tf.keras.layers.Lambda + +# Default input types. +InputTypes = data_formatters.base.InputTypes + + +# Layer utility functions. +def linear_layer(size, activation=None, use_time_distributed=False, use_bias=True): + """Returns simple Keras linear layer. + + Args: + size: Output size + activation: Activation function to apply if required + use_time_distributed: Whether to apply layer across time + use_bias: Whether bias should be included in layer + """ + linear = tf.keras.layers.Dense(size, activation=activation, use_bias=use_bias) + if use_time_distributed: + linear = tf.keras.layers.TimeDistributed(linear) + return linear + + +def apply_mlp( + inputs, hidden_size, output_size, output_activation=None, hidden_activation="tanh", use_time_distributed=False +): + """Applies simple feed-forward network to an input. + + Args: + inputs: MLP inputs + hidden_size: Hidden state size + output_size: Output size of MLP + output_activation: Activation function to apply on output + hidden_activation: Activation function to apply on input + use_time_distributed: Whether to apply across time + + Returns: + Tensor for MLP outputs. + """ + if use_time_distributed: + hidden = tf.keras.layers.TimeDistributed(tf.keras.layers.Dense(hidden_size, activation=hidden_activation))( + inputs + ) + return tf.keras.layers.TimeDistributed(tf.keras.layers.Dense(output_size, activation=output_activation))(hidden) + else: + hidden = tf.keras.layers.Dense(hidden_size, activation=hidden_activation)(inputs) + return tf.keras.layers.Dense(output_size, activation=output_activation)(hidden) + + +def apply_gating_layer(x, hidden_layer_size, dropout_rate=None, use_time_distributed=True, activation=None): + """Applies a Gated Linear Unit (GLU) to an input. + + Args: + x: Input to gating layer + hidden_layer_size: Dimension of GLU + dropout_rate: Dropout rate to apply if any + use_time_distributed: Whether to apply across time + activation: Activation function to apply to the linear feature transform if + necessary + + Returns: + Tuple of tensors for: (GLU output, gate) + """ + + if dropout_rate is not None: + x = tf.keras.layers.Dropout(dropout_rate)(x) + + if use_time_distributed: + activation_layer = tf.keras.layers.TimeDistributed( + tf.keras.layers.Dense(hidden_layer_size, activation=activation) + )(x) + gated_layer = tf.keras.layers.TimeDistributed(tf.keras.layers.Dense(hidden_layer_size, activation="sigmoid"))(x) + else: + activation_layer = tf.keras.layers.Dense(hidden_layer_size, activation=activation)(x) + gated_layer = tf.keras.layers.Dense(hidden_layer_size, activation="sigmoid")(x) + + return tf.keras.layers.Multiply()([activation_layer, gated_layer]), gated_layer + + +def add_and_norm(x_list): + """Applies skip connection followed by layer normalisation. + + Args: + x_list: List of inputs to sum for skip connection + + Returns: + Tensor output from layer. + """ + tmp = Add()(x_list) + tmp = LayerNorm()(tmp) + return tmp + + +def gated_residual_network( + x, + hidden_layer_size, + output_size=None, + dropout_rate=None, + use_time_distributed=True, + additional_context=None, + return_gate=False, +): + """Applies the gated residual network (GRN) as defined in paper. + + Args: + x: Network inputs + hidden_layer_size: Internal state size + output_size: Size of output layer + dropout_rate: Dropout rate if dropout is applied + use_time_distributed: Whether to apply network across time dimension + additional_context: Additional context vector to use if relevant + return_gate: Whether to return GLU gate for diagnostic purposes + + Returns: + Tuple of tensors for: (GRN output, GLU gate) + """ + + # Setup skip connection + if output_size is None: + output_size = hidden_layer_size + skip = x + else: + linear = Dense(output_size) + if use_time_distributed: + linear = tf.keras.layers.TimeDistributed(linear) + skip = linear(x) + + # Apply feedforward network + hidden = linear_layer(hidden_layer_size, activation=None, use_time_distributed=use_time_distributed)(x) + if additional_context is not None: + hidden = hidden + linear_layer( + hidden_layer_size, activation=None, use_time_distributed=use_time_distributed, use_bias=False + )(additional_context) + hidden = tf.keras.layers.Activation("elu")(hidden) + hidden = linear_layer(hidden_layer_size, activation=None, use_time_distributed=use_time_distributed)(hidden) + + gating_layer, gate = apply_gating_layer( + hidden, output_size, dropout_rate=dropout_rate, use_time_distributed=use_time_distributed, activation=None + ) + + if return_gate: + return add_and_norm([skip, gating_layer]), gate + else: + return add_and_norm([skip, gating_layer]) + + +# Attention Components. +def get_decoder_mask(self_attn_inputs): + """Returns causal mask to apply for self-attention layer. + + Args: + self_attn_inputs: Inputs to self attention layer to determine mask shape + """ + len_s = tf.shape(self_attn_inputs)[1] + bs = tf.shape(self_attn_inputs)[:1] + mask = K.cumsum(tf.eye(len_s, batch_shape=bs), 1) + return mask + + +class ScaledDotProductAttention: + """Defines scaled dot product attention layer. + + Attributes: + dropout: Dropout rate to use + activation: Normalisation function for scaled dot product attention (e.g. + softmax by default) + """ + + def __init__(self, attn_dropout=0.0): + self.dropout = Dropout(attn_dropout) + self.activation = Activation("softmax") + + def __call__(self, q, k, v, mask): + """Applies scaled dot product attention. + + Args: + q: Queries + k: Keys + v: Values + mask: Masking if required -- sets softmax to very large value + + Returns: + Tuple of (layer outputs, attention weights) + """ + temper = tf.sqrt(tf.cast(tf.shape(k)[-1], dtype="float32")) + attn = Lambda(lambda x: K.batch_dot(x[0], x[1], axes=[2, 2]) / temper)([q, k]) # shape=(batch, q, k) + if mask is not None: + mmask = Lambda(lambda x: (-1e9) * (1.0 - K.cast(x, "float32")))(mask) # setting to infinity + attn = Add()([attn, mmask]) + attn = self.activation(attn) + attn = self.dropout(attn) + output = Lambda(lambda x: K.batch_dot(x[0], x[1]))([attn, v]) + return output, attn + + +class InterpretableMultiHeadAttention: + """Defines interpretable multi-head attention layer. + + Attributes: + n_head: Number of heads + d_k: Key/query dimensionality per head + d_v: Value dimensionality + dropout: Dropout rate to apply + qs_layers: List of queries across heads + ks_layers: List of keys across heads + vs_layers: List of values across heads + attention: Scaled dot product attention layer + w_o: Output weight matrix to project internal state to the original TFT + state size + """ + + def __init__(self, n_head, d_model, dropout): + """Initialises layer. + + Args: + n_head: Number of heads + d_model: TFT state dimensionality + dropout: Dropout discard rate + """ + + self.n_head = n_head + self.d_k = self.d_v = d_k = d_v = d_model // n_head + self.dropout = dropout + + self.qs_layers = [] + self.ks_layers = [] + self.vs_layers = [] + + # Use same value layer to facilitate interp + vs_layer = Dense(d_v, use_bias=False) + + for _ in range(n_head): + self.qs_layers.append(Dense(d_k, use_bias=False)) + self.ks_layers.append(Dense(d_k, use_bias=False)) + self.vs_layers.append(vs_layer) # use same vs_layer + + self.attention = ScaledDotProductAttention() + self.w_o = Dense(d_model, use_bias=False) + + def __call__(self, q, k, v, mask=None): + """Applies interpretable multihead attention. + + Using T to denote the number of time steps fed into the transformer. + + Args: + q: Query tensor of shape=(?, T, d_model) + k: Key of shape=(?, T, d_model) + v: Values of shape=(?, T, d_model) + mask: Masking if required with shape=(?, T, T) + + Returns: + Tuple of (layer outputs, attention weights) + """ + n_head = self.n_head + + heads = [] + attns = [] + for i in range(n_head): + qs = self.qs_layers[i](q) + ks = self.ks_layers[i](k) + vs = self.vs_layers[i](v) + head, attn = self.attention(qs, ks, vs, mask) + + head_dropout = Dropout(self.dropout)(head) + heads.append(head_dropout) + attns.append(attn) + head = K.stack(heads) if n_head > 1 else heads[0] + attn = K.stack(attns) + + outputs = K.mean(head, axis=0) if n_head > 1 else head + outputs = self.w_o(outputs) + outputs = Dropout(self.dropout)(outputs) # output dropout + + return outputs, attn + + +class TFTDataCache(object): + """Caches data for the TFT.""" + + _data_cache = {} + + @classmethod + def update(cls, data, key): + """Updates cached data. + + Args: + data: Source to update + key: Key to dictionary location + """ + cls._data_cache[key] = data + + @classmethod + def get(cls, key): + """Returns data stored at key location.""" + return cls._data_cache[key].copy() + + @classmethod + def contains(cls, key): + """Retuns boolean indicating whether key is present in cache.""" + + return key in cls._data_cache + + +# TFT model definitions. +class TemporalFusionTransformer(object): + """Defines Temporal Fusion Transformer. + + Attributes: + name: Name of model + time_steps: Total number of input time steps per forecast date (i.e. Width + of Temporal fusion decoder N) + input_size: Total number of inputs + output_size: Total number of outputs + category_counts: Number of categories per categorical variable + n_multiprocessing_workers: Number of workers to use for parallel + computations + column_definition: List of tuples of (string, DataType, InputType) that + define each column + quantiles: Quantiles to forecast for TFT + use_cudnn: Whether to use Keras CuDNNLSTM or standard LSTM layers + hidden_layer_size: Internal state size of TFT + dropout_rate: Dropout discard rate + max_gradient_norm: Maximum norm for gradient clipping + learning_rate: Initial learning rate of ADAM optimizer + minibatch_size: Size of minibatches for training + num_epochs: Maximum number of epochs for training + early_stopping_patience: Maximum number of iterations of non-improvement + before early stopping kicks in + num_encoder_steps: Size of LSTM encoder -- i.e. number of past time steps + before forecast date to use + num_stacks: Number of self-attention layers to apply (default is 1 for basic + TFT) + num_heads: Number of heads for interpretable mulit-head attention + model: Keras model for TFT + """ + + def __init__(self, raw_params, use_cudnn=False): + """Builds TFT from parameters. + + Args: + raw_params: Parameters to define TFT + use_cudnn: Whether to use CUDNN GPU optimised LSTM + """ + + self.name = self.__class__.__name__ + + params = dict(raw_params) # copy locally + + # Data parameters + self.time_steps = int(params["total_time_steps"]) + self.input_size = int(params["input_size"]) + self.output_size = int(params["output_size"]) + self.category_counts = json.loads(str(params["category_counts"])) + self.n_multiprocessing_workers = int(params["multiprocessing_workers"]) + + # Relevant indices for TFT + self._input_obs_loc = json.loads(str(params["input_obs_loc"])) + self._static_input_loc = json.loads(str(params["static_input_loc"])) + self._known_regular_input_idx = json.loads(str(params["known_regular_inputs"])) + self._known_categorical_input_idx = json.loads(str(params["known_categorical_inputs"])) + + self.column_definition = params["column_definition"] + + # Network params + self.quantiles = [0.1, 0.5, 0.9] + self.use_cudnn = use_cudnn # Whether to use GPU optimised LSTM + self.hidden_layer_size = int(params["hidden_layer_size"]) + self.dropout_rate = float(params["dropout_rate"]) + self.max_gradient_norm = float(params["max_gradient_norm"]) + self.learning_rate = float(params["learning_rate"]) + self.minibatch_size = int(params["minibatch_size"]) + self.num_epochs = int(params["num_epochs"]) + self.early_stopping_patience = int(params["early_stopping_patience"]) + + self.num_encoder_steps = int(params["num_encoder_steps"]) + self.num_stacks = int(params["stack_size"]) + self.num_heads = int(params["num_heads"]) + + # Serialisation options + self._temp_folder = os.path.join(params["model_folder"], "tmp") + self.reset_temp_folder() + + # Extra components to store Tensorflow nodes for attention computations + self._input_placeholder = None + self._attention_components = None + self._prediction_parts = None + + print("*** {} params ***".format(self.name)) + for k in params: + print("# {} = {}".format(k, params[k])) + + # Build model + self.model = self.build_model() + + def get_tft_embeddings(self, all_inputs): + """Transforms raw inputs to embeddings. + + Applies linear transformation onto continuous variables and uses embeddings + for categorical variables. + + Args: + all_inputs: Inputs to transform + + Returns: + Tensors for transformed inputs. + """ + + time_steps = self.time_steps + + # Sanity checks + for i in self._known_regular_input_idx: + if i in self._input_obs_loc: + raise ValueError("Observation cannot be known a priori!") + for i in self._input_obs_loc: + if i in self._static_input_loc: + raise ValueError("Observation cannot be static!") + + if all_inputs.get_shape().as_list()[-1] != self.input_size: + raise ValueError( + "Illegal number of inputs! Inputs observed={}, expected={}".format( + all_inputs.get_shape().as_list()[-1], self.input_size + ) + ) + + num_categorical_variables = len(self.category_counts) + num_regular_variables = self.input_size - num_categorical_variables + + embedding_sizes = [self.hidden_layer_size for i, size in enumerate(self.category_counts)] + + embeddings = [] + for i in range(num_categorical_variables): + + embedding = tf.keras.Sequential( + [ + tf.keras.layers.InputLayer([time_steps]), + tf.keras.layers.Embedding( + self.category_counts[i], embedding_sizes[i], input_length=time_steps, dtype=tf.float32 + ), + ] + ) + embeddings.append(embedding) + + regular_inputs, categorical_inputs = ( + all_inputs[:, :, :num_regular_variables], + all_inputs[:, :, num_regular_variables:], + ) + + embedded_inputs = [embeddings[i](categorical_inputs[Ellipsis, i]) for i in range(num_categorical_variables)] + + # Static inputs + if self._static_input_loc: + static_inputs = [ + tf.keras.layers.Dense(self.hidden_layer_size)(regular_inputs[:, 0, i : i + 1]) + for i in range(num_regular_variables) + if i in self._static_input_loc + ] + [ + embedded_inputs[i][:, 0, :] + for i in range(num_categorical_variables) + if i + num_regular_variables in self._static_input_loc + ] + static_inputs = tf.keras.backend.stack(static_inputs, axis=1) + + else: + static_inputs = None + + def convert_real_to_embedding(x): + """Applies linear transformation for time-varying inputs.""" + return tf.keras.layers.TimeDistributed(tf.keras.layers.Dense(self.hidden_layer_size))(x) + + # Targets + obs_inputs = tf.keras.backend.stack( + [convert_real_to_embedding(regular_inputs[Ellipsis, i : i + 1]) for i in self._input_obs_loc], axis=-1 + ) + + # Observed (a prioir unknown) inputs + wired_embeddings = [] + for i in range(num_categorical_variables): + if i not in self._known_categorical_input_idx and i + num_regular_variables not in self._input_obs_loc: + e = embeddings[i](categorical_inputs[:, :, i]) + wired_embeddings.append(e) + + unknown_inputs = [] + for i in range(regular_inputs.shape[-1]): + if i not in self._known_regular_input_idx and i not in self._input_obs_loc: + e = convert_real_to_embedding(regular_inputs[Ellipsis, i : i + 1]) + unknown_inputs.append(e) + + if unknown_inputs + wired_embeddings: + unknown_inputs = tf.keras.backend.stack(unknown_inputs + wired_embeddings, axis=-1) + else: + unknown_inputs = None + + # A priori known inputs + known_regular_inputs = [ + convert_real_to_embedding(regular_inputs[Ellipsis, i : i + 1]) + for i in self._known_regular_input_idx + if i not in self._static_input_loc + ] + known_categorical_inputs = [ + embedded_inputs[i] + for i in self._known_categorical_input_idx + if i + num_regular_variables not in self._static_input_loc + ] + + known_combined_layer = tf.keras.backend.stack(known_regular_inputs + known_categorical_inputs, axis=-1) + + return unknown_inputs, known_combined_layer, obs_inputs, static_inputs + + def _get_single_col_by_type(self, input_type): + """Returns name of single column for input type.""" + + return utils.get_single_col_by_input_type(input_type, self.column_definition) + + def training_data_cached(self): + """Returns boolean indicating if training data has been cached.""" + + return TFTDataCache.contains("train") and TFTDataCache.contains("valid") + + def cache_batched_data(self, data, cache_key, num_samples=-1): + """Batches and caches data once for using during training. + + Args: + data: Data to batch and cache + cache_key: Key used for cache + num_samples: Maximum number of samples to extract (-1 to use all data) + """ + + if num_samples > 0: + TFTDataCache.update(self._batch_sampled_data(data, max_samples=num_samples), cache_key) + else: + TFTDataCache.update(self._batch_data(data), cache_key) + + print('Cached data "{}" updated'.format(cache_key)) + + def _batch_sampled_data(self, data, max_samples): + """Samples segments into a compatible format. + + Args: + data: Sources data to sample and batch + max_samples: Maximum number of samples in batch + + Returns: + Dictionary of batched data with the maximum samples specified. + """ + + if max_samples < 1: + raise ValueError("Illegal number of samples specified! samples={}".format(max_samples)) + + id_col = self._get_single_col_by_type(InputTypes.ID) + time_col = self._get_single_col_by_type(InputTypes.TIME) + + data.sort_values(by=[id_col, time_col], inplace=True) + + print("Getting valid sampling locations.") + valid_sampling_locations = [] + split_data_map = {} + for identifier, df in data.groupby(id_col): + print("Getting locations for {}".format(identifier)) + num_entries = len(df) + if num_entries >= self.time_steps: + valid_sampling_locations += [ + (identifier, self.time_steps + i) for i in range(num_entries - self.time_steps + 1) + ] + split_data_map[identifier] = df + + inputs = np.zeros((max_samples, self.time_steps, self.input_size)) + outputs = np.zeros((max_samples, self.time_steps, self.output_size)) + time = np.empty((max_samples, self.time_steps, 1), dtype=object) + identifiers = np.empty((max_samples, self.time_steps, 1), dtype=object) + + if max_samples > 0 and len(valid_sampling_locations) > max_samples: + print("Extracting {} samples...".format(max_samples)) + ranges = [ + valid_sampling_locations[i] + for i in np.random.choice(len(valid_sampling_locations), max_samples, replace=False) + ] + else: + print("Max samples={} exceeds # available segments={}".format(max_samples, len(valid_sampling_locations))) + ranges = valid_sampling_locations + + id_col = self._get_single_col_by_type(InputTypes.ID) + time_col = self._get_single_col_by_type(InputTypes.TIME) + target_col = self._get_single_col_by_type(InputTypes.TARGET) + input_cols = [tup[0] for tup in self.column_definition if tup[2] not in {InputTypes.ID, InputTypes.TIME}] + + for i, tup in enumerate(ranges): + if (i + 1 % 1000) == 0: + print(i + 1, "of", max_samples, "samples done...") + identifier, start_idx = tup + sliced = split_data_map[identifier].iloc[start_idx - self.time_steps : start_idx] + inputs[i, :, :] = sliced[input_cols] + outputs[i, :, :] = sliced[[target_col]] + time[i, :, 0] = sliced[time_col] + identifiers[i, :, 0] = sliced[id_col] + + sampled_data = { + "inputs": inputs, + "outputs": outputs[:, self.num_encoder_steps :, :], + "active_entries": np.ones_like(outputs[:, self.num_encoder_steps :, :]), + "time": time, + "identifier": identifiers, + } + + return sampled_data + + def _batch_data(self, data): + """Batches data for training. + + Converts raw dataframe from a 2-D tabular format to a batched 3-D array + to feed into Keras model. + + Args: + data: DataFrame to batch + + Returns: + Batched Numpy array with shape=(?, self.time_steps, self.input_size) + """ + + # Functions. + def _batch_single_entity(input_data): + time_steps = len(input_data) + lags = self.time_steps + x = input_data.values + if time_steps >= lags: + return np.stack([x[i : time_steps - (lags - 1) + i, :] for i in range(lags)], axis=1) + + else: + return None + + id_col = self._get_single_col_by_type(InputTypes.ID) + time_col = self._get_single_col_by_type(InputTypes.TIME) + target_col = self._get_single_col_by_type(InputTypes.TARGET) + input_cols = [tup[0] for tup in self.column_definition if tup[2] not in {InputTypes.ID, InputTypes.TIME}] + + data_map = {} + for _, sliced in data.groupby(id_col): + + col_mappings = {"identifier": [id_col], "time": [time_col], "outputs": [target_col], "inputs": input_cols} + + for k in col_mappings: + cols = col_mappings[k] + arr = _batch_single_entity(sliced[cols].copy()) + + if k not in data_map: + data_map[k] = [arr] + else: + data_map[k].append(arr) + + # Combine all data + for k in data_map: + # Wendi: Avoid returning None when the length is not enough + data_map[k] = np.concatenate([i for i in data_map[k] if i is not None], axis=0) + + # Shorten target so we only get decoder steps + data_map["outputs"] = data_map["outputs"][:, self.num_encoder_steps :, :] + + active_entries = np.ones_like(data_map["outputs"]) + if "active_entries" not in data_map: + data_map["active_entries"] = active_entries + else: + data_map["active_entries"].append(active_entries) + + return data_map + + def _get_active_locations(self, x): + """Formats sample weights for Keras training.""" + return (np.sum(x, axis=-1) > 0.0) * 1.0 + + def _build_base_graph(self): + """Returns graph defining layers of the TFT.""" + + # Size definitions. + time_steps = self.time_steps + combined_input_size = self.input_size + encoder_steps = self.num_encoder_steps + + # Inputs. + all_inputs = tf.keras.layers.Input( + shape=( + time_steps, + combined_input_size, + ) + ) + + unknown_inputs, known_combined_layer, obs_inputs, static_inputs = self.get_tft_embeddings(all_inputs) + + # Isolate known and observed historical inputs. + if unknown_inputs is not None: + historical_inputs = concat( + [ + unknown_inputs[:, :encoder_steps, :], + known_combined_layer[:, :encoder_steps, :], + obs_inputs[:, :encoder_steps, :], + ], + axis=-1, + ) + else: + historical_inputs = concat( + [known_combined_layer[:, :encoder_steps, :], obs_inputs[:, :encoder_steps, :]], axis=-1 + ) + + # Isolate only known future inputs. + future_inputs = known_combined_layer[:, encoder_steps:, :] + + def static_combine_and_mask(embedding): + """Applies variable selection network to static inputs. + + Args: + embedding: Transformed static inputs + + Returns: + Tensor output for variable selection network + """ + + # Add temporal features + _, num_static, _ = embedding.get_shape().as_list() + + flatten = tf.keras.layers.Flatten()(embedding) + + # Nonlinear transformation with gated residual network. + mlp_outputs = gated_residual_network( + flatten, + self.hidden_layer_size, + output_size=num_static, + dropout_rate=self.dropout_rate, + use_time_distributed=False, + additional_context=None, + ) + + sparse_weights = tf.keras.layers.Activation("softmax")(mlp_outputs) + sparse_weights = K.expand_dims(sparse_weights, axis=-1) + + trans_emb_list = [] + for i in range(num_static): + e = gated_residual_network( + embedding[:, i : i + 1, :], + self.hidden_layer_size, + dropout_rate=self.dropout_rate, + use_time_distributed=False, + ) + trans_emb_list.append(e) + + transformed_embedding = concat(trans_emb_list, axis=1) + + combined = tf.keras.layers.Multiply()([sparse_weights, transformed_embedding]) + + static_vec = K.sum(combined, axis=1) + + return static_vec, sparse_weights + + static_encoder, static_weights = static_combine_and_mask(static_inputs) + + static_context_variable_selection = gated_residual_network( + static_encoder, self.hidden_layer_size, dropout_rate=self.dropout_rate, use_time_distributed=False + ) + static_context_enrichment = gated_residual_network( + static_encoder, self.hidden_layer_size, dropout_rate=self.dropout_rate, use_time_distributed=False + ) + static_context_state_h = gated_residual_network( + static_encoder, self.hidden_layer_size, dropout_rate=self.dropout_rate, use_time_distributed=False + ) + static_context_state_c = gated_residual_network( + static_encoder, self.hidden_layer_size, dropout_rate=self.dropout_rate, use_time_distributed=False + ) + + def lstm_combine_and_mask(embedding): + """Apply temporal variable selection networks. + + Args: + embedding: Transformed inputs. + + Returns: + Processed tensor outputs. + """ + + # Add temporal features + _, time_steps, embedding_dim, num_inputs = embedding.get_shape().as_list() + + flatten = K.reshape(embedding, [-1, time_steps, embedding_dim * num_inputs]) + + expanded_static_context = K.expand_dims(static_context_variable_selection, axis=1) + + # Variable selection weights + mlp_outputs, static_gate = gated_residual_network( + flatten, + self.hidden_layer_size, + output_size=num_inputs, + dropout_rate=self.dropout_rate, + use_time_distributed=True, + additional_context=expanded_static_context, + return_gate=True, + ) + + sparse_weights = tf.keras.layers.Activation("softmax")(mlp_outputs) + sparse_weights = tf.expand_dims(sparse_weights, axis=2) + + # Non-linear Processing & weight application + trans_emb_list = [] + for i in range(num_inputs): + grn_output = gated_residual_network( + embedding[Ellipsis, i], + self.hidden_layer_size, + dropout_rate=self.dropout_rate, + use_time_distributed=True, + ) + trans_emb_list.append(grn_output) + + transformed_embedding = stack(trans_emb_list, axis=-1) + + combined = tf.keras.layers.Multiply()([sparse_weights, transformed_embedding]) + temporal_ctx = K.sum(combined, axis=-1) + + return temporal_ctx, sparse_weights, static_gate + + historical_features, historical_flags, _ = lstm_combine_and_mask(historical_inputs) + future_features, future_flags, _ = lstm_combine_and_mask(future_inputs) + + # LSTM layer + def get_lstm(return_state): + """Returns LSTM cell initialized with default parameters.""" + if self.use_cudnn: + lstm = tf.keras.layers.CuDNNLSTM( + self.hidden_layer_size, + return_sequences=True, + return_state=return_state, + stateful=False, + ) + else: + lstm = tf.keras.layers.LSTM( + self.hidden_layer_size, + return_sequences=True, + return_state=return_state, + stateful=False, + # Additional params to ensure LSTM matches CuDNN, See TF 2.0 : + # (https://www.tensorflow.org/api_docs/python/tf/keras/layers/LSTM) + activation="tanh", + recurrent_activation="sigmoid", + recurrent_dropout=0, + unroll=False, + use_bias=True, + ) + return lstm + + history_lstm, state_h, state_c = get_lstm(return_state=True)( + historical_features, initial_state=[static_context_state_h, static_context_state_c] + ) + + future_lstm = get_lstm(return_state=False)(future_features, initial_state=[state_h, state_c]) + + lstm_layer = concat([history_lstm, future_lstm], axis=1) + + # Apply gated skip connection + input_embeddings = concat([historical_features, future_features], axis=1) + + lstm_layer, _ = apply_gating_layer(lstm_layer, self.hidden_layer_size, self.dropout_rate, activation=None) + temporal_feature_layer = add_and_norm([lstm_layer, input_embeddings]) + + # Static enrichment layers + expanded_static_context = K.expand_dims(static_context_enrichment, axis=1) + enriched, _ = gated_residual_network( + temporal_feature_layer, + self.hidden_layer_size, + dropout_rate=self.dropout_rate, + use_time_distributed=True, + additional_context=expanded_static_context, + return_gate=True, + ) + + # Decoder self attention + self_attn_layer = InterpretableMultiHeadAttention( + self.num_heads, self.hidden_layer_size, dropout=self.dropout_rate + ) + + mask = get_decoder_mask(enriched) + x, self_att = self_attn_layer(enriched, enriched, enriched, mask=mask) + + x, _ = apply_gating_layer(x, self.hidden_layer_size, dropout_rate=self.dropout_rate, activation=None) + x = add_and_norm([x, enriched]) + + # Nonlinear processing on outputs + decoder = gated_residual_network( + x, self.hidden_layer_size, dropout_rate=self.dropout_rate, use_time_distributed=True + ) + + # Final skip connection + decoder, _ = apply_gating_layer(decoder, self.hidden_layer_size, activation=None) + transformer_layer = add_and_norm([decoder, temporal_feature_layer]) + + # Attention components for explainability + attention_components = { + # Temporal attention weights + "decoder_self_attn": self_att, + # Static variable selection weights + "static_flags": static_weights[Ellipsis, 0], + # Variable selection weights of past inputs + "historical_flags": historical_flags[Ellipsis, 0, :], + # Variable selection weights of future inputs + "future_flags": future_flags[Ellipsis, 0, :], + } + + return transformer_layer, all_inputs, attention_components + + def build_model(self): + """Build model and defines training losses. + + Returns: + Fully defined Keras model. + """ + + with tf.variable_scope(self.name): + + transformer_layer, all_inputs, attention_components = self._build_base_graph() + + outputs = tf.keras.layers.TimeDistributed(tf.keras.layers.Dense(self.output_size * len(self.quantiles)))( + transformer_layer[Ellipsis, self.num_encoder_steps :, :] + ) + + self._attention_components = attention_components + + adam = tf.keras.optimizers.Adam(lr=self.learning_rate, clipnorm=self.max_gradient_norm) + + model = tf.keras.Model(inputs=all_inputs, outputs=outputs) + + print(model.summary()) + + valid_quantiles = self.quantiles + output_size = self.output_size + + class QuantileLossCalculator(object): + """Computes the combined quantile loss for prespecified quantiles. + + Attributes: + quantiles: Quantiles to compute losses + """ + + def __init__(self, quantiles): + """Initializes computer with quantiles for loss calculations. + + Args: + quantiles: Quantiles to use for computations. + """ + self.quantiles = quantiles + + def quantile_loss(self, a, b): + """Returns quantile loss for specified quantiles. + + Args: + a: Targets + b: Predictions + """ + quantiles_used = set(self.quantiles) + + loss = 0.0 + for i, quantile in enumerate(valid_quantiles): + if quantile in quantiles_used: + loss += utils.tensorflow_quantile_loss( + a[Ellipsis, output_size * i : output_size * (i + 1)], + b[Ellipsis, output_size * i : output_size * (i + 1)], + quantile, + ) + return loss + + quantile_loss = QuantileLossCalculator(valid_quantiles).quantile_loss + + model.compile(loss=quantile_loss, optimizer=adam, sample_weight_mode="temporal") + + self._input_placeholder = all_inputs + + return model + + def fit(self, train_df=None, valid_df=None): + """Fits deep neural network for given training and validation data. + + Args: + train_df: DataFrame for training data + valid_df: DataFrame for validation data + """ + + print("*** Fitting {} ***".format(self.name)) + + # Add relevant callbacks + callbacks = [ + tf.keras.callbacks.EarlyStopping(monitor="val_loss", patience=self.early_stopping_patience, min_delta=1e-4), + tf.keras.callbacks.ModelCheckpoint( + filepath=self.get_keras_saved_path(self._temp_folder), + monitor="val_loss", + save_best_only=True, + save_weights_only=True, + ), + tf.keras.callbacks.TerminateOnNaN(), + ] + + print("Getting batched_data") + if train_df is None: + print("Using cached training data") + train_data = TFTDataCache.get("train") + else: + train_data = self._batch_data(train_df) + + if valid_df is None: + print("Using cached validation data") + valid_data = TFTDataCache.get("valid") + else: + valid_data = self._batch_data(valid_df) + + print("Using keras standard fit") + + def _unpack(data): + return data["inputs"], data["outputs"], self._get_active_locations(data["active_entries"]) + + # Unpack without sample weights + data, labels, active_flags = _unpack(train_data) + val_data, val_labels, val_flags = _unpack(valid_data) + + all_callbacks = callbacks + + self.model.fit( + x=data, + y=np.concatenate([labels, labels, labels], axis=-1), + sample_weight=active_flags, + epochs=self.num_epochs, + batch_size=self.minibatch_size, + validation_data=(val_data, np.concatenate([val_labels, val_labels, val_labels], axis=-1), val_flags), + callbacks=all_callbacks, + shuffle=True, + use_multiprocessing=True, + workers=self.n_multiprocessing_workers, + ) + + # Load best checkpoint again + tmp_checkpont = self.get_keras_saved_path(self._temp_folder) + if os.path.exists(tmp_checkpont): + self.load(self._temp_folder, use_keras_loadings=True) + + else: + print("Cannot load from {}, skipping ...".format(self._temp_folder)) + + def evaluate(self, data=None, eval_metric="loss"): + """Applies evaluation metric to the training data. + + Args: + data: Dataframe for evaluation + eval_metric: Evaluation metic to return, based on model definition. + + Returns: + Computed evaluation loss. + """ + + if data is None: + print("Using cached validation data") + raw_data = TFTDataCache.get("valid") + else: + raw_data = self._batch_data(data) + + inputs = raw_data["inputs"] + outputs = raw_data["outputs"] + active_entries = self._get_active_locations(raw_data["active_entries"]) + + metric_values = self.model.evaluate( + x=inputs, + y=np.concatenate([outputs, outputs, outputs], axis=-1), + sample_weight=active_entries, + workers=16, + use_multiprocessing=True, + ) + + metrics = pd.Series(metric_values, self.model.metrics_names) + + return metrics[eval_metric] + + def predict(self, df, return_targets=False): + """Computes predictions for a given input dataset. + + Args: + df: Input dataframe + return_targets: Whether to also return outputs aligned with predictions to + faciliate evaluation + + Returns: + Input dataframe or tuple of (input dataframe, algined output dataframe). + """ + + data = self._batch_data(df) + + inputs = data["inputs"] + time = data["time"] + identifier = data["identifier"] + outputs = data["outputs"] + + combined = self.model.predict(inputs, workers=16, use_multiprocessing=True, batch_size=self.minibatch_size) + + # Format output_csv + if self.output_size != 1: + raise NotImplementedError("Current version only supports 1D targets!") + + def format_outputs(prediction): + """Returns formatted dataframes for prediction.""" + + flat_prediction = pd.DataFrame( + prediction[:, :, 0], columns=["t+{}".format(i) for i in range(self.time_steps - self.num_encoder_steps)] + ) + cols = list(flat_prediction.columns) + flat_prediction["forecast_time"] = time[:, self.num_encoder_steps - 1, 0] + flat_prediction["identifier"] = identifier[:, 0, 0] + + # Arrange in order + return flat_prediction[["forecast_time", "identifier"] + cols] + + # Extract predictions for each quantile into different entries + process_map = { + "p{}".format(int(q * 100)): combined[Ellipsis, i * self.output_size : (i + 1) * self.output_size] + for i, q in enumerate(self.quantiles) + } + + if return_targets: + # Add targets if relevant + process_map["targets"] = outputs + + return {k: format_outputs(process_map[k]) for k in process_map} + + def get_attention(self, df): + """Computes TFT attention weights for a given dataset. + + Args: + df: Input dataframe + + Returns: + Dictionary of numpy arrays for temporal attention weights and variable + selection weights, along with their identifiers and time indices + """ + + data = self._batch_data(df) + inputs = data["inputs"] + identifiers = data["identifier"] + time = data["time"] + + def get_batch_attention_weights(input_batch): + """Returns weights for a given minibatch of data.""" + input_placeholder = self._input_placeholder + attention_weights = {} + for k in self._attention_components: + attention_weight = tf.keras.backend.get_session().run( + self._attention_components[k], {input_placeholder: input_batch.astype(np.float32)} + ) + attention_weights[k] = attention_weight + return attention_weights + + # Compute number of batches + batch_size = self.minibatch_size + n = inputs.shape[0] + num_batches = n // batch_size + if n - (num_batches * batch_size) > 0: + num_batches += 1 + + # Split up inputs into batches + batched_inputs = [inputs[i * batch_size : (i + 1) * batch_size, Ellipsis] for i in range(num_batches)] + + # Get attention weights, while avoiding large memory increases + attention_by_batch = [get_batch_attention_weights(batch) for batch in batched_inputs] + attention_weights = {} + for k in self._attention_components: + attention_weights[k] = [] + for batch_weights in attention_by_batch: + attention_weights[k].append(batch_weights[k]) + + if len(attention_weights[k][0].shape) == 4: + tmp = np.concatenate(attention_weights[k], axis=1) + else: + tmp = np.concatenate(attention_weights[k], axis=0) + + del attention_weights[k] + gc.collect() + attention_weights[k] = tmp + + attention_weights["identifiers"] = identifiers[:, 0, 0] + attention_weights["time"] = time[:, :, 0] + + return attention_weights + + # Serialisation. + def reset_temp_folder(self): + """Deletes and recreates folder with temporary Keras training outputs.""" + print("Resetting temp folder...") + utils.create_folder_if_not_exist(self._temp_folder) + shutil.rmtree(self._temp_folder) + os.makedirs(self._temp_folder) + + def get_keras_saved_path(self, model_folder): + """Returns path to keras checkpoint.""" + return os.path.join(model_folder, "{}.check".format(self.name)) + + def save(self, model_folder): + """Saves optimal TFT weights. + + Args: + model_folder: Location to serialze model. + """ + # Allows for direct serialisation of tensorflow variables to avoid spurious + # issue with Keras that leads to different performance evaluation results + # when model is reloaded (https://github.com/keras-team/keras/issues/4875). + + utils.save(tf.keras.backend.get_session(), model_folder, cp_name=self.name, scope=self.name) + + def load(self, model_folder, use_keras_loadings=False): + """Loads TFT weights. + + Args: + model_folder: Folder containing serialized models. + use_keras_loadings: Whether to load from Keras checkpoint. + + Returns: + + """ + if use_keras_loadings: + # Loads temporary Keras model saved during training. + serialisation_path = self.get_keras_saved_path(model_folder) + print("Loading model from {}".format(serialisation_path)) + self.model.load_weights(serialisation_path) + else: + # Loads tensorflow graph for optimal models. + utils.load(tf.keras.backend.get_session(), model_folder, cp_name=self.name, scope=self.name) + + @classmethod + def get_hyperparm_choices(cls): + """Returns hyperparameter ranges for random search.""" + return { + "dropout_rate": [0.1, 0.2, 0.3, 0.4, 0.5, 0.7, 0.9], + "hidden_layer_size": [10, 20, 40, 80, 160, 240, 320], + "minibatch_size": [64, 128, 256], + "learning_rate": [1e-4, 1e-3, 1e-2], + "max_gradient_norm": [0.01, 1.0, 100.0], + "num_heads": [1, 4], + "stack_size": [1], + } diff --git a/examples/benchmarks/TFT/libs/utils.py b/examples/benchmarks/TFT/libs/utils.py index 813d4b176..4682434d6 100644 --- a/examples/benchmarks/TFT/libs/utils.py +++ b/examples/benchmarks/TFT/libs/utils.py @@ -1,236 +1,224 @@ -# coding=utf-8 -# Copyright 2020 The Google Research Authors. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# Lint as: python3 -"""Generic helper functions used across codebase.""" - -import os -import pathlib - -import numpy as np -import tensorflow as tf -from tensorflow.python.tools.inspect_checkpoint import print_tensors_in_checkpoint_file - - -# Generic. -def get_single_col_by_input_type(input_type, column_definition): - """Returns name of single column. - - Args: - input_type: Input type of column to extract - column_definition: Column definition list for experiment - """ - - l = [tup[0] for tup in column_definition if tup[2] == input_type] - - if len(l) != 1: - raise ValueError('Invalid number of columns for {}'.format(input_type)) - - return l[0] - - -def extract_cols_from_data_type(data_type, column_definition, - excluded_input_types): - """Extracts the names of columns that correspond to a define data_type. - - Args: - data_type: DataType of columns to extract. - column_definition: Column definition to use. - excluded_input_types: Set of input types to exclude - - Returns: - List of names for columns with data type specified. - """ - return [ - tup[0] - for tup in column_definition - if tup[1] == data_type and tup[2] not in excluded_input_types - ] - - -# Loss functions. -def tensorflow_quantile_loss(y, y_pred, quantile): - """Computes quantile loss for tensorflow. - - Standard quantile loss as defined in the "Training Procedure" section of - the main TFT paper - - Args: - y: Targets - y_pred: Predictions - quantile: Quantile to use for loss calculations (between 0 & 1) - - Returns: - Tensor for quantile loss. - """ - - # Checks quantile - if quantile < 0 or quantile > 1: - raise ValueError( - 'Illegal quantile value={}! Values should be between 0 and 1.'.format( - quantile)) - - prediction_underflow = y - y_pred - q_loss = quantile * tf.maximum(prediction_underflow, 0.) + ( - 1. - quantile) * tf.maximum(-prediction_underflow, 0.) - - return tf.reduce_sum(q_loss, axis=-1) - - -def numpy_normalised_quantile_loss(y, y_pred, quantile): - """Computes normalised quantile loss for numpy arrays. - - Uses the q-Risk metric as defined in the "Training Procedure" section of the - main TFT paper. - - Args: - y: Targets - y_pred: Predictions - quantile: Quantile to use for loss calculations (between 0 & 1) - - Returns: - Float for normalised quantile loss. - """ - prediction_underflow = y - y_pred - weighted_errors = quantile * np.maximum(prediction_underflow, 0.) \ - + (1. - quantile) * np.maximum(-prediction_underflow, 0.) - - quantile_loss = weighted_errors.mean() - normaliser = y.abs().mean() - - return 2 * quantile_loss / normaliser - - -# OS related functions. -def create_folder_if_not_exist(directory): - """Creates folder if it doesn't exist. - - Args: - directory: Folder path to create. - """ - # Also creates directories recursively - pathlib.Path(directory).mkdir(parents=True, exist_ok=True) - - -# Tensorflow related functions. -def get_default_tensorflow_config(tf_device='gpu', gpu_id=0): - """Creates tensorflow config for graphs to run on CPU or GPU. - - Specifies whether to run graph on gpu or cpu and which GPU ID to use for multi - GPU machines. - - Args: - tf_device: 'cpu' or 'gpu' - gpu_id: GPU ID to use if relevant - - Returns: - Tensorflow config. - """ - - if tf_device == 'cpu': - os.environ['CUDA_VISIBLE_DEVICES'] = '-1' # for training on cpu - tf_config = tf.ConfigProto( - log_device_placement=False, device_count={'GPU': 0}) - - else: - os.environ['CUDA_DEVICE_ORDER'] = 'PCI_BUS_ID' - os.environ['CUDA_VISIBLE_DEVICES'] = str(gpu_id) - - print('Selecting GPU ID={}'.format(gpu_id)) - - tf_config = tf.ConfigProto(log_device_placement=False) - tf_config.gpu_options.allow_growth = True - - return tf_config - - -def save(tf_session, model_folder, cp_name, scope=None): - """Saves Tensorflow graph to checkpoint. - - Saves all trainiable variables under a given variable scope to checkpoint. - - Args: - tf_session: Session containing graph - model_folder: Folder to save models - cp_name: Name of Tensorflow checkpoint - scope: Variable scope containing variables to save - """ - # Save model - if scope is None: - saver = tf.train.Saver() - else: - var_list = tf.get_collection(tf.GraphKeys.TRAINABLE_VARIABLES, scope=scope) - saver = tf.train.Saver(var_list=var_list, max_to_keep=100000) - - save_path = saver.save(tf_session, - os.path.join(model_folder, '{0}.ckpt'.format(cp_name))) - print('Model saved to: {0}'.format(save_path)) - - -def load(tf_session, model_folder, cp_name, scope=None, verbose=False): - """Loads Tensorflow graph from checkpoint. - - Args: - tf_session: Session to load graph into - model_folder: Folder containing serialised model - cp_name: Name of Tensorflow checkpoint - scope: Variable scope to use. - verbose: Whether to print additional debugging information. - """ - # Load model proper - load_path = os.path.join(model_folder, '{0}.ckpt'.format(cp_name)) - - print('Loading model from {0}'.format(load_path)) - - print_weights_in_checkpoint(model_folder, cp_name) - - initial_vars = set( - [v.name for v in tf.get_default_graph().as_graph_def().node]) - - # Saver - if scope is None: - saver = tf.train.Saver() - else: - var_list = tf.get_collection(tf.GraphKeys.GLOBAL_VARIABLES, scope=scope) - saver = tf.train.Saver(var_list=var_list, max_to_keep=100000) - # Load - saver.restore(tf_session, load_path) - all_vars = set([v.name for v in tf.get_default_graph().as_graph_def().node]) - - if verbose: - print('Restored {0}'.format(','.join(initial_vars.difference(all_vars)))) - print('Existing {0}'.format(','.join(all_vars.difference(initial_vars)))) - print('All {0}'.format(','.join(all_vars))) - - print('Done.') - - -def print_weights_in_checkpoint(model_folder, cp_name): - """Prints all weights in Tensorflow checkpoint. - - Args: - model_folder: Folder containing checkpoint - cp_name: Name of checkpoint - - Returns: - - """ - load_path = os.path.join(model_folder, '{0}.ckpt'.format(cp_name)) - - print_tensors_in_checkpoint_file( - file_name=load_path, - tensor_name='', - all_tensors=True, - all_tensor_names=True) +# coding=utf-8 +# Copyright 2020 The Google Research Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Lint as: python3 +"""Generic helper functions used across codebase.""" + +import os +import pathlib + +import numpy as np +import tensorflow as tf +from tensorflow.python.tools.inspect_checkpoint import print_tensors_in_checkpoint_file + + +# Generic. +def get_single_col_by_input_type(input_type, column_definition): + """Returns name of single column. + + Args: + input_type: Input type of column to extract + column_definition: Column definition list for experiment + """ + + l = [tup[0] for tup in column_definition if tup[2] == input_type] + + if len(l) != 1: + raise ValueError("Invalid number of columns for {}".format(input_type)) + + return l[0] + + +def extract_cols_from_data_type(data_type, column_definition, excluded_input_types): + """Extracts the names of columns that correspond to a define data_type. + + Args: + data_type: DataType of columns to extract. + column_definition: Column definition to use. + excluded_input_types: Set of input types to exclude + + Returns: + List of names for columns with data type specified. + """ + return [tup[0] for tup in column_definition if tup[1] == data_type and tup[2] not in excluded_input_types] + + +# Loss functions. +def tensorflow_quantile_loss(y, y_pred, quantile): + """Computes quantile loss for tensorflow. + + Standard quantile loss as defined in the "Training Procedure" section of + the main TFT paper + + Args: + y: Targets + y_pred: Predictions + quantile: Quantile to use for loss calculations (between 0 & 1) + + Returns: + Tensor for quantile loss. + """ + + # Checks quantile + if quantile < 0 or quantile > 1: + raise ValueError("Illegal quantile value={}! Values should be between 0 and 1.".format(quantile)) + + prediction_underflow = y - y_pred + q_loss = quantile * tf.maximum(prediction_underflow, 0.0) + (1.0 - quantile) * tf.maximum( + -prediction_underflow, 0.0 + ) + + return tf.reduce_sum(q_loss, axis=-1) + + +def numpy_normalised_quantile_loss(y, y_pred, quantile): + """Computes normalised quantile loss for numpy arrays. + + Uses the q-Risk metric as defined in the "Training Procedure" section of the + main TFT paper. + + Args: + y: Targets + y_pred: Predictions + quantile: Quantile to use for loss calculations (between 0 & 1) + + Returns: + Float for normalised quantile loss. + """ + prediction_underflow = y - y_pred + weighted_errors = quantile * np.maximum(prediction_underflow, 0.0) + (1.0 - quantile) * np.maximum( + -prediction_underflow, 0.0 + ) + + quantile_loss = weighted_errors.mean() + normaliser = y.abs().mean() + + return 2 * quantile_loss / normaliser + + +# OS related functions. +def create_folder_if_not_exist(directory): + """Creates folder if it doesn't exist. + + Args: + directory: Folder path to create. + """ + # Also creates directories recursively + pathlib.Path(directory).mkdir(parents=True, exist_ok=True) + + +# Tensorflow related functions. +def get_default_tensorflow_config(tf_device="gpu", gpu_id=0): + """Creates tensorflow config for graphs to run on CPU or GPU. + + Specifies whether to run graph on gpu or cpu and which GPU ID to use for multi + GPU machines. + + Args: + tf_device: 'cpu' or 'gpu' + gpu_id: GPU ID to use if relevant + + Returns: + Tensorflow config. + """ + + if tf_device == "cpu": + os.environ["CUDA_VISIBLE_DEVICES"] = "-1" # for training on cpu + tf_config = tf.ConfigProto(log_device_placement=False, device_count={"GPU": 0}) + + else: + os.environ["CUDA_DEVICE_ORDER"] = "PCI_BUS_ID" + os.environ["CUDA_VISIBLE_DEVICES"] = str(gpu_id) + + print("Selecting GPU ID={}".format(gpu_id)) + + tf_config = tf.ConfigProto(log_device_placement=False) + tf_config.gpu_options.allow_growth = True + + return tf_config + + +def save(tf_session, model_folder, cp_name, scope=None): + """Saves Tensorflow graph to checkpoint. + + Saves all trainiable variables under a given variable scope to checkpoint. + + Args: + tf_session: Session containing graph + model_folder: Folder to save models + cp_name: Name of Tensorflow checkpoint + scope: Variable scope containing variables to save + """ + # Save model + if scope is None: + saver = tf.train.Saver() + else: + var_list = tf.get_collection(tf.GraphKeys.TRAINABLE_VARIABLES, scope=scope) + saver = tf.train.Saver(var_list=var_list, max_to_keep=100000) + + save_path = saver.save(tf_session, os.path.join(model_folder, "{0}.ckpt".format(cp_name))) + print("Model saved to: {0}".format(save_path)) + + +def load(tf_session, model_folder, cp_name, scope=None, verbose=False): + """Loads Tensorflow graph from checkpoint. + + Args: + tf_session: Session to load graph into + model_folder: Folder containing serialised model + cp_name: Name of Tensorflow checkpoint + scope: Variable scope to use. + verbose: Whether to print additional debugging information. + """ + # Load model proper + load_path = os.path.join(model_folder, "{0}.ckpt".format(cp_name)) + + print("Loading model from {0}".format(load_path)) + + print_weights_in_checkpoint(model_folder, cp_name) + + initial_vars = set([v.name for v in tf.get_default_graph().as_graph_def().node]) + + # Saver + if scope is None: + saver = tf.train.Saver() + else: + var_list = tf.get_collection(tf.GraphKeys.GLOBAL_VARIABLES, scope=scope) + saver = tf.train.Saver(var_list=var_list, max_to_keep=100000) + # Load + saver.restore(tf_session, load_path) + all_vars = set([v.name for v in tf.get_default_graph().as_graph_def().node]) + + if verbose: + print("Restored {0}".format(",".join(initial_vars.difference(all_vars)))) + print("Existing {0}".format(",".join(all_vars.difference(initial_vars)))) + print("All {0}".format(",".join(all_vars))) + + print("Done.") + + +def print_weights_in_checkpoint(model_folder, cp_name): + """Prints all weights in Tensorflow checkpoint. + + Args: + model_folder: Folder containing checkpoint + cp_name: Name of checkpoint + + Returns: + + """ + load_path = os.path.join(model_folder, "{0}.ckpt".format(cp_name)) + + print_tensors_in_checkpoint_file(file_name=load_path, tensor_name="", all_tensors=True, all_tensor_names=True) diff --git a/examples/benchmarks/TFT/tft.py b/examples/benchmarks/TFT/tft.py index ee49a1eb7..631204a3d 100644 --- a/examples/benchmarks/TFT/tft.py +++ b/examples/benchmarks/TFT/tft.py @@ -1,246 +1,248 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. - -import numpy as np -import pandas as pd -import tensorflow.compat.v1 as tf -import data_formatters.base -import expt_settings.configs -import libs.hyperparam_opt -import libs.tft_model -import libs.utils as utils -import os -import datetime as dte - - -from qlib.model.base import ModelFT -from qlib.data.dataset import DatasetH -from qlib.data.dataset.handler import DataHandlerLP - - - -# To register new datasets, please add them here. -ALLOW_DATASET = ['Alpha158'] -DATASET_SETTING = { - 'Alpha158': { - 'feature_col': ['RESI5', 'WVMA5', 'RSQR5', 'KLEN', 'RSQR10', 'CORR5', 'CORD5', 'CORR10', 'ROC60', 'RESI10'], - 'label_col': ['LABEL0'], - }, -} -# To register new datasets, please add their configurations here. - -def get_shifted_label(data_df, shifts=5, col_shift='LABEL0'): - return data_df[[col_shift]].groupby('instrument').apply(lambda df: df.shift(shifts)) - -def fill_test_na(test_df): - test_df_res = test_df.copy() - feature_cols = ~test_df_res.columns.str.contains('label', case=False) - test_feature_fna = test_df_res.loc[:, feature_cols].groupby('datetime').apply(lambda df: df.fillna(df.mean())) - test_df_res.loc[:, feature_cols] = test_feature_fna - return test_df_res - -def process_qlib_data(df, dataset, fillna=False): - """Prepare data to fit the TFT model. - - Args: - df: Original DataFrame. - fillna: Whether to fill the data with the mean values. - - Returns: - Transformed DataFrame. - - """ - # Several features selected manually - feature_col = DATASET_SETTING[dataset]['feature_col'] - label_col = DATASET_SETTING[dataset]['label_col'] - temp_df = df.loc[:, feature_col+label_col] - if fillna: - temp_df = fill_test_na(temp_df) - temp_df = temp_df.swaplevel() - temp_df = temp_df.sort_index() - temp_df = temp_df.reset_index(level=0) - dates = pd.to_datetime(temp_df.index) - temp_df['date'] = dates - temp_df['day_of_week'] = dates.dayofweek - temp_df['month'] = dates.month - temp_df['year'] = dates.year - temp_df['const'] = 1.0 - return temp_df - -def process_predicted(df, col_name): - """Transform the TFT predicted data into Qlib format. - - Args: - df: Original DataFrame. - fillna: New column name. - - Returns: - Transformed DataFrame. - - """ - df_res = df.copy() - df_res = df_res.rename(columns={"forecast_time": "datetime", "identifier": "instrument", "t+0": col_name}) - df_res = df_res.set_index(['datetime','instrument']).sort_index() - df_res = df_res[[col_name]] - return df_res - -def format_score(forecast_df, col_name='pred', label_shift=5): - pred = process_predicted(forecast_df, col_name=col_name) - pred = get_shifted_label(pred, shifts=-label_shift, col_shift=col_name) - pred = pred.dropna()[col_name] - return pred - -def transform_df(df, col_name='LABEL0'): - df_res = df['feature'] - df_res[col_name] = df['label'] - return df_res - -class TFTModel(ModelFT): - """TFT Model""" - - def __init__(self, **kwargs): - self.model = None - - def _prepare_data(self, dataset: DatasetH): - df_train, df_valid = dataset.prepare( - ["train", "valid"], col_set=["feature", "label"], data_key=DataHandlerLP.DK_L - ) - return transform_df(df_train), transform_df(df_valid) - - def fit( - self, - dataset: DatasetH, - DATASET = 'Alpha158', - MODEL_FOLDER = 'qlib_alpha158_model', - LABEL_COL = 'LABEL0', - LABEL_SHIFT = 5, - USE_GPU_ID = 0, - **kwargs - ): - - if DATASET not in ALLOW_DATASET: - raise AssertionError("The dataset is not supported, please make a new formatter to fit this dataset") - - dtrain, dvalid = self._prepare_data(dataset) - dtrain.loc[:, LABEL_COL] = get_shifted_label(dtrain, shifts=LABEL_SHIFT, col_shift=LABEL_COL) - dvalid.loc[:, LABEL_COL] = get_shifted_label(dvalid, shifts=LABEL_SHIFT, col_shift=LABEL_COL) - - - train = process_qlib_data(dtrain, DATASET, fillna=True).dropna() - valid = process_qlib_data(dvalid, DATASET, fillna=True).dropna() - - ExperimentConfig = expt_settings.configs.ExperimentConfig - config = ExperimentConfig(DATASET) - self.data_formatter = config.make_data_formatter() - self.model_folder = MODEL_FOLDER - self.gpu_id = USE_GPU_ID - self.label_shift = LABEL_SHIFT - self.expt_name = DATASET - self.label_col = LABEL_COL - - use_gpu = (True, self.gpu_id) - #===========================Training Process=========================== - ModelClass = libs.tft_model.TemporalFusionTransformer - if not isinstance(self.data_formatter, data_formatters.base.GenericDataFormatter): - raise ValueError( - "Data formatters should inherit from" + - "AbstractDataFormatter! Type={}".format(type(self.data_formatter))) - - default_keras_session = tf.keras.backend.get_session() - - if use_gpu[0]: - self.tf_config = utils.get_default_tensorflow_config(tf_device="gpu", gpu_id=use_gpu[1]) - else: - self.tf_config = utils.get_default_tensorflow_config(tf_device="cpu") - - self.data_formatter.set_scalers(train) - - # Sets up default params - fixed_params = self.data_formatter.get_experiment_params() - params = self.data_formatter.get_default_model_params() - - # Wendi: 合并调优的参数和非调优的参数 - params = {**params, **fixed_params} - - if not os.path.exists(self.model_folder): - os.makedirs(self.model_folder) - params['model_folder'] = self.model_folder - - print("*** Begin training ***") - best_loss = np.Inf - - tf.reset_default_graph() - - self.tf_graph = tf.Graph() - with self.tf_graph.as_default(): - self.sess = tf.Session(config=self.tf_config) - tf.keras.backend.set_session(self.sess) - self.model = ModelClass(params, use_cudnn=use_gpu[0]) - self.sess.run(tf.global_variables_initializer()) - self.model.fit(train_df=train, valid_df=valid) - print("*** Finished training ***") - saved_model_dir = self.model_folder+'/'+'saved_model' - if not os.path.exists(saved_model_dir): - os.makedirs(saved_model_dir) - self.model.save(saved_model_dir) - - def extract_numerical_data(data): - """Strips out forecast time and identifier columns.""" - return data[[ - col for col in data.columns - if col not in {"forecast_time", "identifier"} - ]] - - #p50_loss = utils.numpy_normalised_quantile_loss( - # extract_numerical_data(targets), extract_numerical_data(p50_forecast), - # 0.5) - #p90_loss = utils.numpy_normalised_quantile_loss( - # extract_numerical_data(targets), extract_numerical_data(p90_forecast), - # 0.9) - tf.keras.backend.set_session(default_keras_session) - print("Training completed.".format(dte.datetime.now())) - #===========================Training Process=========================== - - def predict(self, dataset): - if self.model is None: - raise ValueError("model is not fitted yet!") - d_test = dataset.prepare("test", col_set=["feature", "label"]) - d_test = transform_df(d_test) - d_test.loc[:, self.label_col] = get_shifted_label(d_test, shifts=self.label_shift, col_shift=self.label_col) - test = process_qlib_data(d_test, self.expt_name, fillna=True).dropna() - - use_gpu = (True, self.gpu_id) - #===========================Predicting Process=========================== - default_keras_session = tf.keras.backend.get_session() - - # Sets up default params - fixed_params = self.data_formatter.get_experiment_params() - params = self.data_formatter.get_default_model_params() - params = {**params, **fixed_params} - - - print("*** Begin predicting ***") - tf.reset_default_graph() - - with self.tf_graph.as_default(): - tf.keras.backend.set_session(self.sess) - output_map = self.model.predict(test, return_targets=True) - targets = self.data_formatter.format_predictions(output_map["targets"]) - p50_forecast = self.data_formatter.format_predictions(output_map["p50"]) - p90_forecast = self.data_formatter.format_predictions(output_map["p90"]) - tf.keras.backend.set_session(default_keras_session) - - predict = format_score(p90_forecast, 'pred', self.label_shift) - label = format_score(targets, 'label', self.label_shift) - #===========================Predicting Process=========================== - return predict, label - - def finetune(self, dataset: DatasetH): - """ - finetune model - Parameters - ---------- - dataset : DatasetH - dataset for finetuning - """ - pass +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +import numpy as np +import pandas as pd +import tensorflow.compat.v1 as tf +import data_formatters.base +import expt_settings.configs +import libs.hyperparam_opt +import libs.tft_model +import libs.utils as utils +import os +import datetime as dte + + +from qlib.model.base import ModelFT +from qlib.data.dataset import DatasetH +from qlib.data.dataset.handler import DataHandlerLP + + +# To register new datasets, please add them here. +ALLOW_DATASET = ["Alpha158"] +DATASET_SETTING = { + "Alpha158": { + "feature_col": ["RESI5", "WVMA5", "RSQR5", "KLEN", "RSQR10", "CORR5", "CORD5", "CORR10", "ROC60", "RESI10"], + "label_col": ["LABEL0"], + }, +} +# To register new datasets, please add their configurations here. + + +def get_shifted_label(data_df, shifts=5, col_shift="LABEL0"): + return data_df[[col_shift]].groupby("instrument").apply(lambda df: df.shift(shifts)) + + +def fill_test_na(test_df): + test_df_res = test_df.copy() + feature_cols = ~test_df_res.columns.str.contains("label", case=False) + test_feature_fna = test_df_res.loc[:, feature_cols].groupby("datetime").apply(lambda df: df.fillna(df.mean())) + test_df_res.loc[:, feature_cols] = test_feature_fna + return test_df_res + + +def process_qlib_data(df, dataset, fillna=False): + """Prepare data to fit the TFT model. + + Args: + df: Original DataFrame. + fillna: Whether to fill the data with the mean values. + + Returns: + Transformed DataFrame. + + """ + # Several features selected manually + feature_col = DATASET_SETTING[dataset]["feature_col"] + label_col = DATASET_SETTING[dataset]["label_col"] + temp_df = df.loc[:, feature_col + label_col] + if fillna: + temp_df = fill_test_na(temp_df) + temp_df = temp_df.swaplevel() + temp_df = temp_df.sort_index() + temp_df = temp_df.reset_index(level=0) + dates = pd.to_datetime(temp_df.index) + temp_df["date"] = dates + temp_df["day_of_week"] = dates.dayofweek + temp_df["month"] = dates.month + temp_df["year"] = dates.year + temp_df["const"] = 1.0 + return temp_df + + +def process_predicted(df, col_name): + """Transform the TFT predicted data into Qlib format. + + Args: + df: Original DataFrame. + fillna: New column name. + + Returns: + Transformed DataFrame. + + """ + df_res = df.copy() + df_res = df_res.rename(columns={"forecast_time": "datetime", "identifier": "instrument", "t+0": col_name}) + df_res = df_res.set_index(["datetime", "instrument"]).sort_index() + df_res = df_res[[col_name]] + return df_res + + +def format_score(forecast_df, col_name="pred", label_shift=5): + pred = process_predicted(forecast_df, col_name=col_name) + pred = get_shifted_label(pred, shifts=-label_shift, col_shift=col_name) + pred = pred.dropna()[col_name] + return pred + + +def transform_df(df, col_name="LABEL0"): + df_res = df["feature"] + df_res[col_name] = df["label"] + return df_res + + +class TFTModel(ModelFT): + """TFT Model""" + + def __init__(self, **kwargs): + self.model = None + + def _prepare_data(self, dataset: DatasetH): + df_train, df_valid = dataset.prepare( + ["train", "valid"], col_set=["feature", "label"], data_key=DataHandlerLP.DK_L + ) + return transform_df(df_train), transform_df(df_valid) + + def fit( + self, + dataset: DatasetH, + DATASET="Alpha158", + MODEL_FOLDER="qlib_alpha158_model", + LABEL_COL="LABEL0", + LABEL_SHIFT=5, + USE_GPU_ID=0, + **kwargs + ): + + if DATASET not in ALLOW_DATASET: + raise AssertionError("The dataset is not supported, please make a new formatter to fit this dataset") + + dtrain, dvalid = self._prepare_data(dataset) + dtrain.loc[:, LABEL_COL] = get_shifted_label(dtrain, shifts=LABEL_SHIFT, col_shift=LABEL_COL) + dvalid.loc[:, LABEL_COL] = get_shifted_label(dvalid, shifts=LABEL_SHIFT, col_shift=LABEL_COL) + + train = process_qlib_data(dtrain, DATASET, fillna=True).dropna() + valid = process_qlib_data(dvalid, DATASET, fillna=True).dropna() + + ExperimentConfig = expt_settings.configs.ExperimentConfig + config = ExperimentConfig(DATASET) + self.data_formatter = config.make_data_formatter() + self.model_folder = MODEL_FOLDER + self.gpu_id = USE_GPU_ID + self.label_shift = LABEL_SHIFT + self.expt_name = DATASET + self.label_col = LABEL_COL + + use_gpu = (True, self.gpu_id) + # ===========================Training Process=========================== + ModelClass = libs.tft_model.TemporalFusionTransformer + if not isinstance(self.data_formatter, data_formatters.base.GenericDataFormatter): + raise ValueError( + "Data formatters should inherit from" + + "AbstractDataFormatter! Type={}".format(type(self.data_formatter)) + ) + + default_keras_session = tf.keras.backend.get_session() + + if use_gpu[0]: + self.tf_config = utils.get_default_tensorflow_config(tf_device="gpu", gpu_id=use_gpu[1]) + else: + self.tf_config = utils.get_default_tensorflow_config(tf_device="cpu") + + self.data_formatter.set_scalers(train) + + # Sets up default params + fixed_params = self.data_formatter.get_experiment_params() + params = self.data_formatter.get_default_model_params() + + # Wendi: 合并调优的参数和非调优的参数 + params = {**params, **fixed_params} + + if not os.path.exists(self.model_folder): + os.makedirs(self.model_folder) + params["model_folder"] = self.model_folder + + print("*** Begin training ***") + best_loss = np.Inf + + tf.reset_default_graph() + + self.tf_graph = tf.Graph() + with self.tf_graph.as_default(): + self.sess = tf.Session(config=self.tf_config) + tf.keras.backend.set_session(self.sess) + self.model = ModelClass(params, use_cudnn=use_gpu[0]) + self.sess.run(tf.global_variables_initializer()) + self.model.fit(train_df=train, valid_df=valid) + print("*** Finished training ***") + saved_model_dir = self.model_folder + "/" + "saved_model" + if not os.path.exists(saved_model_dir): + os.makedirs(saved_model_dir) + self.model.save(saved_model_dir) + + def extract_numerical_data(data): + """Strips out forecast time and identifier columns.""" + return data[[col for col in data.columns if col not in {"forecast_time", "identifier"}]] + + # p50_loss = utils.numpy_normalised_quantile_loss( + # extract_numerical_data(targets), extract_numerical_data(p50_forecast), + # 0.5) + # p90_loss = utils.numpy_normalised_quantile_loss( + # extract_numerical_data(targets), extract_numerical_data(p90_forecast), + # 0.9) + tf.keras.backend.set_session(default_keras_session) + print("Training completed.".format(dte.datetime.now())) + # ===========================Training Process=========================== + + def predict(self, dataset): + if self.model is None: + raise ValueError("model is not fitted yet!") + d_test = dataset.prepare("test", col_set=["feature", "label"]) + d_test = transform_df(d_test) + d_test.loc[:, self.label_col] = get_shifted_label(d_test, shifts=self.label_shift, col_shift=self.label_col) + test = process_qlib_data(d_test, self.expt_name, fillna=True).dropna() + + use_gpu = (True, self.gpu_id) + # ===========================Predicting Process=========================== + default_keras_session = tf.keras.backend.get_session() + + # Sets up default params + fixed_params = self.data_formatter.get_experiment_params() + params = self.data_formatter.get_default_model_params() + params = {**params, **fixed_params} + + print("*** Begin predicting ***") + tf.reset_default_graph() + + with self.tf_graph.as_default(): + tf.keras.backend.set_session(self.sess) + output_map = self.model.predict(test, return_targets=True) + targets = self.data_formatter.format_predictions(output_map["targets"]) + p50_forecast = self.data_formatter.format_predictions(output_map["p50"]) + p90_forecast = self.data_formatter.format_predictions(output_map["p90"]) + tf.keras.backend.set_session(default_keras_session) + + predict = format_score(p90_forecast, "pred", self.label_shift) + label = format_score(targets, "label", self.label_shift) + # ===========================Predicting Process=========================== + return predict, label + + def finetune(self, dataset: DatasetH): + """ + finetune model + Parameters + ---------- + dataset : DatasetH + dataset for finetuning + """ + pass diff --git a/examples/benchmarks/TFT/workflow_by_code_tft.py b/examples/benchmarks/TFT/workflow_by_code_tft.py index 593ac468f..64c7d3df5 100644 --- a/examples/benchmarks/TFT/workflow_by_code_tft.py +++ b/examples/benchmarks/TFT/workflow_by_code_tft.py @@ -1,130 +1,132 @@ - #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.pytorch_lstm import LSTM -from qlib.contrib.data.handler import ALPHA360_Denoise -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 - -# from qlib.model.learner import train_model -from qlib.utils import init_instance_by_config - -import pickle -from tft import TFTModel - -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_cn(target_dir=provider_uri) - - qlib.init(provider_uri=provider_uri, region=REG_CN) - - MARKET = "csi300" - BENCHMARK = "SH000300" - - ################################### - # train model - ################################### - DATA_HANDLER_CONFIG = { - "start_time": "2008-01-01", - "end_time": "2020-08-01", - "fit_start_time": "2008-01-01", - "fit_end_time": "2014-12-31", - "instruments": MARKET, - } - - TRAINER_CONFIG = { - "train_start_time": "2008-01-01", - "train_end_time": "2014-12-31", - "validate_start_time": "2015-01-01", - "validate_end_time": "2016-12-31", - "test_start_time": "2017-01-01", - "test_end_time": "2020-08-01", - } - - task = { - "dataset": { - "class": "DatasetH", - "module_path": "qlib.data.dataset", - "kwargs": { - 'handler': { - "class": "Alpha158", - "module_path": "qlib.contrib.data.handler", - "kwargs": DATA_HANDLER_CONFIG - }, - 'segments': { - 'train': ("2008-01-01", "2014-12-31"), - 'valid': ("2015-01-01", "2016-12-31",), - 'test': ("2017-01-01", "2020-08-01",), - } - } - } - # You shoud record the data in specific sequence - # "record": ['SignalRecord', 'SigAnaRecord', 'PortAnaRecord'], - } - - - model = TFTModel() - dataset = init_instance_by_config(task["dataset"]) - model.fit(dataset) - - pred_score, label_score = model.predict(dataset) - - # save pred_score to file - pred_score_path = Path("~/tmp/qlib/pred_score.pkl").expanduser() - pred_score_path.parent.mkdir(exist_ok=True, parents=True) - pred_score.to_pickle(pred_score_path) - - - ################################### - # backtest - ################################### - STRATEGY_CONFIG = { - "topk": 50, - "n_drop": 5, - } - BACKTEST_CONFIG = { - "verbose": False, - "limit_threshold": 0.095, - "account": 100000000, - "benchmark": BENCHMARK, - "deal_price": "close", - "open_cost": 0.0005, - "close_cost": 0.0015, - "min_cost": 5, - } - - # use default strategy - # custom Strategy, refer to: TODO: Strategy API url - strategy = TopkDropoutStrategy(**STRATEGY_CONFIG) - report_normal, positions_normal = normal_backtest(pred_score, strategy=strategy, **BACKTEST_CONFIG) - - ################################### - # analyze - # If need a more detailed analysis, refer to: examples/train_and_bakctest.ipynb - ################################### - 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 - print(analysis_df) - - +# 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.pytorch_lstm import LSTM +from qlib.contrib.data.handler import ALPHA360_Denoise +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 + +# from qlib.model.learner import train_model +from qlib.utils import init_instance_by_config + +import pickle +from tft import TFTModel + +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_cn(target_dir=provider_uri) + + qlib.init(provider_uri=provider_uri, region=REG_CN) + + MARKET = "csi300" + BENCHMARK = "SH000300" + + ################################### + # train model + ################################### + DATA_HANDLER_CONFIG = { + "start_time": "2008-01-01", + "end_time": "2020-08-01", + "fit_start_time": "2008-01-01", + "fit_end_time": "2014-12-31", + "instruments": MARKET, + } + + TRAINER_CONFIG = { + "train_start_time": "2008-01-01", + "train_end_time": "2014-12-31", + "validate_start_time": "2015-01-01", + "validate_end_time": "2016-12-31", + "test_start_time": "2017-01-01", + "test_end_time": "2020-08-01", + } + + task = { + "dataset": { + "class": "DatasetH", + "module_path": "qlib.data.dataset", + "kwargs": { + "handler": { + "class": "Alpha158", + "module_path": "qlib.contrib.data.handler", + "kwargs": DATA_HANDLER_CONFIG, + }, + "segments": { + "train": ("2008-01-01", "2014-12-31"), + "valid": ( + "2015-01-01", + "2016-12-31", + ), + "test": ( + "2017-01-01", + "2020-08-01", + ), + }, + }, + } + # You shoud record the data in specific sequence + # "record": ['SignalRecord', 'SigAnaRecord', 'PortAnaRecord'], + } + + model = TFTModel() + dataset = init_instance_by_config(task["dataset"]) + model.fit(dataset) + + pred_score, label_score = model.predict(dataset) + + # save pred_score to file + pred_score_path = Path("~/tmp/qlib/pred_score.pkl").expanduser() + pred_score_path.parent.mkdir(exist_ok=True, parents=True) + pred_score.to_pickle(pred_score_path) + + ################################### + # backtest + ################################### + STRATEGY_CONFIG = { + "topk": 50, + "n_drop": 5, + } + BACKTEST_CONFIG = { + "verbose": False, + "limit_threshold": 0.095, + "account": 100000000, + "benchmark": BENCHMARK, + "deal_price": "close", + "open_cost": 0.0005, + "close_cost": 0.0015, + "min_cost": 5, + } + + # use default strategy + # custom Strategy, refer to: TODO: Strategy API url + strategy = TopkDropoutStrategy(**STRATEGY_CONFIG) + report_normal, positions_normal = normal_backtest(pred_score, strategy=strategy, **BACKTEST_CONFIG) + + ################################### + # analyze + # If need a more detailed analysis, refer to: examples/train_and_bakctest.ipynb + ################################### + 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 + print(analysis_df) From e986f2c731f35875ae2c02c344f5b7baa0ea50c7 Mon Sep 17 00:00:00 2001 From: Wendi Li Date: Mon, 23 Nov 2020 16:25:29 +0800 Subject: [PATCH 125/241] Add files via upload --- examples/benchmarks/TFT/README.md | 10 ++++++++++ examples/benchmarks/TFT/requirements.txt | 3 +++ 2 files changed, 13 insertions(+) create mode 100644 examples/benchmarks/TFT/README.md create mode 100644 examples/benchmarks/TFT/requirements.txt diff --git a/examples/benchmarks/TFT/README.md b/examples/benchmarks/TFT/README.md new file mode 100644 index 000000000..03d45e087 --- /dev/null +++ b/examples/benchmarks/TFT/README.md @@ -0,0 +1,10 @@ +# Temporal Fusion Transformers Benchmark +## Source +https://github.com/google-research/google-research/tree/master/tft + +## Run the Workflow +Users can follow the ``workflow_by_code_tft.py`` to run the benchmark. + +### Notes +1. The model must run in GPU, or an error will be raised. +2. New datasets should be registered in ``data_formatters``, for detail please visit the source. \ No newline at end of file diff --git a/examples/benchmarks/TFT/requirements.txt b/examples/benchmarks/TFT/requirements.txt new file mode 100644 index 000000000..04234aaed --- /dev/null +++ b/examples/benchmarks/TFT/requirements.txt @@ -0,0 +1,3 @@ +tensorflow-gpu==1.15.0 +numpy == 1.19.4 +pandas==1.1.0 \ No newline at end of file From 9b7251d8d4ee8911ff66786ce47de568cff3f34e Mon Sep 17 00:00:00 2001 From: Wendi Li Date: Wed, 25 Nov 2020 17:27:35 +0800 Subject: [PATCH 126/241] Create workflow_config_tft.yaml --- .../benchmarks/TFT/workflow_config_tft.yaml | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 examples/benchmarks/TFT/workflow_config_tft.yaml diff --git a/examples/benchmarks/TFT/workflow_config_tft.yaml b/examples/benchmarks/TFT/workflow_config_tft.yaml new file mode 100644 index 000000000..1396400cb --- /dev/null +++ b/examples/benchmarks/TFT/workflow_config_tft.yaml @@ -0,0 +1,51 @@ + +provider_uri: "~/.qlib/qlib_data/cn_data" +region: cn +market: &market csi300 +benchmark: &benchmark SH000300 +data_handler_config: &data_handler_config + start_time: 2008-01-01 + end_time: 2020-08-01 + fit_start_time: 2008-01-01 + fit_end_time: 2014-12-31 + instruments: *market +port_analysis_config: &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 +task: + model: + class: TFTModel + module_path: qlib.examples.benchmarks.TFT + dataset: + class: DatasetH + module_path: qlib.data.dataset + kwargs: + handler: + class: Alpha158 + module_path: qlib.contrib.data.handler + kwargs: *data_handler_config + segments: + train: [2008-01-01, 2014-12-31] + valid: [2015-01-01, 2016-12-31] + test: [2017-01-01, 2020-08-01] + record: + - class: SignalRecord + module_path: qlib.workflow.record_temp + kwargs: {} + - class: PortAnaRecord + module_path: qlib.workflow.record_temp + kwargs: + config: *port_analysis_config From 068ad4ba9007788c9e9619670a314e81d3a87b68 Mon Sep 17 00:00:00 2001 From: Wendi Li Date: Wed, 25 Nov 2020 17:31:15 +0800 Subject: [PATCH 127/241] Update tft.py --- examples/benchmarks/TFT/tft.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/benchmarks/TFT/tft.py b/examples/benchmarks/TFT/tft.py index 631204a3d..92d957d0f 100644 --- a/examples/benchmarks/TFT/tft.py +++ b/examples/benchmarks/TFT/tft.py @@ -82,7 +82,7 @@ def process_predicted(df, col_name): """ df_res = df.copy() - df_res = df_res.rename(columns={"forecast_time": "datetime", "identifier": "instrument", "t+0": col_name}) + df_res = df_res.rename(columns={"forecast_time": "datetime", "identifier": "instrument", "t+5": col_name}) df_res = df_res.set_index(["datetime", "instrument"]).sort_index() df_res = df_res[[col_name]] return df_res @@ -232,8 +232,8 @@ class TFTModel(ModelFT): p90_forecast = self.data_formatter.format_predictions(output_map["p90"]) tf.keras.backend.set_session(default_keras_session) - predict = format_score(p90_forecast, "pred", self.label_shift) - label = format_score(targets, "label", self.label_shift) + predict = format_score(p90_forecast, "pred", 0) # self.label_shift + label = format_score(targets, "label", 0) # ===========================Predicting Process=========================== return predict, label From 1134d58b3166a80a0aaee6538f4916ea36f08edb Mon Sep 17 00:00:00 2001 From: Wendi Li Date: Wed, 25 Nov 2020 17:32:14 +0800 Subject: [PATCH 128/241] Update qlib_Alpha158.py --- examples/benchmarks/TFT/data_formatters/qlib_Alpha158.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/benchmarks/TFT/data_formatters/qlib_Alpha158.py b/examples/benchmarks/TFT/data_formatters/qlib_Alpha158.py index e9236d041..da3d14343 100644 --- a/examples/benchmarks/TFT/data_formatters/qlib_Alpha158.py +++ b/examples/benchmarks/TFT/data_formatters/qlib_Alpha158.py @@ -209,7 +209,7 @@ class Alpha158Formatter(GenericDataFormatter): model_params = { "dropout_rate": 0.3, "hidden_layer_size": 160, - "learning_rate": 0.01, + "learning_rate": 0.001, "minibatch_size": 64, "max_gradient_norm": 0.01, "num_heads": 1, From 9d3be6d894af4edc4fa019d1745f32d84c8f4096 Mon Sep 17 00:00:00 2001 From: Young Date: Wed, 25 Nov 2020 09:54:03 +0000 Subject: [PATCH 129/241] add sys section parser to the workflow config --- .../benchmarks/TFT/workflow_config_tft.yaml | 5 +-- qlib/workflow/cli.py | 32 +++++++++++++++++++ 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/examples/benchmarks/TFT/workflow_config_tft.yaml b/examples/benchmarks/TFT/workflow_config_tft.yaml index 1396400cb..d8ee14e71 100644 --- a/examples/benchmarks/TFT/workflow_config_tft.yaml +++ b/examples/benchmarks/TFT/workflow_config_tft.yaml @@ -1,4 +1,5 @@ - +sys: + rel_path: . provider_uri: "~/.qlib/qlib_data/cn_data" region: cn market: &market csi300 @@ -28,7 +29,7 @@ port_analysis_config: &port_analysis_config task: model: class: TFTModel - module_path: qlib.examples.benchmarks.TFT + module_path: tft dataset: class: DatasetH module_path: qlib.data.dataset diff --git a/qlib/workflow/cli.py b/qlib/workflow/cli.py index a946af9a7..2e087877b 100644 --- a/qlib/workflow/cli.py +++ b/qlib/workflow/cli.py @@ -13,11 +13,43 @@ from qlib.workflow import R from qlib.workflow.record_temp import SignalRecord +def get_path_list(path): + if isinstance(path, str): + return [path] + else: + return [p for p in path] + + +def sys_config(config, config_path): + """ + Configure the `sys` section + + Parameters + ---------- + config : dict + configuration of the workflow + config_path : str + configuration of the path + """ + sys_config = config.get("sys", {}) + + # abspath + for p in get_path_list(sys_config.get("path", [])): + sys.path.append(p) + + # relative path to config path + for p in get_path_list(sys_config.get("rel_path", [])): + sys.path.append(str(Path(config_path).parent.resolve().absolute() / p)) + + # worflow handler function def workflow(config_path, experiment_name="workflow"): with open(config_path) as fp: config = yaml.load(fp, Loader=yaml.Loader) + # config the `sys` section + sys_config(config, config_path) + provider_uri = config.get("provider_uri") region = config.get("region") qlib.init(provider_uri=provider_uri, region=region) From 5c1a8184883460bb322171cb909606354264ede8 Mon Sep 17 00:00:00 2001 From: Wendi Li Date: Wed, 25 Nov 2020 20:09:47 +0800 Subject: [PATCH 130/241] Update README.md --- examples/benchmarks/TFT/README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/examples/benchmarks/TFT/README.md b/examples/benchmarks/TFT/README.md index 03d45e087..6d605a1bd 100644 --- a/examples/benchmarks/TFT/README.md +++ b/examples/benchmarks/TFT/README.md @@ -1,10 +1,12 @@ # Temporal Fusion Transformers Benchmark ## Source -https://github.com/google-research/google-research/tree/master/tft +**Reference**: Lim, Bryan, et al. "Temporal fusion transformers for interpretable multi-horizon time series forecasting." arXiv preprint arXiv:1912.09363 (2019). + +**GitHub**: https://github.com/google-research/google-research/tree/master/tft ## Run the Workflow Users can follow the ``workflow_by_code_tft.py`` to run the benchmark. ### Notes 1. The model must run in GPU, or an error will be raised. -2. New datasets should be registered in ``data_formatters``, for detail please visit the source. \ No newline at end of file +2. New datasets should be registered in ``data_formatters``, for detail please visit the source. From 2952641d3f0ce4130a2732f80607017923874caf Mon Sep 17 00:00:00 2001 From: Wendi Li Date: Wed, 25 Nov 2020 20:16:13 +0800 Subject: [PATCH 131/241] Delete electricity.py --- .../TFT/data_formatters/electricity.py | 254 ------------------ 1 file changed, 254 deletions(-) delete mode 100644 examples/benchmarks/TFT/data_formatters/electricity.py diff --git a/examples/benchmarks/TFT/data_formatters/electricity.py b/examples/benchmarks/TFT/data_formatters/electricity.py deleted file mode 100644 index 366954a71..000000000 --- a/examples/benchmarks/TFT/data_formatters/electricity.py +++ /dev/null @@ -1,254 +0,0 @@ -# coding=utf-8 -# Copyright 2020 The Google Research Authors. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# Lint as: python3 -"""Custom formatting functions for Electricity dataset. - -Defines dataset specific column definitions and data transformations. Uses -entity specific z-score normalization. -""" - -import data_formatters.base -import libs.utils as utils -import pandas as pd -import sklearn.preprocessing - -GenericDataFormatter = data_formatters.base.GenericDataFormatter -DataTypes = data_formatters.base.DataTypes -InputTypes = data_formatters.base.InputTypes - - -class ElectricityFormatter(GenericDataFormatter): - """Defines and formats data for the electricity dataset. - - Note that per-entity z-score normalization is used here, and is implemented - across functions. - - Attributes: - column_definition: Defines input and data type of column used in the - experiment. - identifiers: Entity identifiers used in experiments. - """ - - _column_definition = [ - ("id", DataTypes.REAL_VALUED, InputTypes.ID), - ("hours_from_start", DataTypes.REAL_VALUED, InputTypes.TIME), - ("power_usage", DataTypes.REAL_VALUED, InputTypes.TARGET), - ("hour", DataTypes.REAL_VALUED, InputTypes.KNOWN_INPUT), - ("day_of_week", DataTypes.REAL_VALUED, InputTypes.KNOWN_INPUT), - ("hours_from_start", DataTypes.REAL_VALUED, InputTypes.KNOWN_INPUT), - ("categorical_id", DataTypes.CATEGORICAL, InputTypes.STATIC_INPUT), - ] - - def __init__(self): - """Initialises formatter.""" - - self.identifiers = None - self._real_scalers = None - self._cat_scalers = None - self._target_scaler = None - self._num_classes_per_cat_input = None - self._time_steps = self.get_fixed_params()["total_time_steps"] - - def split_data(self, df, valid_boundary=1315, test_boundary=1339): - """Splits data frame into training-validation-test data frames. - - This also calibrates scaling object, and transforms data for each split. - - Args: - df: Source data frame to split. - valid_boundary: Starting year for validation data - test_boundary: Starting year for test data - - Returns: - Tuple of transformed (train, valid, test) data. - """ - - print("Formatting train-valid-test splits.") - - index = df["days_from_start"] - train = df.loc[index < valid_boundary] - valid = df.loc[(index >= valid_boundary - 7) & (index < test_boundary)] - test = df.loc[index >= test_boundary - 7] - - self.set_scalers(train) - - return (self.transform_inputs(data) for data in [train, valid, test]) - - def set_scalers(self, df): - """Calibrates scalers using the data supplied. - - Args: - df: Data to use to calibrate scalers. - """ - print("Setting scalers with training data...") - - column_definitions = self.get_column_definition() - id_column = utils.get_single_col_by_input_type(InputTypes.ID, column_definitions) - target_column = utils.get_single_col_by_input_type(InputTypes.TARGET, column_definitions) - - # Format real scalers - real_inputs = utils.extract_cols_from_data_type( - DataTypes.REAL_VALUED, column_definitions, {InputTypes.ID, InputTypes.TIME} - ) - - # Initialise scaler caches - self._real_scalers = {} - self._target_scaler = {} - identifiers = [] - for identifier, sliced in df.groupby(id_column): - - if len(sliced) >= self._time_steps: - - data = sliced[real_inputs].values - targets = sliced[[target_column]].values - self._real_scalers[identifier] = sklearn.preprocessing.StandardScaler().fit(data) - - self._target_scaler[identifier] = sklearn.preprocessing.StandardScaler().fit(targets) - identifiers.append(identifier) - - # Format categorical scalers - categorical_inputs = utils.extract_cols_from_data_type( - DataTypes.CATEGORICAL, column_definitions, {InputTypes.ID, InputTypes.TIME} - ) - - categorical_scalers = {} - num_classes = [] - for col in categorical_inputs: - # Set all to str so that we don't have mixed integer/string columns - srs = df[col].apply(str) - categorical_scalers[col] = sklearn.preprocessing.LabelEncoder().fit(srs.values) - num_classes.append(srs.nunique()) - - # Set categorical scaler outputs - self._cat_scalers = categorical_scalers - self._num_classes_per_cat_input = num_classes - - # Extract identifiers in case required - self.identifiers = identifiers - - def transform_inputs(self, df): - """Performs feature transformations. - - This includes both feature engineering, preprocessing and normalisation. - - Args: - df: Data frame to transform. - - Returns: - Transformed data frame. - - """ - - if self._real_scalers is None and self._cat_scalers is None: - raise ValueError("Scalers have not been set!") - - # Extract relevant columns - column_definitions = self.get_column_definition() - id_col = utils.get_single_col_by_input_type(InputTypes.ID, column_definitions) - real_inputs = utils.extract_cols_from_data_type( - DataTypes.REAL_VALUED, column_definitions, {InputTypes.ID, InputTypes.TIME} - ) - categorical_inputs = utils.extract_cols_from_data_type( - DataTypes.CATEGORICAL, column_definitions, {InputTypes.ID, InputTypes.TIME} - ) - - # Transform real inputs per entity - df_list = [] - for identifier, sliced in df.groupby(id_col): - - # Filter out any trajectories that are too short - if len(sliced) >= self._time_steps: - sliced_copy = sliced.copy() - sliced_copy[real_inputs] = self._real_scalers[identifier].transform(sliced_copy[real_inputs].values) - df_list.append(sliced_copy) - - output = pd.concat(df_list, axis=0) - - # Format categorical inputs - for col in categorical_inputs: - string_df = df[col].apply(str) - output[col] = self._cat_scalers[col].transform(string_df) - - return output - - def format_predictions(self, predictions): - """Reverts any normalisation to give predictions in original scale. - - Args: - predictions: Dataframe of model predictions. - - Returns: - Data frame of unnormalised predictions. - """ - - if self._target_scaler is None: - raise ValueError("Scalers have not been set!") - - column_names = predictions.columns - - df_list = [] - for identifier, sliced in predictions.groupby("identifier"): - sliced_copy = sliced.copy() - target_scaler = self._target_scaler[identifier] - - for col in column_names: - if col not in {"forecast_time", "identifier"}: - sliced_copy[col] = target_scaler.inverse_transform(sliced_copy[col]) - df_list.append(sliced_copy) - - output = pd.concat(df_list, axis=0) - - return output - - # Default params - def get_fixed_params(self): - """Returns fixed model parameters for experiments.""" - - fixed_params = { - "total_time_steps": 8 * 24, - "num_encoder_steps": 7 * 24, - "num_epochs": 100, - "early_stopping_patience": 5, - "multiprocessing_workers": 5, - } - - return fixed_params - - def get_default_model_params(self): - """Returns default optimised model parameters.""" - - model_params = { - "dropout_rate": 0.1, - "hidden_layer_size": 160, - "learning_rate": 0.001, - "minibatch_size": 64, - "max_gradient_norm": 0.01, - "num_heads": 4, - "stack_size": 1, - } - - return model_params - - def get_num_samples_for_calibration(self): - """Gets the default number of training and validation samples. - - Use to sub-sample the data for network calibration and a value of -1 uses - all available samples. - - Returns: - Tuple of (training samples, validation samples) - """ - return 450000, 50000 From f0d7eaf7f4324de1803300eac1ed96f24536d92c Mon Sep 17 00:00:00 2001 From: Wendi Li Date: Wed, 25 Nov 2020 20:16:22 +0800 Subject: [PATCH 132/241] Delete favorita.py --- .../TFT/data_formatters/favorita.py | 333 ------------------ 1 file changed, 333 deletions(-) delete mode 100644 examples/benchmarks/TFT/data_formatters/favorita.py diff --git a/examples/benchmarks/TFT/data_formatters/favorita.py b/examples/benchmarks/TFT/data_formatters/favorita.py deleted file mode 100644 index bc7a24140..000000000 --- a/examples/benchmarks/TFT/data_formatters/favorita.py +++ /dev/null @@ -1,333 +0,0 @@ -# coding=utf-8 -# Copyright 2020 The Google Research Authors. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# Lint as: python3 -"""Custom formatting functions for Favorita dataset. - -Defines dataset specific column definitions and data transformations. -""" - -import data_formatters.base -import libs.utils as utils -import pandas as pd -import sklearn.preprocessing - -DataTypes = data_formatters.base.DataTypes -InputTypes = data_formatters.base.InputTypes - - -class FavoritaFormatter(data_formatters.base.GenericDataFormatter): - """Defines and formats data for the Favorita dataset. - - Attributes: - column_definition: Defines input and data type of column used in the - experiment. - identifiers: Entity identifiers used in experiments. - """ - - _column_definition = [ - ("traj_id", DataTypes.REAL_VALUED, InputTypes.ID), - ("date", DataTypes.DATE, InputTypes.TIME), - ("log_sales", DataTypes.REAL_VALUED, InputTypes.TARGET), - ("onpromotion", DataTypes.CATEGORICAL, InputTypes.KNOWN_INPUT), - ("transactions", DataTypes.REAL_VALUED, InputTypes.OBSERVED_INPUT), - ("oil", DataTypes.REAL_VALUED, InputTypes.OBSERVED_INPUT), - ("day_of_week", DataTypes.CATEGORICAL, InputTypes.KNOWN_INPUT), - ("day_of_month", DataTypes.REAL_VALUED, InputTypes.KNOWN_INPUT), - ("month", DataTypes.REAL_VALUED, InputTypes.KNOWN_INPUT), - ("national_hol", DataTypes.CATEGORICAL, InputTypes.KNOWN_INPUT), - ("regional_hol", DataTypes.CATEGORICAL, InputTypes.KNOWN_INPUT), - ("local_hol", DataTypes.CATEGORICAL, InputTypes.KNOWN_INPUT), - ("open", DataTypes.REAL_VALUED, InputTypes.KNOWN_INPUT), - ("item_nbr", DataTypes.CATEGORICAL, InputTypes.STATIC_INPUT), - ("store_nbr", DataTypes.CATEGORICAL, InputTypes.STATIC_INPUT), - ("city", DataTypes.CATEGORICAL, InputTypes.STATIC_INPUT), - ("state", DataTypes.CATEGORICAL, InputTypes.STATIC_INPUT), - ("type", DataTypes.CATEGORICAL, InputTypes.STATIC_INPUT), - ("cluster", DataTypes.CATEGORICAL, InputTypes.STATIC_INPUT), - ("family", DataTypes.CATEGORICAL, InputTypes.STATIC_INPUT), - ("class", DataTypes.CATEGORICAL, InputTypes.STATIC_INPUT), - ("perishable", DataTypes.CATEGORICAL, InputTypes.STATIC_INPUT), - ] - - def __init__(self): - """Initialises formatter.""" - - self.identifiers = None - self._real_scalers = None - self._cat_scalers = None - self._target_scaler = None - self._num_classes_per_cat_input = None - - def split_data(self, df, valid_boundary=None, test_boundary=None): - """Splits data frame into training-validation-test data frames. - - This also calibrates scaling object, and transforms data for each split. - - Args: - df: Source data frame to split. - valid_boundary: Starting year for validation data - test_boundary: Starting year for test data - - Returns: - Tuple of transformed (train, valid, test) data. - """ - - print("Formatting train-valid-test splits.") - - if valid_boundary is None: - valid_boundary = pd.datetime(2015, 12, 1) - - fixed_params = self.get_fixed_params() - time_steps = fixed_params["total_time_steps"] - lookback = fixed_params["num_encoder_steps"] - forecast_horizon = time_steps - lookback - - df["date"] = pd.to_datetime(df["date"]) - df_lists = {"train": [], "valid": [], "test": []} - for _, sliced in df.groupby("traj_id"): - index = sliced["date"] - train = sliced.loc[index < valid_boundary] - train_len = len(train) - valid_len = train_len + forecast_horizon - valid = sliced.iloc[train_len - lookback : valid_len, :] - test = sliced.iloc[valid_len - lookback : valid_len + forecast_horizon, :] - - sliced_map = {"train": train, "valid": valid, "test": test} - - for k in sliced_map: - item = sliced_map[k] - - if len(item) >= time_steps: - df_lists[k].append(item) - - dfs = {k: pd.concat(df_lists[k], axis=0) for k in df_lists} - - train = dfs["train"] - self.set_scalers(train, set_real=True) - - # Use all data for label encoding to handle labels not present in training. - self.set_scalers(df, set_real=False) - - # Filter out identifiers not present in training (i.e. cold-started items). - def filter_ids(frame): - identifiers = set(self.identifiers) - index = frame["traj_id"] - return frame.loc[index.apply(lambda x: x in identifiers)] - - valid = filter_ids(dfs["valid"]) - test = filter_ids(dfs["test"]) - - return (self.transform_inputs(data) for data in [train, valid, test]) - - def set_scalers(self, df, set_real=True): - """Calibrates scalers using the data supplied. - - Label encoding is applied to the entire dataset (i.e. including test), - so that unseen labels can be handled at run-time. - - Args: - df: Data to use to calibrate scalers. - set_real: Whether to fit set real-valued or categorical scalers - """ - print("Setting scalers with training data...") - - column_definitions = self.get_column_definition() - id_column = utils.get_single_col_by_input_type(InputTypes.ID, column_definitions) - target_column = utils.get_single_col_by_input_type(InputTypes.TARGET, column_definitions) - - if set_real: - - # Extract identifiers in case required - self.identifiers = list(df[id_column].unique()) - - # Format real scalers - self._real_scalers = {} - for col in ["oil", "transactions", "log_sales"]: - self._real_scalers[col] = (df[col].mean(), df[col].std()) - - self._target_scaler = (df[target_column].mean(), df[target_column].std()) - - else: - # Format categorical scalers - categorical_inputs = utils.extract_cols_from_data_type( - DataTypes.CATEGORICAL, column_definitions, {InputTypes.ID, InputTypes.TIME} - ) - - categorical_scalers = {} - num_classes = [] - if self.identifiers is None: - raise ValueError("Scale real-valued inputs first!") - id_set = set(self.identifiers) - valid_idx = df["traj_id"].apply(lambda x: x in id_set) - for col in categorical_inputs: - # Set all to str so that we don't have mixed integer/string columns - srs = df[col].apply(str).loc[valid_idx] - categorical_scalers[col] = sklearn.preprocessing.LabelEncoder().fit(srs.values) - - num_classes.append(srs.nunique()) - - # Set categorical scaler outputs - self._cat_scalers = categorical_scalers - self._num_classes_per_cat_input = num_classes - - def transform_inputs(self, df): - """Performs feature transformations. - - This includes both feature engineering, preprocessing and normalisation. - - Args: - df: Data frame to transform. - - Returns: - Transformed data frame. - - """ - output = df.copy() - - if self._real_scalers is None and self._cat_scalers is None: - raise ValueError("Scalers have not been set!") - - column_definitions = self.get_column_definition() - - categorical_inputs = utils.extract_cols_from_data_type( - DataTypes.CATEGORICAL, column_definitions, {InputTypes.ID, InputTypes.TIME} - ) - - # Format real inputs - for col in ["log_sales", "oil", "transactions"]: - mean, std = self._real_scalers[col] - output[col] = (df[col] - mean) / std - - if col == "log_sales": - output[col] = output[col].fillna(0.0) # mean imputation - - # Format categorical inputs - for col in categorical_inputs: - string_df = df[col].apply(str) - output[col] = self._cat_scalers[col].transform(string_df) - - return output - - def format_predictions(self, predictions): - """Reverts any normalisation to give predictions in original scale. - - Args: - predictions: Dataframe of model predictions. - - Returns: - Data frame of unnormalised predictions. - """ - output = predictions.copy() - - column_names = predictions.columns - mean, std = self._target_scaler - for col in column_names: - if col not in {"forecast_time", "identifier"}: - output[col] = (predictions[col] * std) + mean - - return output - - # Default params - def get_fixed_params(self): - """Returns fixed model parameters for experiments.""" - - fixed_params = { - "total_time_steps": 120, - "num_encoder_steps": 90, - "num_epochs": 100, - "early_stopping_patience": 5, - "multiprocessing_workers": 5, - } - - return fixed_params - - def get_default_model_params(self): - """Returns default optimised model parameters.""" - - model_params = { - "dropout_rate": 0.1, - "hidden_layer_size": 240, - "learning_rate": 0.001, - "minibatch_size": 128, - "max_gradient_norm": 100.0, - "num_heads": 4, - "stack_size": 1, - } - - return model_params - - def get_num_samples_for_calibration(self): - """Gets the default number of training and validation samples. - - Use to sub-sample the data for network calibration and a value of -1 uses - all available samples. - - Returns: - Tuple of (training samples, validation samples) - """ - return 450000, 50000 - - def get_column_definition(self): - """ "Formats column definition in order expected by the TFT. - - Modified for Favorita to match column order of original experiment. - - Returns: - Favorita-specific column definition - """ - - column_definition = self._column_definition - - # Sanity checks first. - # Ensure only one ID and time column exist - def _check_single_column(input_type): - - length = len([tup for tup in column_definition if tup[2] == input_type]) - - if length != 1: - raise ValueError("Illegal number of inputs ({}) of type {}".format(length, input_type)) - - _check_single_column(InputTypes.ID) - _check_single_column(InputTypes.TIME) - - identifier = [tup for tup in column_definition if tup[2] == InputTypes.ID] - time = [tup for tup in column_definition if tup[2] == InputTypes.TIME] - real_inputs = [ - tup - for tup in column_definition - if tup[1] == DataTypes.REAL_VALUED and tup[2] not in {InputTypes.ID, InputTypes.TIME} - ] - - col_definition_map = {tup[0]: tup for tup in column_definition} - col_order = [ - "item_nbr", - "store_nbr", - "city", - "state", - "type", - "cluster", - "family", - "class", - "perishable", - "onpromotion", - "day_of_week", - "national_hol", - "regional_hol", - "local_hol", - ] - categorical_inputs = [col_definition_map[k] for k in col_order if k in col_definition_map] - - return identifier + time + real_inputs + categorical_inputs From 65e3ad377978c9d238a8b36bc3c0d93d927dc87d Mon Sep 17 00:00:00 2001 From: Wendi Li Date: Wed, 25 Nov 2020 20:16:29 +0800 Subject: [PATCH 133/241] Delete traffic.py --- .../benchmarks/TFT/data_formatters/traffic.py | 117 ------------------ 1 file changed, 117 deletions(-) delete mode 100644 examples/benchmarks/TFT/data_formatters/traffic.py diff --git a/examples/benchmarks/TFT/data_formatters/traffic.py b/examples/benchmarks/TFT/data_formatters/traffic.py deleted file mode 100644 index ee8ef2e5d..000000000 --- a/examples/benchmarks/TFT/data_formatters/traffic.py +++ /dev/null @@ -1,117 +0,0 @@ -# coding=utf-8 -# Copyright 2020 The Google Research Authors. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# Lint as: python3 -"""Custom formatting functions for Traffic dataset. - -Defines dataset specific column definitions and data transformations. This also -performs z-score normalization across the entire dataset, hence re-uses most of -the same functions as volatility. -""" - -import data_formatters.base -import data_formatters.volatility - -VolatilityFormatter = data_formatters.volatility.VolatilityFormatter -DataTypes = data_formatters.base.DataTypes -InputTypes = data_formatters.base.InputTypes - - -class TrafficFormatter(VolatilityFormatter): - """Defines and formats data for the traffic dataset. - - This also performs z-score normalization across the entire dataset, hence - re-uses most of the same functions as volatility. - - Attributes: - column_definition: Defines input and data type of column used in the - experiment. - identifiers: Entity identifiers used in experiments. - """ - - _column_definition = [ - ("id", DataTypes.REAL_VALUED, InputTypes.ID), - ("hours_from_start", DataTypes.REAL_VALUED, InputTypes.TIME), - ("values", DataTypes.REAL_VALUED, InputTypes.TARGET), - ("time_on_day", DataTypes.REAL_VALUED, InputTypes.KNOWN_INPUT), - ("day_of_week", DataTypes.REAL_VALUED, InputTypes.KNOWN_INPUT), - ("hours_from_start", DataTypes.REAL_VALUED, InputTypes.KNOWN_INPUT), - ("categorical_id", DataTypes.CATEGORICAL, InputTypes.STATIC_INPUT), - ] - - def split_data(self, df, valid_boundary=151, test_boundary=166): - """Splits data frame into training-validation-test data frames. - - This also calibrates scaling object, and transforms data for each split. - - Args: - df: Source data frame to split. - valid_boundary: Starting year for validation data - test_boundary: Starting year for test data - - Returns: - Tuple of transformed (train, valid, test) data. - """ - - print("Formatting train-valid-test splits.") - - index = df["sensor_day"] - train = df.loc[index < valid_boundary] - valid = df.loc[(index >= valid_boundary - 7) & (index < test_boundary)] - test = df.loc[index >= test_boundary - 7] - - self.set_scalers(train) - - return (self.transform_inputs(data) for data in [train, valid, test]) - - # Default params - def get_fixed_params(self): - """Returns fixed model parameters for experiments.""" - - fixed_params = { - "total_time_steps": 8 * 24, - "num_encoder_steps": 7 * 24, - "num_epochs": 100, - "early_stopping_patience": 5, - "multiprocessing_workers": 5, - } - - return fixed_params - - def get_default_model_params(self): - """Returns default optimised model parameters.""" - - model_params = { - "dropout_rate": 0.3, - "hidden_layer_size": 320, - "learning_rate": 0.001, - "minibatch_size": 128, - "max_gradient_norm": 100.0, - "num_heads": 4, - "stack_size": 1, - } - - return model_params - - def get_num_samples_for_calibration(self): - """Gets the default number of training and validation samples. - - Use to sub-sample the data for network calibration and a value of -1 uses - all available samples. - - Returns: - Tuple of (training samples, validation samples) - """ - return 450000, 50000 From 939154b4a5137058c0e6c5458da4954e981475c3 Mon Sep 17 00:00:00 2001 From: Wendi Li Date: Wed, 25 Nov 2020 20:16:37 +0800 Subject: [PATCH 134/241] Delete volatility.py --- .../TFT/data_formatters/volatility.py | 212 ------------------ 1 file changed, 212 deletions(-) delete mode 100644 examples/benchmarks/TFT/data_formatters/volatility.py diff --git a/examples/benchmarks/TFT/data_formatters/volatility.py b/examples/benchmarks/TFT/data_formatters/volatility.py deleted file mode 100644 index b3ddf09fd..000000000 --- a/examples/benchmarks/TFT/data_formatters/volatility.py +++ /dev/null @@ -1,212 +0,0 @@ -# coding=utf-8 -# Copyright 2020 The Google Research Authors. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# Lint as: python3 -"""Custom formatting functions for Volatility dataset. - -Defines dataset specific column definitions and data transformations. -""" - -import data_formatters.base -import libs.utils as utils -import sklearn.preprocessing - -GenericDataFormatter = data_formatters.base.GenericDataFormatter -DataTypes = data_formatters.base.DataTypes -InputTypes = data_formatters.base.InputTypes - - -class VolatilityFormatter(GenericDataFormatter): - """Defines and formats data for the volatility dataset. - - Attributes: - column_definition: Defines input and data type of column used in the - experiment. - identifiers: Entity identifiers used in experiments. - """ - - _column_definition = [ - ("Symbol", DataTypes.CATEGORICAL, InputTypes.ID), - ("date", DataTypes.DATE, InputTypes.TIME), - ("log_vol", DataTypes.REAL_VALUED, InputTypes.TARGET), - ("open_to_close", DataTypes.REAL_VALUED, InputTypes.OBSERVED_INPUT), - ("days_from_start", DataTypes.REAL_VALUED, InputTypes.KNOWN_INPUT), - ("day_of_week", DataTypes.CATEGORICAL, InputTypes.KNOWN_INPUT), - ("day_of_month", DataTypes.CATEGORICAL, InputTypes.KNOWN_INPUT), - ("week_of_year", DataTypes.CATEGORICAL, InputTypes.KNOWN_INPUT), - ("month", DataTypes.CATEGORICAL, InputTypes.KNOWN_INPUT), - ("Region", DataTypes.CATEGORICAL, InputTypes.STATIC_INPUT), - ] - - def __init__(self): - """Initialises formatter.""" - - self.identifiers = None - self._real_scalers = None - self._cat_scalers = None - self._target_scaler = None - self._num_classes_per_cat_input = None - - def split_data(self, df, valid_boundary=2016, test_boundary=2018): - """Splits data frame into training-validation-test data frames. - - This also calibrates scaling object, and transforms data for each split. - - Args: - df: Source data frame to split. - valid_boundary: Starting year for validation data - test_boundary: Starting year for test data - - Returns: - Tuple of transformed (train, valid, test) data. - """ - - print("Formatting train-valid-test splits.") - - index = df["year"] - train = df.loc[index < valid_boundary] - valid = df.loc[(index >= valid_boundary) & (index < test_boundary)] - test = df.loc[index >= test_boundary] - - self.set_scalers(train) - - return (self.transform_inputs(data) for data in [train, valid, test]) - - def set_scalers(self, df): - """Calibrates scalers using the data supplied. - - Args: - df: Data to use to calibrate scalers. - """ - print("Setting scalers with training data...") - - column_definitions = self.get_column_definition() - id_column = utils.get_single_col_by_input_type(InputTypes.ID, column_definitions) - target_column = utils.get_single_col_by_input_type(InputTypes.TARGET, column_definitions) - - # Extract identifiers in case required - self.identifiers = list(df[id_column].unique()) - - # Format real scalers - real_inputs = utils.extract_cols_from_data_type( - DataTypes.REAL_VALUED, column_definitions, {InputTypes.ID, InputTypes.TIME} - ) - - data = df[real_inputs].values - self._real_scalers = sklearn.preprocessing.StandardScaler().fit(data) - self._target_scaler = sklearn.preprocessing.StandardScaler().fit( - df[[target_column]].values - ) # used for predictions - - # Format categorical scalers - categorical_inputs = utils.extract_cols_from_data_type( - DataTypes.CATEGORICAL, column_definitions, {InputTypes.ID, InputTypes.TIME} - ) - - categorical_scalers = {} - num_classes = [] - for col in categorical_inputs: - # Set all to str so that we don't have mixed integer/string columns - srs = df[col].apply(str) - categorical_scalers[col] = sklearn.preprocessing.LabelEncoder().fit(srs.values) - num_classes.append(srs.nunique()) - - # Set categorical scaler outputs - self._cat_scalers = categorical_scalers - self._num_classes_per_cat_input = num_classes - - def transform_inputs(self, df): - """Performs feature transformations. - - This includes both feature engineering, preprocessing and normalisation. - - Args: - df: Data frame to transform. - - Returns: - Transformed data frame. - - """ - output = df.copy() - - if self._real_scalers is None and self._cat_scalers is None: - raise ValueError("Scalers have not been set!") - - column_definitions = self.get_column_definition() - - real_inputs = utils.extract_cols_from_data_type( - DataTypes.REAL_VALUED, column_definitions, {InputTypes.ID, InputTypes.TIME} - ) - categorical_inputs = utils.extract_cols_from_data_type( - DataTypes.CATEGORICAL, column_definitions, {InputTypes.ID, InputTypes.TIME} - ) - - # Format real inputs - output[real_inputs] = self._real_scalers.transform(df[real_inputs].values) - - # Format categorical inputs - for col in categorical_inputs: - string_df = df[col].apply(str) - output[col] = self._cat_scalers[col].transform(string_df) - - return output - - def format_predictions(self, predictions): - """Reverts any normalisation to give predictions in original scale. - - Args: - predictions: Dataframe of model predictions. - - Returns: - Data frame of unnormalised predictions. - """ - output = predictions.copy() - - column_names = predictions.columns - - for col in column_names: - if col not in {"forecast_time", "identifier"}: - output[col] = self._target_scaler.inverse_transform(predictions[col]) - - return output - - # Default params - def get_fixed_params(self): - """Returns fixed model parameters for experiments.""" - - fixed_params = { - "total_time_steps": 252 + 5, - "num_encoder_steps": 252, - "num_epochs": 100, - "early_stopping_patience": 5, - "multiprocessing_workers": 5, - } - - return fixed_params - - def get_default_model_params(self): - """Returns default optimised model parameters.""" - - model_params = { - "dropout_rate": 0.3, - "hidden_layer_size": 160, - "learning_rate": 0.01, - "minibatch_size": 64, - "max_gradient_norm": 0.01, - "num_heads": 1, - "stack_size": 1, - } - - return model_params From 5581cc42bf97582eb320c63b082286cc60327297 Mon Sep 17 00:00:00 2001 From: Wendi Li Date: Wed, 25 Nov 2020 20:20:52 +0800 Subject: [PATCH 135/241] Update configs.py --- examples/benchmarks/TFT/expt_settings/configs.py | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/examples/benchmarks/TFT/expt_settings/configs.py b/examples/benchmarks/TFT/expt_settings/configs.py index d1891a002..6aef0c395 100644 --- a/examples/benchmarks/TFT/expt_settings/configs.py +++ b/examples/benchmarks/TFT/expt_settings/configs.py @@ -22,10 +22,6 @@ for the main experiments used in the publication. import os -import data_formatters.electricity -import data_formatters.favorita -import data_formatters.traffic -import data_formatters.volatility import data_formatters.qlib_Alpha158 @@ -43,7 +39,7 @@ class ExperimentConfig(object): experiment. """ - default_experiments = ["volatility", "electricity", "traffic", "favorita", "Alpha158"] + default_experiments = ["Alpha158"] def __init__(self, experiment="volatility", root_folder=None): """Creates configs based on default experiment chosen. @@ -75,10 +71,6 @@ class ExperimentConfig(object): @property def data_csv_path(self): csv_map = { - "volatility": "formatted_omi_vol.csv", - "electricity": "hourly_electricity.csv", - "traffic": "hourly_data.csv", - "favorita": "favorita_consolidated.csv", "Alpha158": "Alpha158.csv", } @@ -97,10 +89,6 @@ class ExperimentConfig(object): """ data_formatter_class = { - "volatility": data_formatters.volatility.VolatilityFormatter, - "electricity": data_formatters.electricity.ElectricityFormatter, - "traffic": data_formatters.traffic.TrafficFormatter, - "favorita": data_formatters.favorita.FavoritaFormatter, "Alpha158": data_formatters.qlib_Alpha158.Alpha158Formatter, } From 510be10a87a650903ebd1c4d870684eba34616d5 Mon Sep 17 00:00:00 2001 From: Wendi Li Date: Wed, 25 Nov 2020 20:34:52 +0800 Subject: [PATCH 136/241] Delete workflow_by_code_tft.py --- .../benchmarks/TFT/workflow_by_code_tft.py | 132 ------------------ 1 file changed, 132 deletions(-) delete mode 100644 examples/benchmarks/TFT/workflow_by_code_tft.py diff --git a/examples/benchmarks/TFT/workflow_by_code_tft.py b/examples/benchmarks/TFT/workflow_by_code_tft.py deleted file mode 100644 index 64c7d3df5..000000000 --- a/examples/benchmarks/TFT/workflow_by_code_tft.py +++ /dev/null @@ -1,132 +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.pytorch_lstm import LSTM -from qlib.contrib.data.handler import ALPHA360_Denoise -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 - -# from qlib.model.learner import train_model -from qlib.utils import init_instance_by_config - -import pickle -from tft import TFTModel - -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_cn(target_dir=provider_uri) - - qlib.init(provider_uri=provider_uri, region=REG_CN) - - MARKET = "csi300" - BENCHMARK = "SH000300" - - ################################### - # train model - ################################### - DATA_HANDLER_CONFIG = { - "start_time": "2008-01-01", - "end_time": "2020-08-01", - "fit_start_time": "2008-01-01", - "fit_end_time": "2014-12-31", - "instruments": MARKET, - } - - TRAINER_CONFIG = { - "train_start_time": "2008-01-01", - "train_end_time": "2014-12-31", - "validate_start_time": "2015-01-01", - "validate_end_time": "2016-12-31", - "test_start_time": "2017-01-01", - "test_end_time": "2020-08-01", - } - - task = { - "dataset": { - "class": "DatasetH", - "module_path": "qlib.data.dataset", - "kwargs": { - "handler": { - "class": "Alpha158", - "module_path": "qlib.contrib.data.handler", - "kwargs": DATA_HANDLER_CONFIG, - }, - "segments": { - "train": ("2008-01-01", "2014-12-31"), - "valid": ( - "2015-01-01", - "2016-12-31", - ), - "test": ( - "2017-01-01", - "2020-08-01", - ), - }, - }, - } - # You shoud record the data in specific sequence - # "record": ['SignalRecord', 'SigAnaRecord', 'PortAnaRecord'], - } - - model = TFTModel() - dataset = init_instance_by_config(task["dataset"]) - model.fit(dataset) - - pred_score, label_score = model.predict(dataset) - - # save pred_score to file - pred_score_path = Path("~/tmp/qlib/pred_score.pkl").expanduser() - pred_score_path.parent.mkdir(exist_ok=True, parents=True) - pred_score.to_pickle(pred_score_path) - - ################################### - # backtest - ################################### - STRATEGY_CONFIG = { - "topk": 50, - "n_drop": 5, - } - BACKTEST_CONFIG = { - "verbose": False, - "limit_threshold": 0.095, - "account": 100000000, - "benchmark": BENCHMARK, - "deal_price": "close", - "open_cost": 0.0005, - "close_cost": 0.0015, - "min_cost": 5, - } - - # use default strategy - # custom Strategy, refer to: TODO: Strategy API url - strategy = TopkDropoutStrategy(**STRATEGY_CONFIG) - report_normal, positions_normal = normal_backtest(pred_score, strategy=strategy, **BACKTEST_CONFIG) - - ################################### - # analyze - # If need a more detailed analysis, refer to: examples/train_and_bakctest.ipynb - ################################### - 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 - print(analysis_df) From 0bb7457520effd25462578810b366d7203643a2c Mon Sep 17 00:00:00 2001 From: Haoyu Wang <48108414+javaThonc@users.noreply.github.com> Date: Wed, 25 Nov 2020 20:40:32 +0800 Subject: [PATCH 137/241] update hyperparameters --- examples/benchmarks/CatBoost/workflow_config_catboost.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/examples/benchmarks/CatBoost/workflow_config_catboost.yaml b/examples/benchmarks/CatBoost/workflow_config_catboost.yaml index 574b52ddd..0ae0d95b2 100644 --- a/examples/benchmarks/CatBoost/workflow_config_catboost.yaml +++ b/examples/benchmarks/CatBoost/workflow_config_catboost.yaml @@ -32,7 +32,11 @@ task: loss: RMSE learning_rate: 0.0421 subsample: 0.8789 + max_depth: 6 + num_leaves: 100 thread_count: 20 + grow_policy: 'Lossguide', + boostrap_type: 'Poisson' dataset: class: DatasetH module_path: qlib.data.dataset From 72013347339ed66ce4ae3238b7bdefdc1bcb05c3 Mon Sep 17 00:00:00 2001 From: Jactus Date: Wed, 25 Nov 2020 20:40:45 +0800 Subject: [PATCH 138/241] Format --- examples/benchmarks/TFT/tft.py | 2 +- qlib/data/dataset/loader.py | 16 +++++++--------- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/examples/benchmarks/TFT/tft.py b/examples/benchmarks/TFT/tft.py index 92d957d0f..a3b4fc919 100644 --- a/examples/benchmarks/TFT/tft.py +++ b/examples/benchmarks/TFT/tft.py @@ -232,7 +232,7 @@ class TFTModel(ModelFT): p90_forecast = self.data_formatter.format_predictions(output_map["p90"]) tf.keras.backend.set_session(default_keras_session) - predict = format_score(p90_forecast, "pred", 0) # self.label_shift + predict = format_score(p90_forecast, "pred", 0) # self.label_shift label = format_score(targets, "label", 0) # ===========================Predicting Process=========================== return predict, label diff --git a/qlib/data/dataset/loader.py b/qlib/data/dataset/loader.py index 404313e80..db6b1440d 100644 --- a/qlib/data/dataset/loader.py +++ b/qlib/data/dataset/loader.py @@ -21,18 +21,16 @@ class DataLoader(abc.ABC): @abc.abstractmethod def load(self, instruments, start_time=None, end_time=None) -> pd.DataFrame: """ - load the data as pd.DataFrame + load the data as pd.DataFrame. Parameters ---------- - self : [TODO:type] - [TODO:description] - instruments : [TODO:type] - [TODO:description] - start_time : [TODO:type] - [TODO:description] - end_time : [TODO:type] - [TODO:description] + instruments : str or dict + it can either be the market name or the config file of instruments generated by InstrumentProvider. + start_time : str + start of the time range. + end_time : str + end of the time range. Returns ------- From 538106f1b8d358279466d1152a8955e6ce430146 Mon Sep 17 00:00:00 2001 From: Young Date: Wed, 25 Nov 2020 12:40:58 +0000 Subject: [PATCH 139/241] fix format --- examples/benchmarks/TFT/tft.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/benchmarks/TFT/tft.py b/examples/benchmarks/TFT/tft.py index 92d957d0f..a3b4fc919 100644 --- a/examples/benchmarks/TFT/tft.py +++ b/examples/benchmarks/TFT/tft.py @@ -232,7 +232,7 @@ class TFTModel(ModelFT): p90_forecast = self.data_formatter.format_predictions(output_map["p90"]) tf.keras.backend.set_session(default_keras_session) - predict = format_score(p90_forecast, "pred", 0) # self.label_shift + predict = format_score(p90_forecast, "pred", 0) # self.label_shift label = format_score(targets, "label", 0) # ===========================Predicting Process=========================== return predict, label From 233ef717751ef14370eba304831119e96634c14f Mon Sep 17 00:00:00 2001 From: Haoyu Wang <48108414+javaThonc@users.noreply.github.com> Date: Wed, 25 Nov 2020 20:41:04 +0800 Subject: [PATCH 140/241] update parameters --- examples/benchmarks/CatBoost/workflow_config_catboost.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/benchmarks/CatBoost/workflow_config_catboost.yaml b/examples/benchmarks/CatBoost/workflow_config_catboost.yaml index 0ae0d95b2..720599be5 100644 --- a/examples/benchmarks/CatBoost/workflow_config_catboost.yaml +++ b/examples/benchmarks/CatBoost/workflow_config_catboost.yaml @@ -35,8 +35,8 @@ task: max_depth: 6 num_leaves: 100 thread_count: 20 - grow_policy: 'Lossguide', - boostrap_type: 'Poisson' + grow_policy: Lossguide + boostrap_type: Poisson dataset: class: DatasetH module_path: qlib.data.dataset From a371bf757b427cb91e1369747d101286d5a8ff9a Mon Sep 17 00:00:00 2001 From: Jactus Date: Wed, 25 Nov 2020 20:44:49 +0800 Subject: [PATCH 141/241] Update run_all_model script --- examples/run_all_model.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/examples/run_all_model.py b/examples/run_all_model.py index f8894afd3..b448a1857 100644 --- a/examples/run_all_model.py +++ b/examples/run_all_model.py @@ -178,8 +178,7 @@ def get_all_folders() -> dict: folders = dict() for f in os.scandir("benchmarks"): path = Path("benchmarks") / f.name - if f.name != "TFT": - folders[f.name] = str(path.resolve()) + folders[f.name] = str(path.resolve()) return folders From 515c627659bcd3ecf0dd79afec9f4659a9fa9fad Mon Sep 17 00:00:00 2001 From: Hong Zhang Date: Wed, 25 Nov 2020 20:44:52 +0800 Subject: [PATCH 142/241] hats4 --- examples/benchmarks/HATS/README.md | 15 +++++++++++++++ qlib/contrib/model/pytorch_hats.py | 13 +++++++++++-- 2 files changed, 26 insertions(+), 2 deletions(-) create mode 100644 examples/benchmarks/HATS/README.md diff --git a/examples/benchmarks/HATS/README.md b/examples/benchmarks/HATS/README.md new file mode 100644 index 000000000..95619e1ee --- /dev/null +++ b/examples/benchmarks/HATS/README.md @@ -0,0 +1,15 @@ +##Requirement + +* pandas==1.1.2 +* numpy==1.17.4 +* scikit_learn==0.23.2 +* torch==1.7.0 + +##HATS + +* HATS is a a hierarchical attention network for stock prediction which uses relational data for stock market prediction. HATS selectively aggregates information +on different relation types and adds the information to the representations of each company. HATS is used as a relational modeling module with initialized node representations.Furthermore, HATS +can predict not only individual stock prices but also market index movements, which is similar to the graph classification task. + +* HATS uses pretrained model of GRU and LSTM. The code of GRU and LSTM used in Qlib is a pyTorch implemention of GRU and LSTM. +* Paper address:HATS: A Hierarchical Graph Attention Network for Stock Movement Prediction https://arxiv.org/pdf/1908.07999.pdf \ No newline at end of file diff --git a/qlib/contrib/model/pytorch_hats.py b/qlib/contrib/model/pytorch_hats.py index 7b4307e25..593cef635 100644 --- a/qlib/contrib/model/pytorch_hats.py +++ b/qlib/contrib/model/pytorch_hats.py @@ -1,5 +1,14 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. from __future__ import division From b6726628c446a431ccbdab4562ec4fbc1560e599 Mon Sep 17 00:00:00 2001 From: Young Date: Wed, 25 Nov 2020 12:58:43 +0000 Subject: [PATCH 143/241] black format --- setup.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/setup.py b/setup.py index 4fe410b9d..c0733d5f0 100644 --- a/setup.py +++ b/setup.py @@ -43,7 +43,7 @@ REQUIRED = [ "schedule>=0.6.0", "cvxpy==1.0.21", "hyperopt==0.1.1", - "fire>=0.2.1", + "fire>=0.3.1", "statsmodels", "xlrd>=1.0.0", "plotly==4.12.0", @@ -57,8 +57,6 @@ REQUIRED = [ "tornado", "joblib>=0.17.0", "fire>=0.3.1", - "ruamel.yaml>=0.16.12", - "pytorch-tabnet>=2.0.1", ] # Numpy include From 130a7043dd78611d6689910d8ca707a8f4041d4f Mon Sep 17 00:00:00 2001 From: Young Date: Wed, 25 Nov 2020 13:04:14 +0000 Subject: [PATCH 144/241] update setup.py --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index c0733d5f0..3438781b2 100644 --- a/setup.py +++ b/setup.py @@ -57,6 +57,7 @@ REQUIRED = [ "tornado", "joblib>=0.17.0", "fire>=0.3.1", + "ruamel.yaml>=0.16.12", ] # Numpy include From b5722a56b43d1449eaaaad1b6b18005e332fc4d8 Mon Sep 17 00:00:00 2001 From: meng-ustc Date: Wed, 25 Nov 2020 21:06:15 +0800 Subject: [PATCH 145/241] add README.md --- examples/benchmarks/GATs/README.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 examples/benchmarks/GATs/README.md diff --git a/examples/benchmarks/GATs/README.md b/examples/benchmarks/GATs/README.md new file mode 100644 index 000000000..a69e7d433 --- /dev/null +++ b/examples/benchmarks/GATs/README.md @@ -0,0 +1,5 @@ +#GATs +* Graph Attention Networks(GATs) leverage masked self-attentional layers on graph-structured data. The nodes in stacked layers have different weights and they are able to attend over their +neighborhoods’ features, without requiring any kind of costly matrix operation (such as inversion) or depending on knowing the graph structure upfront. +* This code used in Qlib is implemented with PyTorch by ourselves. +* Paper: Graph Attention Networks https://arxiv.org/pdf/1710.10903.pdf \ No newline at end of file From 27f19c1f1b442e333358bc82a9cbcc09173dd34f Mon Sep 17 00:00:00 2001 From: meng-ustc Date: Wed, 25 Nov 2020 21:11:56 +0800 Subject: [PATCH 146/241] Add README.md for GATs --- examples/benchmarks/GATs/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/benchmarks/GATs/README.md b/examples/benchmarks/GATs/README.md index a69e7d433..f432b6c5b 100644 --- a/examples/benchmarks/GATs/README.md +++ b/examples/benchmarks/GATs/README.md @@ -1,4 +1,4 @@ -#GATs +# GATs * Graph Attention Networks(GATs) leverage masked self-attentional layers on graph-structured data. The nodes in stacked layers have different weights and they are able to attend over their neighborhoods’ features, without requiring any kind of costly matrix operation (such as inversion) or depending on knowing the graph structure upfront. * This code used in Qlib is implemented with PyTorch by ourselves. From f84cb64332f0bf005b6b10311972b39f61fca5cc Mon Sep 17 00:00:00 2001 From: lwwang1995 Date: Wed, 25 Nov 2020 22:05:36 +0800 Subject: [PATCH 147/241] Update xgb parameters. --- .../XGBoost/workflow_config_xgboost.yaml | 19 ++++++++----------- qlib/contrib/model/xgboost.py | 6 ++---- 2 files changed, 10 insertions(+), 15 deletions(-) diff --git a/examples/benchmarks/XGBoost/workflow_config_xgboost.yaml b/examples/benchmarks/XGBoost/workflow_config_xgboost.yaml index 407d56fb7..31eee8206 100644 --- a/examples/benchmarks/XGBoost/workflow_config_xgboost.yaml +++ b/examples/benchmarks/XGBoost/workflow_config_xgboost.yaml @@ -29,18 +29,15 @@ task: class: XGBModel module_path: qlib.contrib.model.xgboost kwargs: - objective: reg:linear - n_estimators: 5000 - colsample_bytree: 0.85 - learning_rate: 0.0421 - subsample: 0.8789 - max_depth: 8 - num_leaves: 210 - num_threads: 20 - missing: -1 - min_child_weight: 1 + eval_metric: rmse + colsample_bytree: 0.5 + eta: 0.2 + gamma: 0.55 + max_depth: 2 + min_child_weight: 1.0 + n_estimators: 647 + subsample: 0.8 nthread: 4 - tree_method: hist dataset: class: DatasetH module_path: qlib.data.dataset diff --git a/qlib/contrib/model/xgboost.py b/qlib/contrib/model/xgboost.py index b45e12e10..039fd2c80 100755 --- a/qlib/contrib/model/xgboost.py +++ b/qlib/contrib/model/xgboost.py @@ -22,10 +22,8 @@ from ...data.dataset.handler import DataHandlerLP class XGBModel(Model): """XGBModel Model""" - def __init__(self, obj="mse", **kwargs): - if obj not in {"mse", "binary"}: - raise NotImplementedError - self._params = {"obj": obj} + def __init__(self, **kwargs): + self._params = {} self._params.update(kwargs) self.model = None From 991f14a5b9ecf0ee7b8a2dd0835b6fb0008e5ee0 Mon Sep 17 00:00:00 2001 From: Haoyu Wang <48108414+javaThonc@users.noreply.github.com> Date: Wed, 25 Nov 2020 22:19:20 +0800 Subject: [PATCH 148/241] update freq --- examples/benchmarks/SFM/workflow_config_sfm.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/benchmarks/SFM/workflow_config_sfm.yaml b/examples/benchmarks/SFM/workflow_config_sfm.yaml index 9086bab4a..743c6220f 100644 --- a/examples/benchmarks/SFM/workflow_config_sfm.yaml +++ b/examples/benchmarks/SFM/workflow_config_sfm.yaml @@ -32,7 +32,7 @@ task: d_feat: 6 hidden_size: 64 output_dim: 1 - freq_dim: 15 + freq_dim: 20 dropout_W: 0.5 dropout_U: 0.5 n_epochs: 10 @@ -70,4 +70,4 @@ task: - class: PortAnaRecord module_path: qlib.workflow.record_temp kwargs: - config: *port_analysis_config \ No newline at end of file + config: *port_analysis_config From 5be847909f66ec45e5c98ab43b1bda917dcc00c3 Mon Sep 17 00:00:00 2001 From: Haoyu Wang <48108414+javaThonc@users.noreply.github.com> Date: Thu, 26 Nov 2020 00:29:37 +0800 Subject: [PATCH 149/241] Update workflow_config_catboost.yaml --- examples/benchmarks/CatBoost/workflow_config_catboost.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/benchmarks/CatBoost/workflow_config_catboost.yaml b/examples/benchmarks/CatBoost/workflow_config_catboost.yaml index 720599be5..9c15dc25b 100644 --- a/examples/benchmarks/CatBoost/workflow_config_catboost.yaml +++ b/examples/benchmarks/CatBoost/workflow_config_catboost.yaml @@ -36,7 +36,7 @@ task: num_leaves: 100 thread_count: 20 grow_policy: Lossguide - boostrap_type: Poisson + bootstrap_type: Poisson dataset: class: DatasetH module_path: qlib.data.dataset From 87cee85ceae13d9fc4d97e07bccda6d5c832cfb4 Mon Sep 17 00:00:00 2001 From: Jactus Date: Thu, 26 Nov 2020 00:55:26 +0800 Subject: [PATCH 150/241] Update docs and fix tabnet --- docs/component/data.rst | 234 ++++++++++++++---- docs/component/model.rst | 214 +++++++--------- docs/component/recorder.rst | 6 +- docs/reference/api.rst | 14 ++ docs/start/integration.rst | 103 ++++---- examples/benchmarks/HATS/README.md | 4 +- examples/benchmarks/TFT/README.md | 2 +- .../TabNet/workflow_config_tabnet.yaml | 2 +- qlib/contrib/evaluate.py | 84 +++---- .../analysis_model_performance.py | 16 +- .../analysis_position/cumulative_return.py | 6 +- .../report/analysis_position/rank_label.py | 6 +- .../report/analysis_position/report.py | 6 +- .../report/analysis_position/risk_analysis.py | 6 +- .../report/analysis_position/score_ic.py | 6 +- qlib/contrib/strategy/strategy.py | 70 +++--- qlib/data/base.py | 8 +- qlib/data/cache.py | 40 +-- qlib/data/client.py | 8 +- qlib/data/data.py | 92 +++---- qlib/data/dataset/__init__.py | 30 +-- qlib/data/dataset/handler.py | 38 +-- qlib/data/dataset/loader.py | 8 +- qlib/data/dataset/processor.py | 6 +- qlib/data/filter.py | 60 ++--- qlib/model/base.py | 6 +- qlib/model/riskmodel.py | 44 ++-- 27 files changed, 624 insertions(+), 495 deletions(-) diff --git a/docs/component/data.rst b/docs/component/data.rst index 9ef71a6cb..e14caff3e 100644 --- a/docs/component/data.rst +++ b/docs/component/data.rst @@ -1,7 +1,7 @@ .. _data: ================================ -Data Layer: Data Framework&Usage +Data Layer: Data Framework & Usage ================================ Introduction @@ -15,7 +15,9 @@ The introduction of ``Data Layer`` includes the following parts. - Data Preparation - Data API +- Data Loader - Data Handler +- Dataset - Cache - Data and Cache File Structure @@ -146,43 +148,161 @@ Filter To know more about ``Filter``, please refer to `Filter API <../reference/api.html#module-qlib.data.filter>`_. - Reference ------------- To know more about ``Data API``, please refer to `Data API <../reference/api.html#data>`_. + +Data Loader +================= + +``Data Loader`` in ``Qlib`` is designed to load raw data from the original data source. It will be loaded and used in the ``Data Handler`` module. + +The ``QlibDataLoader`` class in ``Qlib`` is such an interface that allows users to load raw data from the data source. + +Interface +------------ + +Here are some interfaces of the ``QlibDataLoader`` class: + +- `load(instruments, start_time=None, end_time=None)` + - This method loads the data as pd.DataFrame + - Parameters: + - `instruments` : str or dict + it can either be the market name or the config file of instruments generated by InstrumentProvider. + - `start_time` : str + start of the time range. + - `end_time` : str + end of the time range. + - Returns: + - The data being loaded with type `pd.DataFrame` + +- `load_group_df(instruments, exprs: list, names: list, start_time=None, end_time=None)` + - This method loads the dataframe for specific group. + - Parameters: + - `instruments` : str or dict + it can either be the market name or the config file of instruments generated by InstrumentProvider. + - `exprs` : list + the expressions to describe the content of the data. + - `names` : list + the name of the data. + - `start_time` : str + start of the time range. + - `end_time` : str + end of the time range. + - Returns: + - The queried data in type `pd.DataFrame`. + +API +----------- + +To know more about ``Data Loader``, please refer to `Data Loader API <../reference/api.html#module-qlib.data.dataset.loader>`_. + + Data Handler ================= -Users can use ``Data Handler`` in an automatic workflow by ``Estimator``, refer to `Estimator: Workflow Management `_ for more details. +The ``Data Handler`` module in ``Qlib`` is designed to handler those common data processing methods which will be used by most of the models. + +Users can use ``Data Handler`` in an automatic workflow by ``qrun``, refer to `Workflow: Workflow Management `_ for more details. -Also, ``Data Handler`` can be used as an independent module, by which users can easily preprocess data(standardization, remove NaN, etc.) and build datasets. It is a subclass of ``qlib.data.dataset.handler.DataHandlerLP``, which provides some interfaces as follows. Base Class & Interface ---------------------- -Qlib provides a base class `qlib.data.dataset.DataHandlerLP <../reference/api.html#qlib.data.dataset.handler.DataHandlerLP>`_, which provides the following interfaces: +In addition to use ``Data Handler`` in an automatic workflow with ``qrun``, ``Data Handler`` can be used as an independent module, by which users can easily preprocess data (standardization, remove NaN, etc.) and build datasets. -- `load_feature` - Implement the interface to load the data features. +In order to achieve so, ``Qlib`` provides a base class `qlib.data.dataset.DataHandlerLP <../reference/api.html#qlib.data.dataset.handler.DataHandlerLP>`_. The core idea of this class is that: we will have some leanable ``Processors`` which can learn the parameters of data processing. When new data comes in, these `trained` ``Processors`` can then infer on the new data and thus processing real-time data in an efficient way. More information about ``Processors`` will be listed in the next subsection. -- `load_label` - Implement the interface to load the data labels and calculate the users' labels. +Here are some important interfaces that ``DataHandlerLP`` provides: -- `setup_processed_data` - Implement the interface for data preprocessing, such as preparing feature columns, discarding blank lines, and so on. +- `__init__(instruments=None, start_time=None, end_time=None, data_loader: Tuple[dict, str, DataLoader] = None, infer_processors=[], learn_processors=[], process_type=PTYPE_A, **kwargs)` + - Initialization of the class. + - Parameters: + - `infer_processors` : list + - list of of processors to generate data for inference + - example of : -Qlib also provides two functions to help users init the data handler, users can override them for users' needs. + .. code-block:: + + 1) classname & kwargs: + { + "class": "MinMaxNorm", + "kwargs": { + "fit_start_time": "20080101", + "fit_end_time": "20121231" + } + } + 2) Only classname: + "DropnaFeature" + 3) object instance of Processor -- `_init_raw_data` - Users can init the raw df, feature names, and label names of data handler in this function. - If the index of feature df and label df are not the same, users need to override this method to merge them (e.g. inner, left, right merge). + - `learn_processors` : list + similar to infer_processors, but for generating data for learning models + + - `process_type`: str + - PTYPE_I = 'independent' + - self._infer will processed by infer_processors + - self._learn will be processed by learn_processors + - PTYPE_A = 'append' + - self._infer will processed by infer_processors + - self._learn will be processed by infer_processors + learn_processors + - (e.g. self._infer processed by learn_processors ) + +- `fetch(selector: Union[pd.Timestamp, slice, str] = slice(None, None), level: Union[str, int] = "datetime", col_set=DataHandler.CS_ALL, data_key: str = DK_I)` + - This method fetches data from underlying data source + - Parameters: + - `selector` : Union[pd.Timestamp, slice, str] + describe how to select data by index. + - `level` : Union[str, int] + which index level to select the data. + - `col_set` : str + select a set of meaningful columns.(e.g. features, columns). + - `data_key` : str + The data to fetch: DK_*. + - Returns: + - The retrieved results in the type: `pd.DataFrame`. + +- `get_cols(col_set=DataHandler.CS_ALL, data_key: str = DK_I)` + - This method gets the column names. + - Parameters: + - `col_set` : str + select a set of meaningful columns.(e.g. features, columns). + - `data_key` : str + the data to fetch: DK_*. + - Returns: + - A list of column names. If users want to load features and labels by config, users can inherit ``qlib.data.dataset.handler.ConfigDataHandler``, ``Qlib`` also provides some preprocess method in this subclass. If users want to use qlib data, `QLibDataHandler` is recommended. Users can inherit their custom class from `QLibDataHandler`, which is also a subclass of `ConfigDataHandler`. +Processor +---------- + +The ``Processor`` module in ``Qlib`` is designed to be learnable and it is responsible for handling data processing such as `normalization` and `drop none/nan features/labels`. + +``Qlib`` provides the following ``Processors``: + +- ``DropnaProcessor``: `processor` that drops N/A features. +- ``DropnaLabel``: `processor` that drops N/A labels. +- ``TanhProcess``: `processor` that uses `tanh` to process noise data. +- ``ProcessInf``: `processor` that handles infinity values, it will be replaces by the mean of the column. +- ``Fillna``: `processor` that handles N/A values, which will fill the N/A value by 0 or other given number. +- ``MinMaxNorm``: `processor` that applies min-max normalization. +- ``ZscoreNorm``: `processor` that applies z-score normalization. +- ``CSZScoreNorm``: `processor` that applies cross sectional z-score normalization. +- ``CSRankNorm``: `processor` that applies cross sectional rank normalization. + +Users can also create their own `processor` by inheriting the base class of ``Processor``. Please refer to the implementation of all the processors for more information (`Processor Link `_). + +API +--------- + +To know more about ``Processor``, please refer to `Processor API <../reference/api.html#module-qlib.data.dataset.processor>`_. + + Usage -------------- @@ -194,15 +314,12 @@ Usage - `get_rolling_data` - According to the start and end dates, and `rolling_period`, an iterator is returned, which can be used to traverse the features and labels used for rolling. - - - Example -------------- -``Data Handler`` can be run with ``estimator`` by modifying the configuration file, and can also be used as a single module. +``Data Handler`` can be run with ``qrun`` by modifying the configuration file, and can also be used as a single module. -Know more about how to run ``Data Handler`` with ``Estimator``, please refer to `Estimator: Workflow Management `_ +Know more about how to run ``Data Handler`` with ``qrun``, please refer to `Workflow: Workflow Management `_ Qlib provides implemented data handler `Alpha158`. The following example shows how to run `Alpha158` as a single module. @@ -211,45 +328,70 @@ Qlib provides implemented data handler `Alpha158`. The following example shows h .. code-block:: Python + import qlib from qlib.contrib.data.handler import Alpha158 - from qlib.contrib.model.gbdt import LGBModel - DATA_HANDLER_CONFIG = { - "dropna_label": True, - "start_date": "2007-01-01", - "end_date": "2020-08-01", - "market": "csi300", + data_handler_config = { + "start_time": "2008-01-01", + "end_time": "2020-08-01", + "fit_start_time": "2008-01-01", + "fit_end_time": "2014-12-31", + "instruments": "csi300", } - TRAINER_CONFIG = { - "train_start_date": "2007-01-01", - "train_end_date": "2014-12-31", - "validate_start_date": "2015-01-01", - "validate_end_date": "2016-12-31", - "test_start_date": "2017-01-01", - "test_end_date": "2020-08-01", - } + if __name__ == "__main__": + qlib.init() + h = Alpha158(**data_handler_config) - exampleDataHandler = Alpha158(**DATA_HANDLER_CONFIG) + # get all the columns of the data + print(h.get_cols()) - # example of 'get_split_data' - x_train, y_train, x_validate, y_validate, x_test, y_test = exampleDataHandler.get_split_data(**TRAINER_CONFIG) + # fetch all the labels + print(h.fetch(col_set="label")) - # example of 'get_rolling_data' - - for (x_train, y_train, x_validate, y_validate, x_test, y_test) in exampleDataHandler.get_rolling_data(**TRAINER_CONFIG): - print(x_train, y_train, x_validate, y_validate, x_test, y_test) - - -.. note:: (x_train, y_train, x_validate, y_validate, x_test, y_test) can be used as arguments for the `fit`, `predic``, and `score` methods of the ``Interday Model`` , please refer to `Model `_. - -Also, the above example has been given in ``examples.estimator.train_backtest_analyze.ipynb``. + # fetch all the features + print(h.fetch(col_set="feature")) API --------- To know more about ``Data Handler``, please refer to `Data Handler API <../reference/api.html#module-qlib.data.dataset.handler>`_. + +Dataset +================= + +The ``Dataset`` module in ``Qlib`` aims to prepare data for model training and inferencing. + +The motivation of this module is that we want to maximize the flexibility of of different models to handle data that are suitable for themselves. This module gives the model the rights to process their data in an unique way. For instance, models such as ``GBDT`` may work well on data that contains `nan` or `None` value, while neural networks such as ``DNN`` will break down on such data. + +The ``DatasetH`` class is the `dataset` with `Data Handler`. Here is the most important interface of the class: + +- `prepare(segments: Union[List[str], Tuple[str], str, slice], col_set=DataHandler.CS_ALL, data_key=DataHandlerLP.DK_I, **kwargs)` + - This method prepares the data for learning and inference. + - Parameters: + - `segments` : Union[List[str], Tuple[str], str, slice] + Describe the scope of the data to be prepared + Here are some examples: + + - 'train' + + - ['train', 'valid'] + + - `col_set` : str + The col_set will be passed to self._handler when fetching data. + - `data_key` : str + The data to fetch: DK_* + Default is DK_I, which indicate fetching data for **inference**. + + +API +--------- + +To know more about ``Dataset``, please refer to `Dataset API <../reference/api.html#module-qlib.data.dataset.__init__>`_. + + + Cache ========== diff --git a/docs/component/model.rst b/docs/component/model.rst index 6a6b02f86..b4e341df8 100644 --- a/docs/component/model.rst +++ b/docs/component/model.rst @@ -7,7 +7,7 @@ Interday Model: Model Training & Prediction Introduction =================== -``Interday Model`` is designed to make the `prediction score` about stocks. Users can use the ``Interday Model`` in an automatic workflow by ``Estimator``, please refer to `Estimator: Workflow Management `_. +``Interday Model`` is designed to make the `prediction score` about stocks. Users can use the ``Interday Model`` in an automatic workflow by ``qrun``, please refer to `Workflow: Workflow Management `_. Because the components in ``Qlib`` are designed in a loosely-coupled way, ``Interday Model`` can be used as an independent module also. @@ -20,151 +20,125 @@ The base class provides the following interfaces: - `__init__(**kwargs)` - Initialization. - - If users use ``Estimator`` to start an `experiment`, the parameter of `__init__` method shoule be consistent with the hyperparameters in the configuration file. -- `fit(self, x_train, y_train, x_valid, y_valid, w_train=None, w_valid=None, **kwargs)` +- `fit(self, dataset, **kwargs)` - Train model. - Parameter: - - `x_train`, pd.DataFrame type, train feature - The following example explains the value of `x_train`: + - `dataset`, ``Qlib``'s ``DatasetH`` type. For more information about ``DatasetH``, users can refer to the related document: `Qlib Dataset <../component/data.html#dataset>`_. + The `dataset` is passed into the `model`'s method because there are some unique data preprocessing procedures for each, we want to give each model maximum flexibility to handle the data that is suitable for their own. + The following code example shows how to retrieve `x_train`, `y_train` and `w_train` from the `dataset`: - .. code-block:: YAML - - KMID KLEN KMID2 KUP KUP2 - instrument datetime - SH600004 2012-01-04 0.000000 0.017685 0.000000 0.012862 0.727275 - 2012-01-05 -0.006473 0.025890 -0.250001 0.012945 0.499998 - 2012-01-06 0.008117 0.019481 0.416666 0.008117 0.416666 - 2012-01-09 0.016051 0.025682 0.624998 0.006421 0.250001 - 2012-01-10 0.017323 0.026772 0.647057 0.003150 0.117648 - ... ... ... ... ... ... - SZ300273 2014-12-25 -0.005295 0.038697 -0.136843 0.016293 0.421052 - 2014-12-26 -0.022486 0.041701 -0.539215 0.002453 0.058824 - 2014-12-29 -0.031526 0.039092 -0.806451 0.000000 0.000000 - 2014-12-30 -0.010000 0.032174 -0.310811 0.013913 0.432433 - 2014-12-31 0.010917 0.020087 0.543479 0.001310 0.065216 + .. code-block:: Python - - `x_train` is a pandas DataFrame, whose index is MultiIndex . Each column of `x_train` corresponds to a feature, and the column name is the feature name. - - .. note:: - - The number and names of the columns are determined by the data handler, please refer to `Data Handler `_ and `Estimator Data Section `_. - - - `y_train`, pd.DataFrame type, train label - The following example explains the value of `y_train`: + # get features and labels + df_train, df_valid = dataset.prepare( + ["train", "valid"], col_set=["feature", "label"], data_key=DataHandlerLP.DK_L + ) + x_train, y_train = df_train["feature"], df_train["label"] + x_valid, y_valid = df_valid["feature"], df_valid["label"] - .. code-block:: YAML - - LABEL - instrument datetime - SH600004 2012-01-04 -0.798456 - 2012-01-05 -1.366716 - 2012-01-06 -0.491026 - 2012-01-09 0.296900 - 2012-01-10 0.501426 - ... ... - SZ300273 2014-12-25 -0.465540 - 2014-12-26 0.233864 - 2014-12-29 0.471368 - 2014-12-30 0.411914 - 2014-12-31 1.342723 - - `y_train` is a pandas DataFrame, whose index is MultiIndex . The `LABEL` column represents the value of train label. - - .. note:: - - The number and names of the columns are determined by the ``Data Handler``, please refer to `Data Handler `_. - - - `x_valid`, pd.DataFrame type, validation feature - The format of `x_valid` is same as `x_train` - - - - `y_valid`, pd.DataFrame type, validation label - The format of `y_valid` is same as `y_train` - - - `w_train`(Optional args, default is None), pd.DataFrame type, train weight - `w_train` is a pandas DataFrame, whose shape and index is same as `x_train`. The float value in `w_train` represents the weight of the feature at the same position in `x_train`. - - - `w_train`(Optional args, default is None), pd.DataFrame type, validation weight - `w_train` is a pandas DataFrame, whose shape and index is the same as `x_valid`. The float value in `w_train` represents the weight of the feature at the same position in `x_train`. - -- `predict(self, x_test, **kwargs)` - - Predict test data 'x_test' - - Parameter: - - `x_test`, pd.DataFrame type, test features - The form of `x_test` is same as `x_train` in 'fit' method. - - Return: - - `label`, np.ndarray type, test label - The label of `x_test` that predicted by model. - -- `score(self, x_test, y_test, w_test=None, **kwargs)` - - Evaluate model with test feature/label - - Parameter: - - `x_test`, pd.DataFrame type, test feature - The format of `x_test` is same as `x_train` in `fit` method. + # get weights + try: + wdf_train, wdf_valid = dataset.prepare(["train", "valid"], col_set=["weight"], data_key=DataHandlerLP.DK_L) + w_train, w_valid = wdf_train["weight"], wdf_valid["weight"] + except KeyError as e: + w_train = pd.DataFrame(np.ones_like(y_train.values), index=y_train.index) + w_valid = pd.DataFrame(np.ones_like(y_valid.values), index=y_valid.index) - - `x_test`, pd.DataFrame type, test label - The format of `y_test` is same as `y_train` in `fit` method. +- `predict(self, dataset, **kwargs)` + - Predict test data. + - Parameter: + - `dataset`, ``Qlib``'s ``DatasetH`` type. The usage is similar to the example above. + - Returns: + - Predic results with type: `pandas.Series`. - - `w_test`, pd.DataFrame type, test weight - The format of `w_test` is same as `w_train` in `fit` method. - - Return: float type, evaluation score +- `finetune(self, dataset, **kwargs)` + - Finetune the model. + - Parameter: + - `dataset`, ``Qlib``'s ``DatasetH`` type. The usage is similar to the example above. -For other interfaces such as `save`, `load`, `finetune`, please refer to `Model API <../reference/api.html#module-qlib.model.base>`_. + +For other interfaces such as `finetune`, please refer to `Model API <../reference/api.html#module-qlib.model.base>`_. Example ================== -``Qlib`` provides ``LightGBM`` and ``DNN`` models as the baseline, the following steps show how to run`` LightGBM`` as an independent module. +``Qlib``'s `Model Zoo` includes models such as ``LightGBM``, ``DNN``, ``LSTM``, etc.. These models are treated as the baselines of ``Interday Model``. The following steps show how to run`` LightGBM`` as an independent module. - Initialize ``Qlib`` with `qlib.init` first, please refer to `Initialization <../start/initialization.html>`_. - Run the following code to get the `prediction score` `pred_score` .. code-block:: Python - from qlib.contrib.data.handler import Alpha158 from qlib.contrib.model.gbdt import LGBModel + from qlib.contrib.data.handler import Alpha158 + from qlib.utils import init_instance_by_config, flatten_dict + from qlib.workflow import R + from qlib.workflow.record_temp import SignalRecord, PortAnaRecord - DATA_HANDLER_CONFIG = { - "dropna_label": True, - "start_date": "2007-01-01", - "end_date": "2020-08-01", - "market": MARKET, + market = "csi300" + benchmark = "SH000300" + + data_handler_config = { + "start_time": "2008-01-01", + "end_time": "2020-08-01", + "fit_start_time": "2008-01-01", + "fit_end_time": "2014-12-31", + "instruments": market, } - TRAINER_CONFIG = { - "train_start_date": "2007-01-01", - "train_end_date": "2014-12-31", - "validate_start_date": "2015-01-01", - "validate_end_date": "2016-12-31", - "test_start_date": "2017-01-01", - "test_end_date": "2020-08-01", + 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": ("2008-01-01", "2014-12-31"), + "valid": ("2015-01-01", "2016-12-31"), + "test": ("2017-01-01", "2020-08-01"), + }, + }, + }, } + + # model initiaiton + model = init_instance_by_config(task["model"]) + dataset = init_instance_by_config(task["dataset"]) - x_train, y_train, x_validate, y_validate, x_test, y_test = Alpha158( - **DATA_HANDLER_CONFIG - ).get_split_data(**TRAINER_CONFIG) + # start exp + with R.start(experiment_name="workflow"): + # train + R.log_params(**flatten_dict(task)) + model.fit(dataset) + # prediction + recorder = R.get_recorder() + sr = SignalRecord(model, dataset, recorder) + sr.generate() - MODEL_CONFIG = { - "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, - } - # use default model - model = LGBModel(**MODEL_CONFIG) - model.fit(x_train, y_train, x_validate, y_validate) - _pred = model.predict(x_test) - pred_score = pd.DataFrame(index=_pred.index) - pred_score["score"] = _pred.iloc(axis=1)[0] - - .. note:: `Alpha158` is the data handler provided by ``Qlib``, please refer to `Data Handler `_. + .. note:: + + `Alpha158` is the data handler provided by ``Qlib``, please refer to `Data Handler `_. + `SignalRecord` is the `Record Template` in ``Qlib``, please refer to `Workflow `_. Also, the above example has been given in ``examples/train_backtest_analyze.ipynb``. diff --git a/docs/component/recorder.rst b/docs/component/recorder.rst index efd67e859..0d1e83168 100644 --- a/docs/component/recorder.rst +++ b/docs/component/recorder.rst @@ -402,8 +402,8 @@ Record Template The ``RecordTemp`` class is a class that enables generate experiment results such as IC and backtest in a certain format. We have provided three different `Record Template` class: -- ``SignalRecord``: This class generates the `preidction` of the model. -- ``SigAnaRecord``: This class generates the `IC`, `ICIR`, `Rank IC` and `Rank ICIR`. +- ``SignalRecord``: This class generates the `preidction` results of the model. +- ``SigAnaRecord``: This class generates the `IC`, `ICIR`, `Rank IC` and `Rank ICIR` of the model. - ``PortAnaRecord``: This class generates the results of `backtest`. The detailed information about `backtest` as well as the available `strategy`, users can refer to `Strategy <../component/strategy.html>`_ and `Backtest <../component/backtest.html>`_. -For more information, please refer to `Record Template API <../reference/api.html#module-qlib.workflow.record_temp>`_. \ No newline at end of file +For more information about the APIs, please refer to `Record Template API <../reference/api.html#module-qlib.workflow.record_temp>`_. \ No newline at end of file diff --git a/docs/reference/api.rst b/docs/reference/api.rst index 76d2a74a5..d99a26f49 100644 --- a/docs/reference/api.rst +++ b/docs/reference/api.rst @@ -60,12 +60,26 @@ Cache Contrib ==================== +Data Loader +--------------- +.. automodule:: qlib.data.dataset.loader + :members: Data Handler --------------- .. automodule:: qlib.data.dataset.handler :members: +Processor +--------------- +.. automodule:: qlib.data.dataset.processor + :members: + +Dataset +--------------- +.. automodule:: qlib.data.dataset.__init__ + :members: + Model -------------------- .. automodule:: qlib.model.base diff --git a/docs/start/integration.rst b/docs/start/integration.rst index 5276729b5..102d88425 100644 --- a/docs/start/integration.rst +++ b/docs/start/integration.rst @@ -5,7 +5,7 @@ Custom Model Integration Introduction =================== -``Qlib`` provides ``lightGBM`` and ``Dnn`` model as the baseline of ``Interday Model``. In addition to the default model, users can integrate their own custom models into ``Qlib``. +``Qlib``'s `Model Zoo` includes models such as ``LightGBM``, ``DNN``, ``LSTM``, etc.. These models are treated as the baselines of ``Interday Model``. In addition to the default models ``Qlib`` provide, users can integrate their own custom models into ``Qlib``. Users can integrate their own custom models according to the following steps. @@ -32,79 +32,76 @@ The Custom models need to inherit `qlib.model.base.Model <../reference/api.html# - Override the `fit` method - ``Qlib`` calls the fit method to train the model - - The parameters must include training feature `x_train`, training label `y_train`, test feature `x_valid`, test label `y_valid` at least. - - The parameters could include some optional parameters with default values, such as train weight `w_train`, test weight `w_valid` and `num_boost_round = 1000`. + - The parameters must include training feature `dataset`. + - The parameters could include some optional parameters with default values, such as `num_boost_round = 1000` for `GBDT`. - Code Example: In the following example, `num_boost_round = 1000` is an optional parameter. .. code-block:: Python - def fit(self, x_train:pd.DataFrame, y_train:pd.DataFrame, x_valid:pd.DataFrame, y_valid:pd.DataFrame, - w_train:pd.DataFrame = None, w_valid:pd.DataFrame = None, num_boost_round = 1000, **kwargs): + def fit(self, dataset: DatasetH, num_boost_round = 1000, **kwargs): + + # prepare dataset for lgb training and evaluation + df_train, df_valid = dataset.prepare( + ["train", "valid"], col_set=["feature", "label"], data_key=DataHandlerLP.DK_L + ) + x_train, y_train = df_train["feature"], df_train["label"] + x_valid, y_valid = df_valid["feature"], df_valid["label"] # Lightgbm need 1D array as its label if y_train.values.ndim == 2 and y_train.values.shape[1] == 1: - y_train_1d, y_valid_1d = np.squeeze(y_train.values), np.squeeze(y_valid.values) + y_train, y_valid = np.squeeze(y_train.values), np.squeeze(y_valid.values) else: - raise ValueError('LightGBM doesn\'t support multi-label training') + raise ValueError("LightGBM doesn't support multi-label training") - w_train_weight = None if w_train is None else w_train.values - w_valid_weight = None if w_valid is None else w_valid.values + dtrain = lgb.Dataset(x_train.values, label=y_train) + dvalid = lgb.Dataset(x_valid.values, label=y_valid) - dtrain = lgb.Dataset(x_train.values, label=y_train_1d, weight=w_train_weight) - dvalid = lgb.Dataset(x_valid.values, label=y_valid_1d, weight=w_valid_weight) - self._model = lgb.train( - self._params, - dtrain, + # fit the model + self.model = lgb.train( + self.params, + dtrain, num_boost_round=num_boost_round, valid_sets=[dtrain, dvalid], - valid_names=['train', 'valid'], + valid_names=["train", "valid"], + early_stopping_rounds=early_stopping_rounds, + verbose_eval=verbose_eval, + evals_result=evals_result, **kwargs ) - Override the `predict` method - - The parameters include the test features. + - The parameters must include training feature `dataset`, which will be userd to get the test dataset. - Return the `prediction score`. - Please refer to `Model API <../reference/api.html#module-qlib.model.base>`_ for the parameter types of the fit method. - - Code Example: In the following example, users need to use dnn to predict the label(such as `preds`) of test data `x_test` and return it. + - Code Example: In the following example, users need to use `LightGBM` to predict the label(such as `preds`) of test data `x_test` and return it. .. code-block:: Python - def predict(self, x_test:pd.DataFrame, **kwargs)-> numpy.ndarray: - if self._model is None: - raise ValueError('model is not fitted yet!') - return self._model.predict(x_test.values) + def predict(self, dataset: DatasetH, **kwargs)-> pandas.Series: + if self.model is None: + raise ValueError("model is not fitted yet!") + x_test = dataset.prepare("test", col_set="feature", data_key=DataHandlerLP.DK_I) + return pd.Series(self.model.predict(x_test.values), index=x_test.index) -- Override the `save` method & `load` method - - The `save` method parameter includes the a `filename` that represents an absolute path, user need to save model into the path. - - The `load` method parameter includes the a `buffer` read from the `filename` passed in the `save` method, users need to load model from the `buffer`. - - Code Example: +- Override the `finetune` method + - The parameters must include training feature `dataset`. + - Code Example: In the following example, users will use `LightGBM` as the model and finetune it. .. code-block:: Python - def save(self, filename): - if self._model is None: - raise ValueError('model is not fitted yet!') - self._model.save_model(filename) - - def load(self, buffer): - self._model = lgb.Booster(params={'model_str': buffer.decode('utf-8')}) - -.. Without tuner, this part will not be used -.. - Override the `score` method(This step is optional) -.. - The parameters include the test features and test labels. -.. - Return the evaluation score of the model. It's recommended to adopt the loss between labels and `prediction score`. -.. - Code Example: In the following example, users need to calculate the weighted loss with test data `x_test`, test label `y_test` and the weight `w_test`. -.. .. code-block:: Python -.. -.. def score(self, x_test:pd.Dataframe, y_test:pd.Dataframe, w_test:pd.DataFrame = None) -> float: -.. # Remove rows from x, y and w, which contain Nan in any columns in y_test. -.. x_test, y_test, w_test = drop_nan_by_y_index(x_test, y_test, w_test) -.. preds = self.predict(x_test) -.. w_test_weight = None if w_test is None else w_test.values -.. scorer = mean_squared_error if self.loss_type == 'mse' else roc_auc_score -.. return scorer(y_test.values, preds, sample_weight=w_test_weight) + def finetune(self, dataset: DatasetH, num_boost_round=10, verbose_eval=20): + dtrain, _ = self._prepare_data(dataset) + self.model = lgb.train( + self.params, + dtrain, + num_boost_round=num_boost_round, + init_model=self.model, + valid_sets=[dtrain], + valid_names=["train"], + verbose_eval=verbose_eval, + ) Configuration File ======================= -The configuration file is described in detail in the `estimator <../component/estimator.html#complete-example>`_ document. In order to integrate the custom model into ``Qlib``, users need to modify the "model" field in the configuration file. +The configuration file is described in detail in the `Workflow <../component/workflow.html#complete-example>`_ document. In order to integrate the custom model into ``Qlib``, users need to modify the "model" field in the configuration file. - Example: The following example describes the `model` field of configuration file about the custom lightgbm model mentioned above, where `module_path` is the module path, `class` is the class name, and `args` is the hyperparameter passed into the __init__ method. All parameters in the field is passed to `self._params` by `\*\*kwargs` in `__init__` except `loss = mse`. @@ -124,20 +121,20 @@ The configuration file is described in detail in the `estimator <../component/es num_leaves: 210 num_threads: 20 -Users could find configuration file of the baseline of the ``Model`` in ``qlib/examples/estimator/estimator_config.yaml`` and ``qlib/examples/estimator/estimator_config_dnn.yaml`` +Users could find configuration file of the baselines of the ``Model`` in ``examples/benchmarks``. All the configurations of different models are listed under the corresponding model folder. Model Testing ===================== -Assuming that the configuration file is ``examples/estimator/estimator_config.yaml``, users can run the following command to test the custom model: +Assuming that the configuration file is ``examples/benchmarks/LightGBM/workflow_config_lightgbm.yaml``, users can run the following command to test the custom model: .. code-block:: bash cd examples # Avoid running program under the directory contains `qlib` - estimator -c estimator/estimator_config.yaml + qrun benchmarks/LightGBM/workflow_config_lightgbm.yaml -.. note:: ``estimator`` is a built-in command of ``Qlib``. +.. note:: ``qrun`` is a built-in command of ``Qlib``. -Also, ``Model`` can also be tested as a single module. An example has been given in ``examples/train_backtest_analyze.ipynb``. +Also, ``Model`` can also be tested as a single module. An example has been given in ``examples/workflow_by_code.ipynb``. Reference diff --git a/examples/benchmarks/HATS/README.md b/examples/benchmarks/HATS/README.md index 95619e1ee..b70dbff25 100644 --- a/examples/benchmarks/HATS/README.md +++ b/examples/benchmarks/HATS/README.md @@ -1,11 +1,11 @@ -##Requirement +## Requirement * pandas==1.1.2 * numpy==1.17.4 * scikit_learn==0.23.2 * torch==1.7.0 -##HATS +## HATS * HATS is a a hierarchical attention network for stock prediction which uses relational data for stock market prediction. HATS selectively aggregates information on different relation types and adds the information to the representations of each company. HATS is used as a relational modeling module with initialized node representations.Furthermore, HATS diff --git a/examples/benchmarks/TFT/README.md b/examples/benchmarks/TFT/README.md index 6d605a1bd..a64ca0129 100644 --- a/examples/benchmarks/TFT/README.md +++ b/examples/benchmarks/TFT/README.md @@ -5,7 +5,7 @@ **GitHub**: https://github.com/google-research/google-research/tree/master/tft ## Run the Workflow -Users can follow the ``workflow_by_code_tft.py`` to run the benchmark. +Users can follow the ``workflow_by_code_tft.py`` to run the benchmark. Please be **aware** that this script can only support Python 3.5 - 3.8. ### Notes 1. The model must run in GPU, or an error will be raised. diff --git a/examples/benchmarks/TabNet/workflow_config_tabnet.yaml b/examples/benchmarks/TabNet/workflow_config_tabnet.yaml index 0ee95f238..5f6aa8b6d 100644 --- a/examples/benchmarks/TabNet/workflow_config_tabnet.yaml +++ b/examples/benchmarks/TabNet/workflow_config_tabnet.yaml @@ -44,7 +44,7 @@ task: module_path: qlib.data.dataset kwargs: handler: - class: Alpha158 + class: ALPHA360_Denoise module_path: qlib.contrib.data.handler kwargs: *data_handler_config segments: diff --git a/qlib/contrib/evaluate.py b/qlib/contrib/evaluate.py index 2b85f1a9b..4bb5e4372 100644 --- a/qlib/contrib/evaluate.py +++ b/qlib/contrib/evaluate.py @@ -26,9 +26,9 @@ def risk_analysis(r, N=252): Parameters ---------- r : pandas.Series - daily return series + daily return series. N: int - scaler for annualizing information_ratio (day: 250, week: 50, month: 12) + scaler for annualizing information_ratio (day: 250, week: 50, month: 12). """ mean = r.mean() std = r.std(ddof=1) @@ -61,7 +61,7 @@ def get_strategy( ---------- strategy : Strategy() - strategy used in backtest + strategy used in backtest. topk : int (Default value: 50) top-N stocks to buy. margin : int or float(Default value: 0.5) @@ -73,14 +73,14 @@ def get_strategy( sell_limit = pred_in_a_day.count() * margin - buffer margin, in single score_mode, continue holding stock if it is in nlargest(sell_limit) - sell_limit should be no less than topk + buffer margin, in single score_mode, continue holding stock if it is in nlargest(sell_limit). + sell_limit should be no less than topk. n_drop : int - number of stocks to be replaced in each trading date + number of stocks to be replaced in each trading date. risk_degree: float - 0-1, 0.95 for example, use 95% money to trade + 0-1, 0.95 for example, use 95% money to trade. str_type: 'amount', 'weight' or 'dropout' - strategy type: TopkAmountStrategy ,TopkWeightStrategy or TopkDropoutStrategy + strategy type: TopkAmountStrategy ,TopkWeightStrategy or TopkDropoutStrategy. Returns ------- @@ -126,21 +126,21 @@ def get_exchange( ---------- # exchange related arguments - exchange: Exchange() + exchange: Exchange(). subscribe_fields: list - subscribe fields + subscribe fields. open_cost : float - open transaction cost + open transaction cost. close_cost : float - close transaction cost + close transaction cost. min_cost : float - min transaction cost + min transaction cost. trade_unit : int - 100 for China A + 100 for China A. deal_price: str - dealing price type: 'close', 'open', 'vwap' + dealing price type: 'close', 'open', 'vwap'. limit_threshold : float - limit move 0.1 (10%) for example, long and short with same limit + limit move 0.1 (10%) for example, long and short with same limit. extract_codes: bool will we pass the codes extracted from the pred to the exchange. NOTE: This will be faster with offline qlib. @@ -193,20 +193,20 @@ def backtest(pred, account=1e9, shift=1, benchmark="SH000905", verbose=True, **k - **backtest workflow related or commmon arguments** pred : pandas.DataFrame - predict should has index and one `score` column + predict should has index and one `score` column. account : float - init account value + init account value. shift : int - whether to shift prediction by one day + whether to shift prediction by one day. benchmark : str - benchmark code, default is SH000905 CSI 500 + benchmark code, default is SH000905 CSI 500. verbose : bool - whether to print log + whether to print log. - **strategy related arguments** strategy : Strategy() - strategy used in backtest + strategy used in backtest. topk : int (Default value: 50) top-N stocks to buy. margin : int or float(Default value: 0.5) @@ -218,33 +218,33 @@ def backtest(pred, account=1e9, shift=1, benchmark="SH000905", verbose=True, **k sell_limit = pred_in_a_day.count() * margin - buffer margin, in single score_mode, continue holding stock if it is in nlargest(sell_limit) - sell_limit should be no less than topk + buffer margin, in single score_mode, continue holding stock if it is in nlargest(sell_limit). + sell_limit should be no less than topk. n_drop : int - number of stocks to be replaced in each trading date + number of stocks to be replaced in each trading date. risk_degree: float - 0-1, 0.95 for example, use 95% money to trade + 0-1, 0.95 for example, use 95% money to trade. str_type: 'amount', 'weight' or 'dropout' - strategy type: TopkAmountStrategy ,TopkWeightStrategy or TopkDropoutStrategy + strategy type: TopkAmountStrategy ,TopkWeightStrategy or TopkDropoutStrategy. - **exchange related arguments** exchange: Exchange() pass the exchange for speeding up. subscribe_fields: list - subscribe fields + subscribe fields. open_cost : float open transaction cost. The default value is 0.002(0.2%). close_cost : float close transaction cost. The default value is 0.002(0.2%). min_cost : float - min transaction cost + min transaction cost. trade_unit : int - 100 for China A + 100 for China A. deal_price: str - dealing price type: 'close', 'open', 'vwap' + dealing price type: 'close', 'open', 'vwap'. limit_threshold : float - limit move 0.1 (10%) for example, long and short with same limit + limit move 0.1 (10%) for example, long and short with same limit. extract_codes: bool will we pass the codes extracted from the pred to the exchange. @@ -291,17 +291,17 @@ def long_short_backtest( """ A backtest for long-short strategy - :param pred: The trading signal produced on day `T` - :param topk: The short topk securities and long topk securities - :param deal_price: The price to deal the trading + :param pred: The trading signal produced on day `T`. + :param topk: The short topk securities and long topk securities. + :param deal_price: The price to deal the trading. :param shift: Whether to shift prediction by one day. The trading day will be T+1 if shift==1. - :param open_cost: open transaction cost - :param close_cost: close transaction cost - :param trade_unit: 100 for China A - :param limit_threshold: limit move 0.1 (10%) for example, long and short with same limit - :param min_cost: min transaction cost - :param subscribe_fields: subscribe fields - :param extract_codes: bool + :param open_cost: open transaction cost. + :param close_cost: close transaction cost. + :param trade_unit: 100 for China A. + :param limit_threshold: limit move 0.1 (10%) for example, long and short with same limit. + :param min_cost: min transaction cost. + :param subscribe_fields: subscribe fields. + :param extract_codes: bool. will we pass the codes extracted from the pred to the exchange. NOTE: This will be faster with offline qlib. :return: The result of backtest, it is represented by a dict. diff --git a/qlib/contrib/report/analysis_model/analysis_model_performance.py b/qlib/contrib/report/analysis_model/analysis_model_performance.py index 1c69145db..1cb14d261 100644 --- a/qlib/contrib/report/analysis_model/analysis_model_performance.py +++ b/qlib/contrib/report/analysis_model/analysis_model_performance.py @@ -252,7 +252,7 @@ def model_performance_graph( """Model performance :param pred_label: index is **pd.MultiIndex**, index name is **[instrument, datetime]**; columns names is **[score, - label]**. It is usually same as the label of model training(e.g. "Ref($close, -2)/Ref($close, -1) - 1") + label]**. It is usually same as the label of model training(e.g. "Ref($close, -2)/Ref($close, -1) - 1"). .. code-block:: python @@ -266,13 +266,13 @@ def model_performance_graph( :param lag: `pred.groupby(level='instrument')['score'].shift(lag)`. It will be only used in the auto-correlation computing. - :param N: group number, default 5 - :param reverse: if `True`, `pred['score'] *= -1` - :param rank: if **True**, calculate rank ic - :param graph_names: graph names; default ['cumulative_return', 'pred_ic', 'pred_autocorr', 'pred_turnover'] - :param show_notebook: whether to display graphics in notebook, the default is `True` - :param show_nature_day: whether to display the abscissa of non-trading day - :return: if show_notebook is True, display in notebook; else return `plotly.graph_objs.Figure` list + :param N: group number, default 5. + :param reverse: if `True`, `pred['score'] *= -1`. + :param rank: if **True**, calculate rank ic. + :param graph_names: graph names; default ['cumulative_return', 'pred_ic', 'pred_autocorr', 'pred_turnover']. + :param show_notebook: whether to display graphics in notebook, the default is `True`. + :param show_nature_day: whether to display the abscissa of non-trading day. + :return: if show_notebook is True, display in notebook; else return `plotly.graph_objs.Figure` list. """ figure_list = [] for graph_name in graph_names: diff --git a/qlib/contrib/report/analysis_position/cumulative_return.py b/qlib/contrib/report/analysis_position/cumulative_return.py index 941785e83..abb68ea60 100644 --- a/qlib/contrib/report/analysis_position/cumulative_return.py +++ b/qlib/contrib/report/analysis_position/cumulative_return.py @@ -218,10 +218,10 @@ def cumulative_return_graph( Graph desc: - - Axis X: Trading day + - Axis X: Trading day. - Axis Y: - - Above axis Y: `(((Ref($close, -1)/$close - 1) * weight).sum() / weight.sum()).cumsum()` - - Below axis Y: Daily weight sum + - Above axis Y: `(((Ref($close, -1)/$close - 1) * weight).sum() / weight.sum()).cumsum()`. + - Below axis Y: Daily weight sum. - In the **sell** graph, `y < 0` stands for profit; in other cases, `y > 0` stands for profit. - In the **buy_minus_sell** graph, the **y** value of the **weight** graph at the bottom is `buy_weight + sell_weight`. - In each graph, the **red line** in the histogram on the right represents the average. diff --git a/qlib/contrib/report/analysis_position/rank_label.py b/qlib/contrib/report/analysis_position/rank_label.py index e2f7fe1cf..72a358adc 100644 --- a/qlib/contrib/report/analysis_position/rank_label.py +++ b/qlib/contrib/report/analysis_position/rank_label.py @@ -97,9 +97,9 @@ def rank_label_graph( qcr.rank_label_graph(positions, features_df, pred_df_dates.min(), pred_df_dates.max()) - :param position: position data; **qlib.contrib.backtest.backtest.backtest** result + :param position: position data; **qlib.contrib.backtest.backtest.backtest** result. :param label_data: **D.features** result; index is **pd.MultiIndex**, index name is **[instrument, datetime]**; columns names is **[label]**. - **The label T is the change from T to T+1**, it is recommended to use ``close``, example: `D.features(D.instruments('csi500'), ['Ref($close, -1)/$close-1'])` + **The label T is the change from T to T+1**, it is recommended to use ``close``, example: `D.features(D.instruments('csi500'), ['Ref($close, -1)/$close-1'])`. .. code-block:: python @@ -115,7 +115,7 @@ def rank_label_graph( :param start_date: start date :param end_date: end_date - :param show_notebook: **True** or **False**. If True, show graph in notebook, else return figures + :param show_notebook: **True** or **False**. If True, show graph in notebook, else return figures. :return: """ position = copy.deepcopy(position) diff --git a/qlib/contrib/report/analysis_position/report.py b/qlib/contrib/report/analysis_position/report.py index 6d108cabf..438aab8b9 100644 --- a/qlib/contrib/report/analysis_position/report.py +++ b/qlib/contrib/report/analysis_position/report.py @@ -186,7 +186,7 @@ def report_graph(report_df: pd.DataFrame, show_notebook: bool = True) -> [list, qcr.report_graph(report_normal_df) - :param report_df: **df.index.name** must be **date**, **df.columns** must contain **return**, **turnover**, **cost**, **bench** + :param report_df: **df.index.name** must be **date**, **df.columns** must contain **return**, **turnover**, **cost**, **bench**. .. code-block:: python @@ -200,8 +200,8 @@ def report_graph(report_df: pd.DataFrame, show_notebook: bool = True) -> [list, 2017-01-10 -0.000416 0.000440 -0.003350 0.208396 - :param show_notebook: whether to display graphics in notebook, the default is **True** - :return: if show_notebook is True, display in notebook; else return **plotly.graph_objs.Figure** list + :param show_notebook: whether to display graphics in notebook, the default is **True**. + :return: if show_notebook is True, display in notebook; else return **plotly.graph_objs.Figure** list. """ report_df = report_df.copy() fig_list = _report_figure(report_df) diff --git a/qlib/contrib/report/analysis_position/risk_analysis.py b/qlib/contrib/report/analysis_position/risk_analysis.py index 124a9b3b0..051c78035 100644 --- a/qlib/contrib/report/analysis_position/risk_analysis.py +++ b/qlib/contrib/report/analysis_position/risk_analysis.py @@ -218,7 +218,7 @@ def risk_analysis_graph( max_drawdown -0.088263 - :param report_normal_df: **df.index.name** must be **date**, df.columns must contain **return**, **turnover**, **cost**, **bench** + :param report_normal_df: **df.index.name** must be **date**, df.columns must contain **return**, **turnover**, **cost**, **bench**. .. code-block:: python @@ -232,7 +232,7 @@ def risk_analysis_graph( 2017-01-10 -0.000416 0.000440 -0.003350 0.208396 - :param report_long_short_df: **df.index.name** must be **date**, df.columns contain **long**, **short**, **long_short** + :param report_long_short_df: **df.index.name** must be **date**, df.columns contain **long**, **short**, **long_short**. .. code-block:: python @@ -246,7 +246,7 @@ def risk_analysis_graph( 2017-01-10 0.000824 -0.001944 -0.001120 - :param show_notebook: Whether to display graphics in a notebook, default **True** + :param show_notebook: Whether to display graphics in a notebook, default **True**. If True, show graph in notebook If False, return graph figure :return: diff --git a/qlib/contrib/report/analysis_position/score_ic.py b/qlib/contrib/report/analysis_position/score_ic.py index 9a2fc8560..a6a7a8b0e 100644 --- a/qlib/contrib/report/analysis_position/score_ic.py +++ b/qlib/contrib/report/analysis_position/score_ic.py @@ -36,7 +36,7 @@ def score_ic_graph(pred_label: pd.DataFrame, show_notebook: bool = True) -> [lis analysis_position.score_ic_graph(pred_label) - :param pred_label: index is **pd.MultiIndex**, index name is **[instrument, datetime]**; columns names is **[score, label]** + :param pred_label: index is **pd.MultiIndex**, index name is **[instrument, datetime]**; columns names is **[score, label]**. .. code-block:: python @@ -49,8 +49,8 @@ def score_ic_graph(pred_label: pd.DataFrame, show_notebook: bool = True) -> [lis 2017-12-15 -0.102778 -0.102778 - :param show_notebook: whether to display graphics in notebook, the default is **True** - :return: if show_notebook is True, display in notebook; else return **plotly.graph_objs.Figure** list + :param show_notebook: whether to display graphics in notebook, the default is **True**. + :return: if show_notebook is True, display in notebook; else return **plotly.graph_objs.Figure** list. """ _ic_df = _get_score_ic(pred_label) # FIXME: support HIGH-FREQ diff --git a/qlib/contrib/strategy/strategy.py b/qlib/contrib/strategy/strategy.py index f2e2a4554..23e8b5185 100644 --- a/qlib/contrib/strategy/strategy.py +++ b/qlib/contrib/strategy/strategy.py @@ -31,16 +31,16 @@ class BaseStrategy: Parameters ----------- score_series : pd.Seires - stock_id , score + stock_id , score. current : Position() - current state of position - DO NOT directly change the state of current + current state of position. + DO NOT directly change the state of current. trade_exchange : Exchange() - trade exchange + trade exchange. pred_date : pd.Timestamp - predict date + predict date. trade_date : pd.Timestamp - trade date + trade date. """ pass @@ -49,11 +49,11 @@ class BaseStrategy: Parameters ----------- score_series : pd.Series - stock_id , score + stock_id , score. pred_date : pd.Timestamp - oredict date + oredict date. trade_date : pd.Timestamp - trade date + trade date. """ pass @@ -67,7 +67,7 @@ class BaseStrategy: """ This method only be used in 'online' module, it will generate the *args to initial the strategy. :param - mode : model used in 'online' module + mode : model used in 'online' module. """ return {} @@ -82,7 +82,7 @@ class StrategyWrapper: def __init__(self, inner_strategy): """__init__ - :param inner_strategy: set the inner strategy + :param inner_strategy: set the inner strategy. """ self.inner_strategy = inner_strategy @@ -99,9 +99,9 @@ class AdjustTimer: Responsible for timing of position adjusting This is designed as multiple inheritance mechanism due to: - - the is_adjust may need access to the internel state of a strategy + - the is_adjust may need access to the internel state of a strategy. - - it can be reguard as a enhancement to the existing strategy + - it can be reguard as a enhancement to the existing strategy. """ # adjust position in each trade date @@ -146,12 +146,12 @@ class WeightStrategyBase(BaseStrategy, AdjustTimer): Parameters ----------- score : pd.Series - pred score for this trade date, index is stock_id, contain 'score' column + pred score for this trade date, index is stock_id, contain 'score' column. current : Position() - current position + current position. trade_exchange : Exchange() trade_date : pd.Timestamp - trade date + trade date. """ raise NotImplementedError() @@ -160,13 +160,13 @@ class WeightStrategyBase(BaseStrategy, AdjustTimer): Parameters ----------- score_series : pd.Seires - stock_id , score + stock_id , score. current : Position() - current of account + current of account. trade_exchange : Exchange() - exchange + exchange. trade_date : pd.Timestamp - date + date. """ # judge if to adjust if not self.is_adjust(trade_date): @@ -206,26 +206,26 @@ class TopkDropoutStrategy(BaseStrategy, ListAdjustTimer): Parameters ----------- topk : int - The number of stocks in the portfolio + the number of stocks in the portfolio. n_drop : int - number of stocks to be replaced in each trading date + number of stocks to be replaced in each trading date. method_sell : str - dropout method_sell, random/bottom + dropout method_sell, random/bottom. method_buy : str - dropout method_buy, random/top + dropout method_buy, random/top. risk_degree : float - position percentage of total value + position percentage of total value. thresh : int - minimun holding days since last buy singal of the stock + minimun holding days since last buy singal of the stock. hold_thresh : int minimum holding days - before sell stock , will check current.get_stock_count(order.stock_id) >= self.thresh + before sell stock , will check current.get_stock_count(order.stock_id) >= self.thresh. only_tradable : bool will the strategy only consider the tradable stock when buying and selling. if only_tradable: - strategy will make buy sell decision without checking the tradable state of the stock + strategy will make buy sell decision without checking the tradable state of the stock. else: - strategy will make decision with the tradable state of the stock info and avoid buy and sell them + strategy will make decision with the tradable state of the stock info and avoid buy and sell them. """ super(TopkDropoutStrategy, self).__init__() ListAdjustTimer.__init__(self, kwargs.get("adjust_dates", None)) @@ -245,7 +245,7 @@ class TopkDropoutStrategy(BaseStrategy, ListAdjustTimer): def get_risk_degree(self, date): """get_risk_degree Return the proportion of your total value you will used in investment. - Dynamically risk_degree will result in Market timing + Dynamically risk_degree will result in Market timing. """ # It will use 95% amoutn of your total value by default return self.risk_degree @@ -257,15 +257,15 @@ class TopkDropoutStrategy(BaseStrategy, ListAdjustTimer): Parameters ----------- score_series : pd.Series - stock_id , score + stock_id , score. current : Position() - current of account + current of account. trade_exchange : Exchange() - exchange + exchange. pred_date : pd.Timestamp - predict date + predict date. trade_date : pd.Timestamp - trade date + trade date. """ if not self.is_adjust(trade_date): return [] diff --git a/qlib/data/base.py b/qlib/data/base.py index 433b6585a..92fc57ffe 100644 --- a/qlib/data/base.py +++ b/qlib/data/base.py @@ -129,13 +129,13 @@ class Expression(abc.ABC): Parameters ---------- instrument : str - instrument code + instrument code. start_index : str - feature start index [in calendar] + feature start index [in calendar]. end_index : str - feature end index [in calendar] + feature end index [in calendar]. freq : str - feature frequency + feature frequency. Returns ---------- diff --git a/qlib/data/cache.py b/qlib/data/cache.py index bf8baab31..3fab2b527 100644 --- a/qlib/data/cache.py +++ b/qlib/data/cache.py @@ -76,8 +76,8 @@ class MemCache(object): Parameters ---------- - mem_cache_size_limit: cache max size - limit_type: length or sizeof; length(call fun: len), size(call fun: sys.getsizeof) + mem_cache_size_limit: cache max size. + limit_type: length or sizeof; length(call fun: len), size(call fun: sys.getsizeof). """ if limit_type not in ["length", "sizeof"]: raise ValueError(f"limit_type must be length or sizeof, your limit_type is {limit_type}") @@ -118,9 +118,9 @@ class MemCacheExpire: def set_cache(mem_cache, key, value): """set cache - :param mem_cache: MemCache attribute('c'/'i'/'f') - :param key: cache key - :param value: cache value + :param mem_cache: MemCache attribute('c'/'i'/'f'). + :param key: cache key. + :param value: cache value. """ mem_cache[key] = value, time.time() @@ -128,9 +128,9 @@ class MemCacheExpire: def get_cache(mem_cache, key): """get mem cache - :param mem_cache: MemCache attribute('c'/'i'/'f') - :param key: cache key - :return: cache value; if cache not exist, return None + :param mem_cache: MemCache attribute('c'/'i'/'f'). + :param key: cache key. + :return: cache value; if cache not exist, return None. """ value = None expire = False @@ -275,12 +275,12 @@ class ExpressionCache(BaseProviderCache): Parameters ---------- cache_uri : str - the complete uri of expression cache file (include dir path) + the complete uri of expression cache file (include dir path). Returns ------- int - 0(successful update)/ 1(no need to update)/ 2(update failure) + 0(successful update)/ 1(no need to update)/ 2(update failure). """ raise NotImplementedError("Implement this method if you want to make expression cache up to date") @@ -348,7 +348,7 @@ class DatasetCache(BaseProviderCache): Parameters ---------- cache_uri : str - the complete uri of dataset cache file (include dir path) + the complete uri of dataset cache file (include dir path). Returns ------- @@ -361,9 +361,9 @@ class DatasetCache(BaseProviderCache): def cache_to_origin_data(data, fields): """cache data to origin data - :param data: pd.DataFrame, cache data - :param fields: feature fields - :return: pd.DataFrame + :param data: pd.DataFrame, cache data. + :param fields: feature fields. + :return: pd.DataFrame. """ not_space_fields = remove_fields_space(fields) data = data.loc[:, not_space_fields] @@ -583,7 +583,7 @@ class DiskDatasetCache(DatasetCache): :param cache_path: :param start_time: :param end_time: - :param fields: The fields order of the dataset cache is sorted. So rearrange the columns to make it consistent + :param fields: The fields order of the dataset cache is sorted. So rearrange the columns to make it consistent. :return: """ @@ -771,12 +771,12 @@ class DiskDatasetCache(DatasetCache): - This is a hdf file sorted by datetime - :param cache_path: The path to store the cache - :param instruments: The instruments to store the cache - :param fields: The fields to store the cache - :param freq: The freq to store the cache + :param cache_path: The path to store the cache. + :param instruments: The instruments to store the cache. + :param fields: The fields to store the cache. + :param freq: The freq to store the cache. - :return type pd.DataFrame; The fields of the returned DataFrame are consistent with the parameters of the function + :return type pd.DataFrame; The fields of the returned DataFrame are consistent with the parameters of the function. """ # get calendar from .data import Cal diff --git a/qlib/data/client.py b/qlib/data/client.py index 928faaa72..65a830f20 100644 --- a/qlib/data/client.py +++ b/qlib/data/client.py @@ -51,13 +51,13 @@ class Client(object): Parameters ---------- request_type : str - type of proposed request, 'calendar'/'instrument'/'feature' + type of proposed request, 'calendar'/'instrument'/'feature'. request_content : dict - records the information of the request + records the information of the request. msg_proc_func : func - the function to process the message when receiving response, should have arg `*args` + the function to process the message when receiving response, should have arg `*args`. msg_queue: Queue - The queue to pass the messsage after callback + The queue to pass the messsage after callback. """ head_info = {"version": qlib.__version__} diff --git a/qlib/data/data.py b/qlib/data/data.py index ef5e7fe8a..a4c3d63f2 100644 --- a/qlib/data/data.py +++ b/qlib/data/data.py @@ -41,13 +41,13 @@ class CalendarProvider(abc.ABC): Parameters ---------- start_time : str - start of the time range + start of the time range. end_time : str - end of the time range + end of the time range. freq : str - time frequency, available: year/quarter/month/week/day + time frequency, available: year/quarter/month/week/day. future : bool - whether including future trading day + whether including future trading day. Returns ---------- @@ -62,24 +62,24 @@ class CalendarProvider(abc.ABC): Parameters ---------- start_time : str - start of the time range + start of the time range. end_time : str - end of the time range + end of the time range. freq : str - time frequency, available: year/quarter/month/week/day + time frequency, available: year/quarter/month/week/day. future : bool - whether including future trading day + whether including future trading day. Returns ------- pd.Timestamp - the real start time + the real start time. pd.Timestamp - the real end time + the real end time. int - the index of start time + the index of start time. int - the index of end time + the index of end time. """ start_time = pd.Timestamp(start_time) end_time = pd.Timestamp(end_time) @@ -103,16 +103,16 @@ class CalendarProvider(abc.ABC): Parameters ---------- freq : str - frequency of read calendar file + frequency of read calendar file. future : bool - whether including future trading day + whether including future trading day. Returns ------- list - list of timestamps + list of timestamps. dict - dict composed by timestamp as key and index as value for fast search + dict composed by timestamp as key and index as value for fast search. """ flag = f"{freq}_future_{future}" if flag in H["c"]: @@ -141,14 +141,14 @@ class InstrumentProvider(abc.ABC): Parameters ---------- market : str - market/industry/index shortname, e.g. all/sse/szse/sse50/csi300/csi500 + market/industry/index shortname, e.g. all/sse/szse/sse50/csi300/csi500. filter_pipe : list - the list of dynamic filters + the list of dynamic filters. Returns ---------- dict - dict of stockpool config + dict of stockpool config. {`market`=>base market name, `filter_pipe`=>list of filters} example : @@ -182,13 +182,13 @@ class InstrumentProvider(abc.ABC): Parameters ---------- instruments : dict - stockpool config + stockpool config. start_time : str - start of the time range + start of the time range. end_time : str - end of the time range + end of the time range. as_list : bool - return instruments as list or dict + return instruments as list or dict. Returns ------- @@ -243,15 +243,15 @@ class FeatureProvider(abc.ABC): Parameters ---------- instrument : str - a certain instrument + a certain instrument. field : str - a certain field of feature + a certain field of feature. start_time : str - start of the time range + start of the time range. end_time : str - end of the time range + end of the time range. freq : str - time frequency, available: year/quarter/month/week/day + time frequency, available: year/quarter/month/week/day. Returns ------- @@ -294,15 +294,15 @@ class ExpressionProvider(abc.ABC): Parameters ---------- instrument : str - a certain instrument + a certain instrument. field : str - a certain field of feature + a certain field of feature. start_time : str - start of the time range + start of the time range. end_time : str - end of the time range + end of the time range. freq : str - time frequency, available: year/quarter/month/week/day + time frequency, available: year/quarter/month/week/day. Returns ------- @@ -325,20 +325,20 @@ class DatasetProvider(abc.ABC): Parameters ---------- instruments : list or dict - list/dict of instruments or dict of stockpool config + list/dict of instruments or dict of stockpool config. fields : list - list of feature instances + list of feature instances. start_time : str - start of the time range + start of the time range. end_time : str - end of the time range + end of the time range. freq : str - time frequency + time frequency. Returns ---------- pd.DataFrame - a pandas dataframe with index + a pandas dataframe with index. """ raise NotImplementedError("Subclass of DatasetProvider must implement `Dataset` method") @@ -357,17 +357,17 @@ class DatasetProvider(abc.ABC): Parameters ---------- instruments : list or dict - list/dict of instruments or dict of stockpool config + list/dict of instruments or dict of stockpool config. fields : list - list of feature instances + list of feature instances. start_time : str - start of the time range + start of the time range. end_time : str - end of the time range + end of the time range. freq : str - time frequency + time frequency. disk_cache : int - whether to skip(0)/use(1)/replace(2) disk_cache + whether to skip(0)/use(1)/replace(2) disk_cache. """ return DiskDatasetCache._uri(instruments, fields, start_time, end_time, freq, disk_cache) @@ -526,7 +526,7 @@ class LocalCalendarProvider(CalendarProvider): Parameters ---------- freq : str - frequency of read calendar file + frequency of read calendar file. Returns ---------- diff --git a/qlib/data/dataset/__init__.py b/qlib/data/dataset/__init__.py index e972aba3c..74e14f47a 100644 --- a/qlib/data/dataset/__init__.py +++ b/qlib/data/dataset/__init__.py @@ -17,7 +17,7 @@ class Dataset(Serializable): init is designed to finish following steps: - setup data - - The data related attributes' names should start with '_' so that it will not be saved on disk when serializing + - The data related attributes' names should start with '_' so that it will not be saved on disk when serializing. - initialize the state of the dataset(info to prepare the data) - The name of essential state for preparing data should not start with '_' so that it could be serialized on disk when serializing. @@ -29,17 +29,17 @@ class Dataset(Serializable): def setup_data(self, *args, **kwargs): """ - setup the data + Setup the data. We split the setup_data function for following situation: - - User have a Dataset object with learned status on disk + - User have a Dataset object with learned status on disk. - - User load the Dataset object from the disk(Note the init function is skiped) + - User load the Dataset object from the disk(Note the init function is skiped). - - User call `setup_data` to load new data + - User call `setup_data` to load new data. - - User prepare data for model based on previous status + - User prepare data for model based on previous status. """ pass @@ -66,9 +66,10 @@ class DatasetH(Dataset): User should try to put the data preprocessing functions into handler. Only following data processing functions should be placed in Dataset: + - The processing is related to specific model. - - The processing is related to data split + - The processing is related to data split. """ def __init__(self, handler: Union[dict, DataHandler], segments: list): @@ -76,15 +77,15 @@ class DatasetH(Dataset): Parameters ---------- handler : Union[dict, DataHandler] - handler will be passed into setup_data + handler will be passed into setup_data. segments : list - handler will be passed into setup_data + handler will be passed into setup_data. """ super().__init__(handler, segments) def setup_data(self, handler: Union[dict, DataHandler], segments: list): """ - setup the underlying data + Setup the underlying data. Parameters ---------- @@ -121,7 +122,7 @@ class DatasetH(Dataset): **kwargs, ) -> Union[List[pd.DataFrame], pd.DataFrame]: """ - prepare the data for learning and inference + Prepare the data for learning and inference. Parameters ---------- @@ -132,11 +133,12 @@ class DatasetH(Dataset): - 'train' - ['train', 'valid'] + col_set : str - The col_set will be passed to self._handler when fetching data - data_key: str + The col_set will be passed to self._handler when fetching data. + data_key : str The data to fetch: DK_* - Default is DK_I, which indicate fetching data for **inference** + Default is DK_I, which indicate fetching data for **inference**. Returns ------- diff --git a/qlib/data/dataset/handler.py b/qlib/data/dataset/handler.py index 4d3d88c38..1710ff9e3 100644 --- a/qlib/data/dataset/handler.py +++ b/qlib/data/dataset/handler.py @@ -29,7 +29,7 @@ class DataHandler(Serializable): """ The steps to using a handler 1. initialized data handler (call by `init`). - 2. use the data + 2. use the data. The data handler try to maintain a handler with 2 level. @@ -65,17 +65,17 @@ class DataHandler(Serializable): Parameters ---------- instruments : - The stock list to retrive + The stock list to retrive. start_time : - start_time of the original data + start_time of the original data. end_time : - end_time of the original data + end_time of the original data. data_loader : Tuple[dict, str, DataLoader] - data loader to load the data + data loader to load the data. init_data : - intialize the original data in the constructor + intialize the original data in the constructor. fetch_orig : bool - Return the original data instead of copy if possible + Return the original data instead of copy if possible. """ # Set logger self.logger = get_module_logger("DataHandler") @@ -219,9 +219,9 @@ class DataHandler(Serializable): get a iterator of sliced data with given periods Args: - periods (int): number of periods - min_periods (int): minimum periods for sliced dataframe - kwargs (dict): will be passed to `self.fetch` + periods (int): number of periods. + min_periods (int): minimum periods for sliced dataframe. + kwargs (dict): will be passed to `self.fetch`. """ trading_dates = self._data.index.unique(level="datetime") if min_periods is None: @@ -377,7 +377,7 @@ class DataHandlerLP(DataHandler): Parameters ---------- init_type : str - The type `IT_*` listed above + The type `IT_*` listed above. enable_cache : bool default value is false: @@ -419,13 +419,13 @@ class DataHandlerLP(DataHandler): Parameters ---------- selector : Union[pd.Timestamp, slice, str] - describe how to select data by index + describe how to select data by index. level : Union[str, int] - which index level to select the data + which index level to select the data. col_set : str - select a set of meaningful columns.(e.g. features, columns) - data_key: str - The data to fetch: DK_* + select a set of meaningful columns.(e.g. features, columns). + data_key : str + the data to fetch: DK_*. Returns ------- @@ -443,9 +443,9 @@ class DataHandlerLP(DataHandler): Parameters ---------- col_set : str - select a set of meaningful columns.(e.g. features, columns) - data_key: str - The data to fetch: DK_* + select a set of meaningful columns.(e.g. features, columns). + data_key : str + the data to fetch: DK_*. Returns ------- diff --git a/qlib/data/dataset/loader.py b/qlib/data/dataset/loader.py index db6b1440d..d1de4821c 100644 --- a/qlib/data/dataset/loader.py +++ b/qlib/data/dataset/loader.py @@ -100,16 +100,16 @@ class DLWParser(DataLoader): Parameters ---------- instruments : - the instruments + the instruments. exprs : list - The expressions to describe the content of the data + the expressions to describe the content of the data. names : list - The name of the data + the name of the data. Returns ------- pd.DataFrame: - the queried dataframe + the queried dataframe. """ pass diff --git a/qlib/data/dataset/processor.py b/qlib/data/dataset/processor.py index 2201c0891..e4003a1f5 100755 --- a/qlib/data/dataset/processor.py +++ b/qlib/data/dataset/processor.py @@ -21,7 +21,7 @@ def get_group_columns(df: pd.DataFrame, group: str): Parameters ---------- df : pd.DataFrame - with multi of columns + with multi of columns. group : str the name of the feature group, i.e. the first level value of the group index. """ @@ -56,7 +56,7 @@ class Processor(Serializable): Parameters ---------- df : pd.DataFrame - The raw_df of handler or result from previous processor + The raw_df of handler or result from previous processor. """ pass @@ -68,7 +68,7 @@ class Processor(Serializable): Returns ------- bool: - if it is usable for infenrece + if it is usable for infenrece. """ return True diff --git a/qlib/data/filter.py b/qlib/data/filter.py index 47b093b67..70f9d3278 100644 --- a/qlib/data/filter.py +++ b/qlib/data/filter.py @@ -32,7 +32,7 @@ class BaseDFilter(abc.ABC): Parameters ---------- config : dict - dict of config parameters + dict of config parameters. """ raise NotImplementedError("Subclass of BaseDFilter must reimplement `from_config` method") @@ -43,7 +43,7 @@ class BaseDFilter(abc.ABC): Returns ---------- dict - return the dict of config parameters + return the dict of config parameters. """ raise NotImplementedError("Subclass of BaseDFilter must reimplement `to_config` method") @@ -69,9 +69,9 @@ class SeriesDFilter(BaseDFilter): Parameters ---------- fstart_time: str - the time for the filter rule to start filter the instruments + the time for the filter rule to start filter the instruments. fend_time: str - the time for the filter rule to stop filter the instruments + the time for the filter rule to stop filter the instruments. """ super(SeriesDFilter, self).__init__() self.filter_start_time = pd.Timestamp(fstart_time) if fstart_time else None @@ -83,12 +83,12 @@ class SeriesDFilter(BaseDFilter): Parameters ---------- instruments: dict - the dict of instruments in the form {instrument_name => list of timestamp tuple} + the dict of instruments in the form {instrument_name => list of timestamp tuple}. Returns ---------- pd.Timestamp, pd.Timestamp - the lower time bound and upper time bound of all the instruments + the lower time bound and upper time bound of all the instruments. """ trange = Cal.calendar(freq=self.filter_freq) ubound, lbound = trange[0], trange[-1] @@ -105,14 +105,14 @@ class SeriesDFilter(BaseDFilter): Parameters ---------- time_range : D.calendar - the time range of the instruments + the time range of the instruments. target_timestamp : list - the list of tuple (timestamp, timestamp) + the list of tuple (timestamp, timestamp). Returns ---------- pd.Series - the series of bool value for an instrument + the series of bool value for an instrument. """ # Construct a whole dict of {date => bool} timestamp_series = {timestamp: False for timestamp in time_range} @@ -124,19 +124,19 @@ class SeriesDFilter(BaseDFilter): return timestamp_series def _filterSeries(self, timestamp_series, filter_series): - """Filter the timestamp series with filter series by using element-wise AND operation of the two series + """Filter the timestamp series with filter series by using element-wise AND operation of the two series. Parameters ---------- timestamp_series : pd.Series - the series of bool value indicating existing time + the series of bool value indicating existing time. filter_series : pd.Series - the series of bool value indicating filter feature + the series of bool value indicating filter feature. Returns ---------- pd.Series - the series of bool value indicating whether the date satisfies the filter condition and exists in target timestamp + the series of bool value indicating whether the date satisfies the filter condition and exists in target timestamp. """ fstart, fend = list(filter_series.keys())[0], list(filter_series.keys())[-1] filter_series = filter_series.astype("bool") # Make sure the filter_series is boolean @@ -144,17 +144,17 @@ class SeriesDFilter(BaseDFilter): return timestamp_series def _toTimestamp(self, timestamp_series): - """Convert the timestamp series to a list of tuple (timestamp, timestamp) indicating a continuous range of TRUE + """Convert the timestamp series to a list of tuple (timestamp, timestamp) indicating a continuous range of TRUE. Parameters ---------- timestamp_series: pd.Series - the series of bool value after being filtered + the series of bool value after being filtered. Returns ---------- list - the list of tuple (timestamp, timestamp) + the list of tuple (timestamp, timestamp). """ # sort the timestamp_series according to the timestamps timestamp_series.sort_index() @@ -194,18 +194,18 @@ class SeriesDFilter(BaseDFilter): Parameters ---------- instruments : dict - the dict of instruments to be filtered + the dict of instruments to be filtered. fstart : pd.Timestamp - start time of filter + start time of filter. fend : pd.Timestamp - end time of filter + end time of filter. - .. note:: fstart/fend indicates the intersection of instruments start/end time and filter start/end time + .. note:: fstart/fend indicates the intersection of instruments start/end time and filter start/end time. Returns ---------- pd.Dataframe - a series of {pd.Timestamp => bool} + a series of {pd.Timestamp => bool}. """ raise NotImplementedError("Subclass of SeriesDFilter must reimplement `getFilterSeries` method") @@ -215,16 +215,16 @@ class SeriesDFilter(BaseDFilter): Parameters ---------- instruments: dict - input instruments to be filtered + input instruments to be filtered. start_time: str - start of the time range + start of the time range. end_time: str - end of the time range + end of the time range. Returns ---------- dict - filtered instruments, same structure as input instruments + filtered instruments, same structure as input instruments. """ lbound, ubound = self._getTimeBound(instruments) start_time = pd.Timestamp(start_time or lbound) @@ -272,7 +272,7 @@ class NameDFilter(SeriesDFilter): params: ------ name_rule_re: str - regular expression for the name rule + regular expression for the name rule. """ super(NameDFilter, self).__init__(fstart_time, fend_time) self.name_rule_re = name_rule_re @@ -325,13 +325,13 @@ class ExpressionDFilter(SeriesDFilter): params: ------ fstart_time: str - filter the feature starting from this time + filter the feature starting from this time. fend_time: str - filter the feature ending by this time + filter the feature ending by this time. rule_expression: str - an input expression for the rule + an input expression for the rule. keep: bool - whether to keep the instruments of which features don't exist in the filter time span + whether to keep the instruments of which features don't exist in the filter time span. """ super(ExpressionDFilter, self).__init__(fstart_time, fend_time) self.rule_expression = rule_expression diff --git a/qlib/model/base.py b/qlib/model/base.py index d6ee50e33..fd220cd7e 100644 --- a/qlib/model/base.py +++ b/qlib/model/base.py @@ -33,7 +33,7 @@ class Model(BaseModel): Parameters ---------- dataset : Dataset - dataset will generate the processed data from model training + dataset will generate the processed data from model training. """ raise NotImplementedError() @@ -44,7 +44,7 @@ class Model(BaseModel): Parameters ---------- dataset : Dataset - dataset will generate the processed dataset from model training + dataset will generate the processed dataset from model training. """ raise NotImplementedError() @@ -59,6 +59,6 @@ class ModelFT(Model): Parameters ---------- dataset : Dataset - dataset will generate the processed dataset from model training + dataset will generate the processed dataset from model training. """ raise NotImplementedError() diff --git a/qlib/model/riskmodel.py b/qlib/model/riskmodel.py index b5275213b..07a1e0c9f 100644 --- a/qlib/model/riskmodel.py +++ b/qlib/model/riskmodel.py @@ -23,9 +23,9 @@ class RiskModel(BaseModel): def __init__(self, nan_option: str = "ignore", assume_centered: bool = False, scale_return: bool = True): """ Args: - nan_option (str): nan handling option (`ignore`/`mask`/`fill`) - assume_centered (bool): whether the data is assumed to be centered - scale_return (bool): whether scale returns as percentage + nan_option (str): nan handling option (`ignore`/`mask`/`fill`). + assume_centered (bool): whether the data is assumed to be centered. + scale_return (bool): whether scale returns as percentage. """ # nan assert nan_option in [ @@ -45,11 +45,11 @@ class RiskModel(BaseModel): Args: X (pd.Series, pd.DataFrame or np.ndarray): data from which to estimate the covariance, with variables as columns and observations as rows. - return_corr (bool): whether return the correlation matrix - is_price (bool): whether `X` contains price (if not assume stock returns) + return_corr (bool): whether return the correlation matrix. + is_price (bool): whether `X` contains price (if not assume stock returns). Returns: - pd.DataFrame or np.ndarray: estimated covariance (or correlation) + pd.DataFrame or np.ndarray: estimated covariance (or correlation). """ # transform input into 2D array if not isinstance(X, (pd.Series, pd.DataFrame)): @@ -101,10 +101,10 @@ class RiskModel(BaseModel): By default, this method implements the empirical covariance estimation. Args: - X (np.ndarray): data matrix containing multiple variables (columns) and observations (rows) + X (np.ndarray): data matrix containing multiple variables (columns) and observations (rows). Returns: - np.ndarray: covariance matrix + np.ndarray: covariance matrix. """ xTx = np.asarray(X.T.dot(X)) N = len(X) @@ -117,7 +117,7 @@ class RiskModel(BaseModel): """handle nan and centerize data Note: - if `nan_option='mask'` then the returned array will be `np.ma.MaskedArray` + if `nan_option='mask'` then the returned array will be `np.ma.MaskedArray`. """ # handle nan if self.nan_option == self.FILL_NAN: @@ -139,15 +139,15 @@ class ShrinkCovEstimator(RiskModel): where `alpha` is the shrink parameter and `F` is the shrinking target. The following shrinking parameters (`alpha`) are supported: - - `lw` [1][2][3]: use Ledoit-Wolf shrinking parameter - - `oas` [4]: use Oracle Approximating Shrinkage shrinking parameter - - float: directly specify the shrink parameter, should be between [0, 1] + - `lw` [1][2][3]: use Ledoit-Wolf shrinking parameter. + - `oas` [4]: use Oracle Approximating Shrinkage shrinking parameter. + - float: directly specify the shrink parameter, should be between [0, 1]. The following shrinking targets (`F`) are supported: - - `const_var` [1][4][5]: assume stocks have the same constant variance and zero correlation - - `const_corr` [2][6]: assume stocks have different variance but equal correlation - - `single_factor` [3][7]: assume single factor model as the shrinking target - - np.ndarray: provide the shrinking targets directly + - `const_var` [1][4][5]: assume stocks have the same constant variance and zero correlation. + - `const_corr` [2][6]: assume stocks have different variance but equal correlation. + - `single_factor` [3][7]: assume single factor model as the shrinking target. + - np.ndarray: provide the shrinking targets directly. Note: - The optimal shrinking parameter depends on the selection of the shrinking target. @@ -402,13 +402,13 @@ class POETCovEstimator(RiskModel): def __init__(self, num_factors: int = 0, thresh: float = 1.0, thresh_method: str = "soft", **kwargs): """ Args: - num_factors (int): number of factors (if set to zero, no factor model will be used) - thresh (float): the positive constant for thresholding + num_factors (int): number of factors (if set to zero, no factor model will be used). + thresh (float): the positive constant for thresholding. thresh_method (str): thresholding method, which can be - - 'soft': soft thresholding - - 'hard': hard thresholding - - 'scad': scad thresholding - kwargs: see `RiskModel` for more information + - 'soft': soft thresholding. + - 'hard': hard thresholding. + - 'scad': scad thresholding. + kwargs: see `RiskModel` for more information. """ super().__init__(**kwargs) From f185f48185bb89290ae68b186f826afbb941f098 Mon Sep 17 00:00:00 2001 From: Jactus Date: Thu, 26 Nov 2020 00:59:48 +0800 Subject: [PATCH 151/241] Update docs --- docs/component/data.rst | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/docs/component/data.rst b/docs/component/data.rst index e14caff3e..efcd81ffd 100644 --- a/docs/component/data.rst +++ b/docs/component/data.rst @@ -297,23 +297,8 @@ The ``Processor`` module in ``Qlib`` is designed to be learnable and it is respo Users can also create their own `processor` by inheriting the base class of ``Processor``. Please refer to the implementation of all the processors for more information (`Processor Link `_). -API ---------- - To know more about ``Processor``, please refer to `Processor API <../reference/api.html#module-qlib.data.dataset.processor>`_. - -Usage --------------- - -``Data Handler`` can be used as a single module, which provides the following mehtods: - -- `get_split_data` - - According to the start and end dates, return features and labels of the pandas DataFrame type used for the 'Model' - -- `get_rolling_data` - - According to the start and end dates, and `rolling_period`, an iterator is returned, which can be used to traverse the features and labels used for rolling. - Example -------------- From 21eb86e5cb2df4df95d36b75f8ed8931c953baa3 Mon Sep 17 00:00:00 2001 From: Jactus Date: Thu, 26 Nov 2020 11:54:06 +0800 Subject: [PATCH 152/241] Update run_all_model --- examples/benchmarks/TFT/README.md | 8 ++-- examples/run_all_model.py | 67 ++++++++++++++++++++++++++++--- 2 files changed, 67 insertions(+), 8 deletions(-) diff --git a/examples/benchmarks/TFT/README.md b/examples/benchmarks/TFT/README.md index a64ca0129..e9e44db1a 100644 --- a/examples/benchmarks/TFT/README.md +++ b/examples/benchmarks/TFT/README.md @@ -5,8 +5,10 @@ **GitHub**: https://github.com/google-research/google-research/tree/master/tft ## Run the Workflow -Users can follow the ``workflow_by_code_tft.py`` to run the benchmark. Please be **aware** that this script can only support Python 3.5 - 3.8. +Users can follow the ``workflow_by_code_tft.py`` to run the benchmark. ### Notes -1. The model must run in GPU, or an error will be raised. -2. New datasets should be registered in ``data_formatters``, for detail please visit the source. +1. Please be **aware** that this script can only support `Python 3.5 - 3.8`, and `Cuda 10.0 or 10.1`. +2. Please remember to install `cudatoolkit==10.1` and `cudnn==7.6` on your machine. +3. The model must run in GPU, or an error will be raised. +4. New datasets should be registered in ``data_formatters``, for detail please visit the source. diff --git a/examples/run_all_model.py b/examples/run_all_model.py index b448a1857..6f12434da 100644 --- a/examples/run_all_model.py +++ b/examples/run_all_model.py @@ -10,6 +10,7 @@ import shutil import tempfile import statistics from pathlib import Path +from operator import xor from subprocess import Popen, PIPE from threading import Thread from pprint import pprint @@ -161,6 +162,19 @@ class ExtendedEnvBuilder(venv.EnvBuilder): self.install_script(context, "pip", url) +# function to check cuda version on the machine, this case is for the model TFT +def check_cuda(folders): + path = "/usr/local/cuda/version.txt" + exclude_tft = True + if os.path.exists(path): + with open(path, "w") as f: + if "10.1" in str(f.read()) or "10.0" in str(f.read()): + exclude_tft = False + if exclude_tft and "TFT" in folders: + del folders["TFT"] + return folders + + # function to calculate the mean and std of a list in the results dictionary def cal_mean_std(results) -> dict: mean_std = dict() @@ -174,11 +188,23 @@ def cal_mean_std(results) -> dict: # function to get all the folders benchmark folder -def get_all_folders() -> dict: +def get_all_folders(models, exclude) -> dict: folders = dict() + if isinstance(models, str): + model_list = models.split(",") + models = [m.lower().strip("[ ]") for m in model_list] + elif isinstance(models, list): + models = [m.lower() for m in models] + elif models is None: + models = [f.name.lower() for f in os.scandir("benchmarks")] + else: + raise ValueError("Input models type is not supported. Please provide str or list without space.") for f in os.scandir("benchmarks"): - path = Path("benchmarks") / f.name - folders[f.name] = str(path.resolve()) + add = xor(bool(f.name.lower() in models), bool(exclude)) + if add: + path = Path("benchmarks") / f.name + folders[f.name] = str(path.resolve()) + folders = check_cuda(folders) return folders @@ -225,13 +251,44 @@ def gen_and_save_md_table(metrics): # function to run the all the models -def run(times=1): +def run(times=1, models=None, exclude=False): """ Please be aware that this function can only work under Linux. MacOS and Windows will be supported in the future. Any PR to enhance this method is highly welcomed. + + Parameters: + ----------- + times : int + determines how many times the model should be running. + models : str or list + determines the specific model or list of models to run or exclude. + exclude : boolean + determines whether the model being used is excluded or included. + + Usage: + ------- + Here are some use cases of the function in the bash: + + .. code-block:: bash + + # Case 1 - run all models multiple times + python run_all_model.py 3 + + # Case 2 - run specific models multiple times + python run_all_model.py 3 dnn + + # Case 3 - run other models except those are given as arguments for multiple times + python run_all_model.py 3 [dnn,tft,lstm] True + + # Case 4 - run specific models for one time + python run_all_model.py --models=[dnn,lightgbm] + + # Case 5 - run other models except those are given as aruments for one time + python run_all_model.py --models=[dnn,tft,sfm] --exclude=True + """ # get all folders - folders = get_all_folders() + folders = get_all_folders(models, exclude) # set up compatible = True if sys.version_info < (3, 3): From d0ca52f3fdd34ea246751b94da69df588a50e7d0 Mon Sep 17 00:00:00 2001 From: Dong Zhou Date: Thu, 26 Nov 2020 12:04:48 +0800 Subject: [PATCH 153/241] add robust zscore processor & ALPHA360 support custom processors --- qlib/contrib/data/handler.py | 84 ++++++++++++++++++++-------------- qlib/data/dataset/processor.py | 38 ++++++++++++++- 2 files changed, 86 insertions(+), 36 deletions(-) diff --git a/qlib/contrib/data/handler.py b/qlib/contrib/data/handler.py index 8cce92907..07ef2267a 100644 --- a/qlib/contrib/data/handler.py +++ b/qlib/contrib/data/handler.py @@ -10,6 +10,28 @@ from inspect import getfullargspec import copy +def check_transform_proc(proc_l, fit_start_time, fit_end_time): + new_l = [] + for p in proc_l: + if not isinstance(p, Processor): + klass, pkwargs = get_cls_kwargs(p, processor_module) + args = getfullargspec(klass).args + if "fit_start_time" in args and "fit_end_time" in args: + assert ( + fit_start_time is not None and fit_end_time is not None + ), "Make sure `fit_start_time` and `fit_end_time` are not None." + pkwargs.update( + { + "fit_start_time": fit_start_time, + "fit_end_time": fit_end_time, + } + ) + new_l.append({"class": klass.__name__, "kwargs": pkwargs}) + else: + new_l.append(p) + return new_l + + class ALPHA360_Denoise(DataHandlerLP): def __init__(self, instruments="csi500", start_time=None, end_time=None, fit_start_time=None, fit_end_time=None): data_loader = { @@ -83,8 +105,31 @@ class ALPHA360_Denoise(DataHandlerLP): return fields, names +_DEFAULT_LEARN_PROCESSORS = [ + {"class": "DropnaLabel"}, + {"class": "CSZScoreNorm", "kwargs": {"fields_group": "label"}}, +] +_DEFAULT_INFER_PROCESSORS = [ + {"class": "ProcessInf", "kwargs": {}}, + {"class": "ZScoreNorm", "kwargs": {}}, + {"class": "Fillna", "kwargs": {}}, +] + + class ALPHA360(DataHandlerLP): - def __init__(self, instruments="csi500", start_time=None, end_time=None, fit_start_time=None, fit_end_time=None): + def __init__( + self, + instruments="csi500", + start_time=None, + end_time=None, + infer_processors=_DEFAULT_INFER_PROCESSORS, + learn_processors=_DEFAULT_LEARN_PROCESSORS, + fit_start_time=None, + fit_end_time=None, + ): + infer_processors = check_transform_proc(infer_processors, fit_start_time, fit_end_time) + learn_processors = check_transform_proc(learn_processors, fit_start_time, fit_end_time) + data_loader = { "class": "QlibDataLoader", "kwargs": { @@ -95,16 +140,6 @@ class ALPHA360(DataHandlerLP): }, } - learn_processors = [ - {"class": "DropnaLabel", "kwargs": {"fields_group": "label"}}, - {"class": "CSZScoreNorm", "kwargs": {"fields_group": "label"}}, - ] - infer_processors = [ - {"class": "ProcessInf", "kwargs": {}}, - {"class": "ZscoreNorm", "kwargs": {"fit_start_time": fit_start_time, "fit_end_time": fit_end_time}}, - {"class": "Fillna", "kwargs": {}}, - ] - super().__init__( instruments, start_time, @@ -168,33 +203,12 @@ class Alpha158(DataHandlerLP): start_time=None, end_time=None, infer_processors=[], - learn_processors=["DropnaLabel", {"class": "CSZScoreNorm", "kwargs": {"fields_group": "label"}}], + learn_processors=_DEFAULT_LEARN_PROCESSORS, fit_start_time=None, fit_end_time=None, ): - def check_transform_proc(proc_l): - new_l = [] - for p in proc_l: - if not isinstance(p, Processor): - klass, pkwargs = get_cls_kwargs(p, processor_module) - args = getfullargspec(klass).args - if "fit_start_time" in args and "fit_end_time" in args: - assert ( - fit_start_time is not None and fit_end_time is not None - ), "Make sure `fit_start_time` and `fit_end_time` are not None." - pkwargs.update( - { - "fit_start_time": fit_start_time, - "fit_end_time": fit_end_time, - } - ) - new_l.append({"class": klass.__name__, "kwargs": pkwargs}) - else: - new_l.append(p) - return new_l - - infer_processors = check_transform_proc(infer_processors) - learn_processors = check_transform_proc(learn_processors) + infer_processors = check_transform_proc(infer_processors, fit_start_time, fit_end_time) + learn_processors = check_transform_proc(learn_processors, fit_start_time, fit_end_time) data_loader = { "class": "QlibDataLoader", diff --git a/qlib/data/dataset/processor.py b/qlib/data/dataset/processor.py index e4003a1f5..b764875ed 100755 --- a/qlib/data/dataset/processor.py +++ b/qlib/data/dataset/processor.py @@ -166,7 +166,9 @@ class MinMaxNorm(Processor): return df -class ZscoreNorm(Processor): +class ZScoreNorm(Processor): + """ZScore Normalization""" + def __init__(self, fit_start_time, fit_end_time, fields_group=None): self.fit_start_time = fit_start_time self.fit_end_time = fit_end_time @@ -193,6 +195,40 @@ class ZscoreNorm(Processor): return df +class RobustZScoreNorm(Processor): + """Robust ZScore Normalization + + Use robust statistics for Z-Score normalization: + mean(x) = median(x) + std(x) = MAD(x) * 1.4826 + + Reference: + https://en.wikipedia.org/wiki/Median_absolute_deviation. + """ + + def __init__(self, fit_start_time, fit_end_time, fields_group=None, clip_outlier=True): + self.fit_start_time = fit_start_time + self.fit_end_time = fit_end_time + self.fields_group = fields_group + self.clip_outlier = clip_outlier + + def fit(self, df): + df = fetch_df_by_index(df, slice(self.fit_start_time, self.fit_end_time), level="datetime") + self.cols = get_group_columns(df, self.fields_group) + X = df[self.cols].values + self.mean_train = np.nanmedian(X, axis=0) + self.std_train = np.nanmedian(np.abs(X - self.mean_train), axis=0) + self.std_train += EPS + self.std_train *= 1.4826 + + def __call__(self, df): + df.loc(axis=1)[self.cols] -= self.mean_train + df.loc(axis=1)[self.cols] /= self.std_train + if self.clip_outlier: + df.clip(-3, 3, inplace=True) + return df + + class CSZScoreNorm(Processor): """Cross Sectional ZScore Normalization""" From 37cc51465cb4ad55bf45f0740dfab9a949c94efc Mon Sep 17 00:00:00 2001 From: Dong Zhou Date: Thu, 26 Nov 2020 12:25:39 +0800 Subject: [PATCH 154/241] improve perf of robust zscore processor --- qlib/data/dataset/processor.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/qlib/data/dataset/processor.py b/qlib/data/dataset/processor.py index b764875ed..e2d251aa7 100755 --- a/qlib/data/dataset/processor.py +++ b/qlib/data/dataset/processor.py @@ -222,8 +222,10 @@ class RobustZScoreNorm(Processor): self.std_train *= 1.4826 def __call__(self, df): - df.loc(axis=1)[self.cols] -= self.mean_train - df.loc(axis=1)[self.cols] /= self.std_train + X = df[self.cols] + X -= self.mean_train + X /= self.std_train + df[self.cols] = X if self.clip_outlier: df.clip(-3, 3, inplace=True) return df From 45192413f9abb5f89c6cc05d80cd980a4cd145db Mon Sep 17 00:00:00 2001 From: Jactus Date: Thu, 26 Nov 2020 13:11:56 +0800 Subject: [PATCH 155/241] Fix --- examples/run_all_model.py | 5 +++-- qlib/workflow/cli.py | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/examples/run_all_model.py b/examples/run_all_model.py index 6f12434da..b09750674 100644 --- a/examples/run_all_model.py +++ b/examples/run_all_model.py @@ -164,13 +164,14 @@ class ExtendedEnvBuilder(venv.EnvBuilder): # function to check cuda version on the machine, this case is for the model TFT def check_cuda(folders): - path = "/usr/local/cuda/version.txt" + path = "/usr/local/cuda/version.txt" # TODO: FIX ME, this will not work on other os systems. exclude_tft = True if os.path.exists(path): - with open(path, "w") as f: + with open(path, "r") as f: if "10.1" in str(f.read()) or "10.0" in str(f.read()): exclude_tft = False if exclude_tft and "TFT" in folders: + sys.stderr.write("Compatible CUDA version not found! Removing TFT from the workflow...\n") del folders["TFT"] return folders diff --git a/qlib/workflow/cli.py b/qlib/workflow/cli.py index 2e087877b..ecec8d3d7 100644 --- a/qlib/workflow/cli.py +++ b/qlib/workflow/cli.py @@ -27,9 +27,9 @@ def sys_config(config, config_path): Parameters ---------- config : dict - configuration of the workflow + configuration of the workflow. config_path : str - configuration of the path + configuration of the path. """ sys_config = config.get("sys", {}) From 5102566aad27ef8e3f55aa022e2216f614394357 Mon Sep 17 00:00:00 2001 From: lwwang1995 Date: Thu, 26 Nov 2020 13:35:07 +0800 Subject: [PATCH 156/241] Update GRU and LSTM model. --- qlib/contrib/model/pytorch_gru.py | 17 ++++------------- qlib/contrib/model/pytorch_lstm.py | 18 +++++------------- 2 files changed, 9 insertions(+), 26 deletions(-) diff --git a/qlib/contrib/model/pytorch_gru.py b/qlib/contrib/model/pytorch_gru.py index 4cc7f9852..2dd8464e2 100755 --- a/qlib/contrib/model/pytorch_gru.py +++ b/qlib/contrib/model/pytorch_gru.py @@ -28,14 +28,10 @@ class GRU(Model): Parameters ---------- - input_dim : int - input dimension - output_dim : int - output dimension - layers : tuple - layer sizes - lr : float - learning rate + d_feat : int + input dimension for each time step + metric: str + the evaluate metric used in early stop optimizer : str optimizer name GPU : str @@ -112,10 +108,6 @@ class GRU(Model): ) ) - if loss not in {"mse", "binary"}: - raise NotImplementedError("loss {} is not supported!".format(loss)) - self._scorer = mean_squared_error if loss == "mse" else roc_auc_score - self.gru_model = GRUModel( d_feat=self.d_feat, hidden_size=self.hidden_size, num_layers=self.num_layers, dropout=self.dropout ) @@ -251,7 +243,6 @@ class GRU(Model): # train self.logger.info("training...") self._fitted = True - # return for step in range(self.n_epochs): self.logger.info("Epoch%d:", step) diff --git a/qlib/contrib/model/pytorch_lstm.py b/qlib/contrib/model/pytorch_lstm.py index 8b8454380..adb895247 100755 --- a/qlib/contrib/model/pytorch_lstm.py +++ b/qlib/contrib/model/pytorch_lstm.py @@ -28,20 +28,17 @@ class LSTM(Model): Parameters ---------- - input_dim : int - input dimension - output_dim : int - output dimension - layers : tuple - layer sizes - lr : float - learning rate + d_feat : int + input dimension for each time step + metric: str + the evaluate metric used in early stop optimizer : str optimizer name GPU : str the GPU ID(s) used for training """ + def __init__( self, d_feat=6, @@ -112,10 +109,6 @@ class LSTM(Model): ) ) - if loss not in {"mse", "binary"}: - raise NotImplementedError("loss {} is not supported!".format(loss)) - self._scorer = mean_squared_error if loss == "mse" else roc_auc_score - self.lstm_model = LSTMModel( d_feat=self.d_feat, hidden_size=self.hidden_size, num_layers=self.num_layers, dropout=self.dropout ) @@ -251,7 +244,6 @@ class LSTM(Model): # train self.logger.info("training...") self._fitted = True - # return for step in range(self.n_epochs): self.logger.info("Epoch%d:", step) From 398f67f8d8687e39fba8f7d54f245797f4cf7df4 Mon Sep 17 00:00:00 2001 From: Hong Zhang Date: Thu, 26 Nov 2020 13:49:13 +0800 Subject: [PATCH 157/241] gat1 --- examples/workflow_by_code_gats.py | 7 ++----- qlib/contrib/model/pytorch_gats.py | 14 ++++---------- 2 files changed, 6 insertions(+), 15 deletions(-) diff --git a/examples/workflow_by_code_gats.py b/examples/workflow_by_code_gats.py index 3bb4edf08..b5bad31ec 100644 --- a/examples/workflow_by_code_gats.py +++ b/examples/workflow_by_code_gats.py @@ -7,19 +7,16 @@ from pathlib import Path import qlib import pandas as pd from qlib.config import REG_CN -from qlib.contrib.model.pytorch_gats import GAT -from qlib.contrib.data.handler import ALPHA360_Denoise + 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 - -# from qlib.model.learner import train_model from qlib.utils import init_instance_by_config -import pickle + if __name__ == "__main__": diff --git a/qlib/contrib/model/pytorch_gats.py b/qlib/contrib/model/pytorch_gats.py index 07af4eda4..fad52e834 100755 --- a/qlib/contrib/model/pytorch_gats.py +++ b/qlib/contrib/model/pytorch_gats.py @@ -28,14 +28,12 @@ class GAT(Model): Parameters ---------- - input_dim : int - input dimension - output_dim : int - output dimension - layers : tuple - layer sizes lr : float learning rate + d_feat : int + input dimensions for each time step + metric : str + the evaluate metric used in early stop optimizer : str optimizer name GPU : str @@ -398,10 +396,6 @@ class GATModel(nn.Module): hidden = self.bn1(hidden) gamma = self.cal_convariance(hidden, hidden) - # gamma = hidden.mm(torch.t(hidden)) - # gamma = self.leaky_relu(gamma) - # gamma = self.softmax(gamma) - # gamma = gamma * (torch.ones(x.shape[0], x.shape[0]).to(device) - torch.diag(torch.ones(x.shape[0])).to(device)) output = gamma.mm(hidden) output = self.fc(output) output = self.bn2(output) From 2a170624216c5344c039edec9173a74370d3de86 Mon Sep 17 00:00:00 2001 From: Jactus Date: Thu, 26 Nov 2020 13:49:51 +0800 Subject: [PATCH 158/241] Update run_all_model and format --- examples/benchmarks/TFT/README.md | 4 ++-- examples/run_all_model.py | 15 --------------- qlib/contrib/model/pytorch_lstm.py | 1 - 3 files changed, 2 insertions(+), 18 deletions(-) diff --git a/examples/benchmarks/TFT/README.md b/examples/benchmarks/TFT/README.md index e9e44db1a..5a6a9f153 100644 --- a/examples/benchmarks/TFT/README.md +++ b/examples/benchmarks/TFT/README.md @@ -8,7 +8,7 @@ Users can follow the ``workflow_by_code_tft.py`` to run the benchmark. ### Notes -1. Please be **aware** that this script can only support `Python 3.5 - 3.8`, and `Cuda 10.0 or 10.1`. -2. Please remember to install `cudatoolkit==10.1` and `cudnn==7.6` on your machine. +1. Please be **aware** that this script can only support `Python 3.5 - 3.8`. +2. If the CUDA version on your machine is not 10.0, please remember to run the following commands `conda install anaconda cudatoolkit=10.0` and `conda install cudnn` on your machine. 3. The model must run in GPU, or an error will be raised. 4. New datasets should be registered in ``data_formatters``, for detail please visit the source. diff --git a/examples/run_all_model.py b/examples/run_all_model.py index b09750674..2f6c4299e 100644 --- a/examples/run_all_model.py +++ b/examples/run_all_model.py @@ -162,20 +162,6 @@ class ExtendedEnvBuilder(venv.EnvBuilder): self.install_script(context, "pip", url) -# function to check cuda version on the machine, this case is for the model TFT -def check_cuda(folders): - path = "/usr/local/cuda/version.txt" # TODO: FIX ME, this will not work on other os systems. - exclude_tft = True - if os.path.exists(path): - with open(path, "r") as f: - if "10.1" in str(f.read()) or "10.0" in str(f.read()): - exclude_tft = False - if exclude_tft and "TFT" in folders: - sys.stderr.write("Compatible CUDA version not found! Removing TFT from the workflow...\n") - del folders["TFT"] - return folders - - # function to calculate the mean and std of a list in the results dictionary def cal_mean_std(results) -> dict: mean_std = dict() @@ -205,7 +191,6 @@ def get_all_folders(models, exclude) -> dict: if add: path = Path("benchmarks") / f.name folders[f.name] = str(path.resolve()) - folders = check_cuda(folders) return folders diff --git a/qlib/contrib/model/pytorch_lstm.py b/qlib/contrib/model/pytorch_lstm.py index adb895247..be43d3698 100755 --- a/qlib/contrib/model/pytorch_lstm.py +++ b/qlib/contrib/model/pytorch_lstm.py @@ -38,7 +38,6 @@ class LSTM(Model): the GPU ID(s) used for training """ - def __init__( self, d_feat=6, From f42661f2d4250ffc26f0f4cedd07d94097ba6b4a Mon Sep 17 00:00:00 2001 From: Hong Zhang Date: Thu, 26 Nov 2020 13:55:12 +0800 Subject: [PATCH 159/241] gat2 --- qlib/contrib/model/pytorch_gats.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/qlib/contrib/model/pytorch_gats.py b/qlib/contrib/model/pytorch_gats.py index fad52e834..aa5b22119 100755 --- a/qlib/contrib/model/pytorch_gats.py +++ b/qlib/contrib/model/pytorch_gats.py @@ -117,11 +117,7 @@ class GAT(Model): seed, ) ) - - if loss not in {"mse", "binary"}: - raise NotImplementedError("loss {} is not supported!".format(loss)) - self._scorer = mean_squared_error if loss == "mse" else roc_auc_score - + self.GAT_model = GATModel( d_feat=self.d_feat, hidden_size=self.hidden_size, @@ -211,7 +207,6 @@ class GAT(Model): losses = [] indices = np.arange(len(x_values)) - np.random.shuffle(indices) for i in range(len(indices))[:: self.batch_size]: From e8bb5061b06da31942bd432c991ea6c5d431d434 Mon Sep 17 00:00:00 2001 From: Hong Zhang Date: Thu, 26 Nov 2020 13:58:45 +0800 Subject: [PATCH 160/241] change code style for gats --- qlib/contrib/model/pytorch_gats.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/qlib/contrib/model/pytorch_gats.py b/qlib/contrib/model/pytorch_gats.py index aa5b22119..7cdfb571a 100755 --- a/qlib/contrib/model/pytorch_gats.py +++ b/qlib/contrib/model/pytorch_gats.py @@ -370,7 +370,6 @@ class GATModel(nn.Module): self.fc_out = nn.Linear(hidden_size, 1) self.leaky_relu = nn.LeakyReLU() self.softmax = nn.Softmax(dim=1) - self.d_feat = d_feat def cal_convariance(self, x, y): # the 2nd dimension of x and y are the same @@ -389,7 +388,6 @@ class GATModel(nn.Module): out, _ = self.rnn(x) hidden = out[:, -1, :] hidden = self.bn1(hidden) - gamma = self.cal_convariance(hidden, hidden) output = gamma.mm(hidden) output = self.fc(output) From 305b9372c7dc24ade46be1dfde759508669e450d Mon Sep 17 00:00:00 2001 From: Hong Zhang Date: Thu, 26 Nov 2020 14:15:54 +0800 Subject: [PATCH 161/241] change code style in Catboost and XGboost --- qlib/contrib/model/catboost_model.py | 20 +++++++++---------- qlib/contrib/model/xgboost.py | 30 ++++++++++++++-------------- 2 files changed, 25 insertions(+), 25 deletions(-) diff --git a/qlib/contrib/model/catboost_model.py b/qlib/contrib/model/catboost_model.py index bba006c35..eb97fc75b 100644 --- a/qlib/contrib/model/catboost_model.py +++ b/qlib/contrib/model/catboost_model.py @@ -34,14 +34,14 @@ class CatBoostModel(Model): def fit( self, dataset: DatasetH, - num_boost_round=1000, - early_stopping_rounds=50, - verbose_eval=20, - evals_result=dict(), + num_boost_round = 1000, + early_stopping_rounds = 50, + verbose_eval = 20, + evals_result = dict(), **kwargs ): df_train, df_valid = dataset.prepare( - ["train", "valid"], col_set=["feature", "label"], data_key=DataHandlerLP.DK_L + ["train", "valid"], col_set = ["feature", "label"], data_key = DataHandlerLP.DK_L ) x_train, y_train = df_train["feature"], df_train["label"] x_valid, y_valid = df_valid["feature"], df_valid["label"] @@ -52,8 +52,8 @@ class CatBoostModel(Model): else: raise ValueError("CatBoost doesn't support multi-label training") - train_pool = Pool(data=x_train, label=y_train_1d) - valid_pool = Pool(data=x_valid, label=y_valid_1d) + train_pool = Pool(data = x_train, label = y_train_1d) + valid_pool = Pool(data = x_valid, label = y_valid_1d) # Initialize the catboost model self._params["iterations"] = num_boost_round @@ -63,7 +63,7 @@ class CatBoostModel(Model): self.model = CatBoost(self._params, **kwargs) # train the model - self.model.fit(train_pool, eval_set=valid_pool, use_best_model=True, **kwargs) + self.model.fit(train_pool, eval_set = valid_pool, use_best_model = True, **kwargs) evals_result = self.model.get_evals_result() evals_result["train"] = list(evals_result["learn"].values())[0] @@ -72,8 +72,8 @@ class CatBoostModel(Model): def predict(self, dataset): if self.model is None: raise ValueError("model is not fitted yet!") - x_test = dataset.prepare("test", col_set="feature") - return pd.Series(self.model.predict(x_test.values), index=x_test.index) + x_test = dataset.prepare("test", col_set = "feature") + return pd.Series(self.model.predict(x_test.values), index = x_test.index) if __name__ == "__main__": diff --git a/qlib/contrib/model/xgboost.py b/qlib/contrib/model/xgboost.py index 039fd2c80..203e71b9a 100755 --- a/qlib/contrib/model/xgboost.py +++ b/qlib/contrib/model/xgboost.py @@ -30,15 +30,15 @@ class XGBModel(Model): def fit( self, dataset: DatasetH, - num_boost_round=1000, - early_stopping_rounds=50, - verbose_eval=20, - evals_result=dict(), + num_boost_round = 1000, + early_stopping_rounds = 50, + verbose_eval = 20, + evals_result = dict(), **kwargs ): df_train, df_valid = dataset.prepare( - ["train", "valid"], col_set=["feature", "label"], data_key=DataHandlerLP.DK_L + ["train", "valid"], col_set = ["feature", "label"], data_key = DataHandlerLP.DK_L ) x_train, y_train = df_train["feature"], df_train["label"] x_valid, y_valid = df_valid["feature"], df_valid["label"] @@ -49,16 +49,16 @@ class XGBModel(Model): else: raise ValueError("XGBoost doesn't support multi-label training") - dtrain = xgb.DMatrix(x_train.values, label=y_train_1d) - dvalid = xgb.DMatrix(x_valid.values, label=y_valid_1d) + dtrain = xgb.DMatrix(x_train.values, label = y_train_1d) + dvalid = xgb.DMatrix(x_valid.values, label = y_valid_1d) self.model = xgb.train( self._params, - dtrain=dtrain, - num_boost_round=num_boost_round, - evals=[(dtrain, "train"), (dvalid, "valid")], - early_stopping_rounds=early_stopping_rounds, - verbose_eval=verbose_eval, - evals_result=evals_result, + dtrain = dtrain, + num_boost_round = num_boost_round, + evals = [(dtrain, "train"), (dvalid, "valid")], + early_stopping_rounds = early_stopping_rounds, + verbose_eval = verbose_eval, + evals_result = evals_result, **kwargs ) evals_result["train"] = list(evals_result["train"].values())[0] @@ -67,5 +67,5 @@ class XGBModel(Model): def predict(self, dataset): if self.model is None: raise ValueError("model is not fitted yet!") - x_test = dataset.prepare("test", col_set="feature") - return pd.Series(self.model.predict(xgb.DMatrix(x_test.values)), index=x_test.index) + x_test = dataset.prepare("test", col_set = "feature") + return pd.Series(self.model.predict(xgb.DMatrix(x_test.values)), index = x_test.index) From 28b11886dd47f9304406167f934875922bd28c65 Mon Sep 17 00:00:00 2001 From: Alex Wang Date: Thu, 26 Nov 2020 14:35:16 +0800 Subject: [PATCH 162/241] update --- examples/workflow_by_code_sfm.py | 21 ++- qlib/contrib/model/pytorch_sfm.py | 298 +++++++++++++++--------------- 2 files changed, 158 insertions(+), 161 deletions(-) diff --git a/examples/workflow_by_code_sfm.py b/examples/workflow_by_code_sfm.py index ccc2d412c..4de79e075 100644 --- a/examples/workflow_by_code_sfm.py +++ b/examples/workflow_by_code_sfm.py @@ -71,21 +71,22 @@ if __name__ == "__main__": "module_path": "qlib.contrib.model.pytorch_sfm", "kwargs": { "d_feat": 6, - "hidden_size": 32, - "output_dim": 16, - "freq_dim": 25, + "hidden_size": 64, + "output_dim" : 32, + "freq_dim" : 25, "dropout_W": 0.5, "dropout_U": 0.5, - "n_epochs": 200, - "lr": 1e-3, - "batch_size": 200, + "n_epochs": 15, + "lr": 1e-2, + "metric": "", + "batch_size": 1600, "early_stop": 20, "eval_steps": 5, "loss": "mse", - "lr_decay": 0.96, - "lr_decay_steps": 100, - "optimizer": "adam", - "GPU": 1, + "lr_decay" : 0.96, + "lr_decay_steps" : 100, + "optimizer" : "adam", + "GPU": 3, "seed": 710, }, }, diff --git a/qlib/contrib/model/pytorch_sfm.py b/qlib/contrib/model/pytorch_sfm.py index d8baa9cb2..4ec61430e 100644 --- a/qlib/contrib/model/pytorch_sfm.py +++ b/qlib/contrib/model/pytorch_sfm.py @@ -31,7 +31,6 @@ from ...model.base import Model from ...data.dataset import DatasetH from ...data.dataset.handler import DataHandlerLP - class SFM_Model(nn.Module): def __init__(self, d_feat=6, output_dim=1, freq_dim=10, hidden_size=64, dropout_W=0.0, dropout_U=0.0, device="cpu"): super().__init__() @@ -76,13 +75,13 @@ class SFM_Model(nn.Module): self.states = [] def forward(self, input): - input = input.reshape(len(input), self.input_dim, -1) # [N, F, T] - input = input.permute(0, 2, 1) # [N, T, F] + input = input.reshape(len(input), self.input_dim, -1) # [N, F, T] + input = input.permute(0, 2, 1) # [N, T, F] time_step = input.shape[1] - + for ts in range(time_step): - x = input[:, ts, :] - if len(self.states) == 0: # hasn't initialized yet + x = input[:, ts,:] + if len(self.states)==0: #hasn't initialized yet self.init_states(x) self.get_constants(x) p_tm1 = self.states[0] @@ -99,65 +98,64 @@ class SFM_Model(nn.Module): x_fre = torch.matmul(x * B_W[0], self.W_fre) + self.b_fre x_c = torch.matmul(x * B_W[0], self.W_c) + self.b_c x_o = torch.matmul(x * B_W[0], self.W_o) + self.b_o - - i = self.inner_activation( - x_i + torch.matmul(h_tm1 * B_U[0], self.U_i) - ) # not sure whether I am doing in the right unsquuze + + i = self.inner_activation(x_i + torch.matmul(h_tm1 * B_U[0], self.U_i)) # not sure whether I am doing in the right unsquuze + ste = self.inner_activation(x_ste + torch.matmul(h_tm1 * B_U[0], self.U_ste)) fre = self.inner_activation(x_fre + torch.matmul(h_tm1 * B_U[0], self.U_fre)) ste = torch.reshape(ste, (-1, self.hidden_dim, 1)) fre = torch.reshape(fre, (-1, 1, self.freq_dim)) - + f = ste * fre - + c = i * self.activation(x_c + torch.matmul(h_tm1 * B_U[0], self.U_c)) time = time_tm1 + 1 omega = torch.tensor(2 * np.pi) * time * frequency - re = torch.cos(omega) + re = torch.cos(omega) im = torch.sin(omega) - + c = torch.reshape(c, (-1, self.hidden_dim, 1)) S_re = f * S_re_tm1 + c * re S_im = f * S_im_tm1 + c * im - + A = torch.square(S_re) + torch.square(S_im) A = torch.reshape(A, (-1, self.freq_dim)).float() A_a = torch.matmul(A * B_U[0], self.U_a) A_a = torch.reshape(A_a, (-1, self.hidden_dim)) a = self.activation(A_a + self.b_a) - + o = self.inner_activation(x_o + torch.matmul(h_tm1 * B_U[0], self.U_o)) h = o * a p = torch.matmul(h, self.W_p) + self.b_p self.states = [p, h, S_re, S_im, time, None, None, None] - self.states = [] + self.states = [] return self.fc_out(p).squeeze() def init_states(self, x): reducer_f = torch.zeros((self.hidden_dim, self.freq_dim)).to(self.device) reducer_p = torch.zeros((self.hidden_dim, self.output_dim)).to(self.device) - + init_state_h = torch.zeros(self.hidden_dim).to(self.device) init_state_p = torch.matmul(init_state_h, reducer_p) - + init_state = torch.zeros_like(init_state_h).to(self.device) init_freq = torch.matmul(init_state_h, reducer_f) init_state = torch.reshape(init_state, (-1, self.hidden_dim, 1)) init_freq = torch.reshape(init_freq, (-1, 1, self.freq_dim)) - + init_state_S_re = init_state * init_freq init_state_S_im = init_state * init_freq - + init_state_time = torch.tensor(0).to(self.device) self.states = [init_state_p, init_state_h, init_state_S_re, init_state_S_im, init_state_time, None, None, None] @@ -203,6 +201,7 @@ class SFM(Model): dropout_U=0.0, n_epochs=200, lr=0.001, + metric = "", batch_size=2000, early_stop=20, eval_steps=5, @@ -227,14 +226,15 @@ class SFM(Model): self.dropout_U = dropout_U self.n_epochs = n_epochs self.lr = lr + self.metric = metric self.batch_size = batch_size self.early_stop = early_stop self.eval_steps = eval_steps self.lr_decay = lr_decay self.lr_decay_steps = lr_decay_steps self.optimizer = optimizer.lower() - self.loss_type = loss - self.device = "cuda:%d" % (GPU) if torch.cuda.is_available() else "cpu" + self.loss = loss + self.device = "cuda:%d"%(GPU) if torch.cuda.is_available() else "cpu" self.use_gpu = torch.cuda.is_available() self.seed = seed @@ -243,11 +243,12 @@ class SFM(Model): "\nd_feat : {}" "\nhidden_size : {}" "\noutput_size : {}" - "\nfrequency_dimension : {}" + "\nfrequency_dimension : {}" "\ndropout_W: {}" "\ndropout_U: {}" "\nn_epochs : {}" "\nlr : {}" + "\nmetric : {}" "\nbatch_size : {}" "\nearly_stop : {}" "\neval_steps : {}" @@ -266,6 +267,7 @@ class SFM(Model): dropout_U, n_epochs, lr, + metric, batch_size, early_stop, eval_steps, @@ -284,14 +286,14 @@ class SFM(Model): self._scorer = mean_squared_error if loss == "mse" else roc_auc_score self.sfm_model = SFM_Model( - d_feat=self.d_feat, + d_feat=self.d_feat, output_dim=self.output_dim, - hidden_size=self.hidden_size, - freq_dim=self.freq_dim, - dropout_W=self.dropout_W, - dropout_U=self.dropout_U, - device=self.device, - ) + hidden_size=self.hidden_size, + freq_dim=self.freq_dim, + dropout_W=self.dropout_W, + dropout_U=self.dropout_U, + device=self.device + ) if optimizer.lower() == "adam": self.train_optimizer = optim.Adam(self.sfm_model.parameters(), lr=self.lr) elif optimizer.lower() == "gd": @@ -299,24 +301,73 @@ class SFM(Model): else: raise NotImplementedError("optimizer {} is not supported!".format(optimizer)) - # Reduce learning rate when loss has stopped decrease - self.scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau( - self.train_optimizer, - mode="min", - factor=0.5, - patience=10, - verbose=True, - threshold=0.0001, - threshold_mode="rel", - cooldown=0, - min_lr=0.00001, - eps=1e-08, - ) - self._fitted = False self.sfm_model.to(self.device) - def fit(self, dataset: DatasetH, evals_result=dict(), verbose=True, save_path=None, **kwargs): + def test_epoch(self, data_x, data_y): + + # prepare training data + x_values = data_x.values + y_values = np.squeeze(data_y.values) + + self.sfm_model.eval() + + scores = [] + losses = [] + + indices = np.arange(len(x_values)) + np.random.shuffle(indices) + + for i in range(len(indices))[:: self.batch_size]: + + if len(indices) - i < self.batch_size: + break + + feature = torch.from_numpy(x_values[indices[i : i + self.batch_size]]).float().to(self.device) + label = torch.from_numpy(y_values[indices[i : i + self.batch_size]]).float().to(self.device) + + pred = self.sfm_model(feature) + loss = self.loss_fn(pred, label) + losses.append(loss.item()) + + score = self.metric_fn(pred, label) + scores.append(score.item()) + + return np.mean(losses), np.mean(scores) + + def train_epoch(self, x_train, y_train): + + x_train_values = x_train.values + y_train_values = np.squeeze(y_train.values) * 100 + + self.sfm_model.train() + + indices = np.arange(len(x_train_values)) + np.random.shuffle(indices) + + for i in range(len(indices))[:: self.batch_size]: + + if len(indices) - i < self.batch_size: + break + + feature = torch.from_numpy(x_train_values[indices[i : i + self.batch_size]]).float().to(self.device) + label = torch.from_numpy(y_train_values[indices[i : i + self.batch_size]]).float().to(self.device) + + pred = self.sfm_model(feature) + loss = self.loss_fn(pred, label) + + self.train_optimizer.zero_grad() + loss.backward() + torch.nn.utils.clip_grad_value_(self.sfm_model.parameters(), 3.0) + self.train_optimizer.step() + + def fit( + self, + dataset: DatasetH, + evals_result=dict(), + verbose=True, + save_path=None, + ): df_train, df_valid = dataset.prepare( ["train", "valid"], col_set=["feature", "label"], data_key=DataHandlerLP.DK_L @@ -324,10 +375,10 @@ class SFM(Model): x_train, y_train = df_train["feature"], df_train["label"] x_valid, y_valid = df_valid["feature"], df_valid["label"] - save_path = create_save_path(save_path) stop_steps = 0 train_loss = 0 - best_loss = np.inf + best_score = -np.inf + best_epoch = 0 evals_result["train"] = [] evals_result["valid"] = [] @@ -335,90 +386,56 @@ class SFM(Model): self.logger.info("training...") self._fitted = True - # prepare training data - x_train_values = torch.from_numpy(x_train.values).float() - y_train_values = torch.from_numpy(np.squeeze(y_train.values)).float() - train_num = y_train_values.shape[0] - - # prepare validation data - x_val_auto = torch.from_numpy(x_valid.values).float() - y_val_auto = torch.from_numpy(np.squeeze(y_valid.values)).float() - - x_val_auto = x_val_auto.to(self.device) - y_val_auto = y_val_auto.to(self.device) - for step in range(self.n_epochs): - if stop_steps >= self.early_stop: - if verbose: - self.logger.info("\tearly stop") - break - loss = AverageMeter() - self.sfm_model.train() - self.train_optimizer.zero_grad() + self.logger.info("Epoch%d:", step) + self.logger.info("training...") + self.train_epoch(x_train, y_train) + self.logger.info("evaluating...") + train_loss, train_score = self.test_epoch(x_train, y_train) + val_loss, val_score = self.test_epoch(x_valid, y_valid) + self.logger.info("train %.6f, valid %.6f" % (train_score, val_score)) + evals_result["train"].append(train_score) + evals_result["valid"].append(val_score) - choice = np.random.choice(train_num, self.batch_size) - x_batch_auto = x_train_values[choice] - y_batch_auto = y_train_values[choice] - - x_batch_auto = x_batch_auto.to(self.device) - y_batch_auto = y_batch_auto.to(self.device) - - # forward - preds = self.sfm_model(x_batch_auto) - cur_loss = self.get_loss(preds, y_batch_auto, self.loss_type) - cur_loss.backward() - self.train_optimizer.step() - loss.update(cur_loss.item()) - - # validation - train_loss += loss.val - if step and step % self.eval_steps == 0: + if val_score > best_score: + best_score = val_score + stop_steps = 0 + best_epoch = step + best_param = copy.deepcopy(self.sfm_model.state_dict()) + else: stop_steps += 1 - train_loss /= self.eval_steps - - with torch.no_grad(): - self.sfm_model.eval() - loss_val = AverageMeter() - - # forward - preds = self.sfm_model(x_val_auto) - cur_loss_val = self.get_loss(preds, y_val_auto, self.loss_type) - loss_val.update(cur_loss_val.item()) - - if verbose: - self.logger.info( - "[Epoch {}]: train_loss {:.6f}, valid_loss {:.6f}".format(step, train_loss, loss_val.val) - ) - evals_result["train"].append(train_loss) - evals_result["valid"].append(loss_val.val) - if loss_val.val < best_loss: - if verbose: - self.logger.info( - "\tvalid loss update from {:.6f} to {:.6f}, save checkpoint.".format( - best_loss, loss_val.val - ) - ) - best_loss = loss_val.val - stop_steps = 0 - torch.save(self.sfm_model.state_dict(), save_path) - train_loss = 0 - # update learning rate - self.scheduler.step(cur_loss_val) - + if stop_steps >= self.early_stop: + self.logger.info("early stop") + break + self.logger.info("best score: %.6lf @ %d" % (best_score, best_epoch)) if self.device != "cpu": torch.cuda.empty_cache() - def get_loss(self, pred, target, loss_type): - if loss_type == "mse": - sqr_loss = (pred - target) ** 2 - loss = sqr_loss.mean() - return loss - elif loss_type == "binary": - loss = nn.BCELoss() - return loss(pred, target) - else: - raise NotImplementedError("loss {} is not supported!".format(loss_type)) + def mse(self, pred, label): + loss = (pred - label) ** 2 + return torch.mean(loss) + + def loss_fn(self, pred, label): + mask = ~torch.isnan(label) + if self.loss == "mse": + return self.mse(pred[mask], label[mask]) + + raise ValueError("unknown loss `%s`" % self.loss) + + def metric_fn(self, pred, label): + + mask = torch.isfinite(label) + if self.metric == "IC": + return self.cal_ic(pred[mask], label[mask]) + + if self.metric == "" or self.metric == "loss": # use loss + return -self.loss_fn(pred[mask], label[mask]) + + raise ValueError("unknown metric `%s`" % self.metric) + + def cal_ic(self, pred, label): + return torch.mean(pred * label) def predict(self, dataset): if not self._fitted: raise ValueError("model is not fitted yet!") @@ -430,7 +447,7 @@ class SFM(Model): sample_num = x_values.shape[0] preds = [] - for begin in range(sample_num)[:: self.batch_size]: + for begin in range(sample_num)[::self.batch_size]: if sample_num - begin < self.batch_size: end = sample_num else: @@ -440,37 +457,16 @@ class SFM(Model): if self.device != "cpu": x_batch = x_batch.to(self.device) - + with torch.no_grad(): - if self.device != "cpu": - pred = self.sfm_model(x_batch).detach().cpu().numpy() - else: - pred = self.sfm_model(x_batch).detach().cpu().numpy() + pred = self.sfm_model(x_batch).detach().cpu().numpy() + preds.append(pred) - + return pd.Series(np.concatenate(preds), index=index) - def save(self, filename, **kwargs): - with save_multiple_parts_file(filename) as model_dir: - model_path = os.path.join(model_dir, os.path.split(model_dir)[-1]) - # Save model - torch.save(self.sfm_model.state_dict(), model_path) - - def load(self, buffer, **kwargs): - with unpack_archive_with_buffer(buffer) as model_dir: - # Get model name - _model_name = os.path.splitext(list(filter(lambda x: x.startswith("model.bin"), os.listdir(model_dir)))[0])[ - 0 - ] - _model_path = os.path.join(model_dir, _model_name) - # Load model - self.sfm_model.load_state_dict(torch.load(_model_path)) - self._fitted = True - - class AverageMeter(object): """Computes and stores the average and current value""" - def __init__(self): self.reset() From 1454bb2b0c7b710a57fcb1af113d6ec1815a8cd7 Mon Sep 17 00:00:00 2001 From: Haoyu Wang <48108414+javaThonc@users.noreply.github.com> Date: Thu, 26 Nov 2020 14:36:43 +0800 Subject: [PATCH 163/241] update format --- examples/workflow_by_code_sfm.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/examples/workflow_by_code_sfm.py b/examples/workflow_by_code_sfm.py index 4de79e075..b9ae61ad3 100644 --- a/examples/workflow_by_code_sfm.py +++ b/examples/workflow_by_code_sfm.py @@ -72,8 +72,8 @@ if __name__ == "__main__": "kwargs": { "d_feat": 6, "hidden_size": 64, - "output_dim" : 32, - "freq_dim" : 25, + "output_dim": 32, + "freq_dim": 25, "dropout_W": 0.5, "dropout_U": 0.5, "n_epochs": 15, @@ -83,9 +83,9 @@ if __name__ == "__main__": "early_stop": 20, "eval_steps": 5, "loss": "mse", - "lr_decay" : 0.96, - "lr_decay_steps" : 100, - "optimizer" : "adam", + "lr_decay": 0.96, + "lr_decay_steps": 100, + "optimizer": "adam", "GPU": 3, "seed": 710, }, From 191691b61cae9a1713b2086b80f059adbee6f8c1 Mon Sep 17 00:00:00 2001 From: Haoyu Wang <48108414+javaThonc@users.noreply.github.com> Date: Thu, 26 Nov 2020 14:49:24 +0800 Subject: [PATCH 164/241] Update workflow_by_code_sfm.py --- examples/workflow_by_code_sfm.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/workflow_by_code_sfm.py b/examples/workflow_by_code_sfm.py index b9ae61ad3..e9a72883a 100644 --- a/examples/workflow_by_code_sfm.py +++ b/examples/workflow_by_code_sfm.py @@ -77,7 +77,7 @@ if __name__ == "__main__": "dropout_W": 0.5, "dropout_U": 0.5, "n_epochs": 15, - "lr": 1e-2, + "lr": 1e-3, "metric": "", "batch_size": 1600, "early_stop": 20, From 96e8ce920af4d35fc1e1a676dc87cff7a02586f1 Mon Sep 17 00:00:00 2001 From: minho Date: Thu, 26 Nov 2020 14:51:43 +0800 Subject: [PATCH 165/241] Add files via upload Add README --- examples/benchmarks/ALSTM/README.md | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 examples/benchmarks/ALSTM/README.md diff --git a/examples/benchmarks/ALSTM/README.md b/examples/benchmarks/ALSTM/README.md new file mode 100644 index 000000000..cd9dd3493 --- /dev/null +++ b/examples/benchmarks/ALSTM/README.md @@ -0,0 +1,10 @@ +# ALSTM + +- ALSTM contains a temporal attentive aggregation layer based on normal LSTM. + +- The code used in Qlib is a pyTorch implementation of Code: https://github.com/fulifeng/Adv-ALSTM + +- Paper: A dual-stage attention-based recurrent neural network for time series prediction. + + https://www.ijcai.org/Proceedings/2017/0366.pdf + From e04bebde62688bd4e12c6c264dfbcae7de8150cc Mon Sep 17 00:00:00 2001 From: meng-ustc Date: Thu, 26 Nov 2020 15:15:05 +0800 Subject: [PATCH 166/241] Update to hats --- examples/workflow_by_code_hats.py | 13 ++---- qlib/contrib/model/pytorch_hats.py | 66 ++++++++++-------------------- 2 files changed, 25 insertions(+), 54 deletions(-) diff --git a/examples/workflow_by_code_hats.py b/examples/workflow_by_code_hats.py index 3ea81ba49..4476a1f32 100644 --- a/examples/workflow_by_code_hats.py +++ b/examples/workflow_by_code_hats.py @@ -3,24 +3,17 @@ import sys from pathlib import Path - import qlib import pandas as pd from qlib.config import REG_CN -from qlib.contrib.model.pytorch_hats import HATS -from qlib.contrib.data.handler import ALPHA360_Denoise 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 - -# from qlib.model.learner import train_model from qlib.utils import init_instance_by_config -import pickle - if __name__ == "__main__": # use default data @@ -30,7 +23,7 @@ if __name__ == "__main__": sys.path.append(str(Path(__file__).resolve().parent.parent.joinpath("scripts"))) from get_data import GetData - GetData().qlib_data_cn(target_dir=provider_uri) + GetData().qlib_data(target_dir=provider_uri, region=REG_CN) qlib.init(provider_uri=provider_uri, region=REG_CN) @@ -74,7 +67,7 @@ if __name__ == "__main__": "loss": "mse", "base_model": "LSTM", "seed": 0, - "GPU": 0, + "GPU": "1", }, }, "dataset": { @@ -97,7 +90,7 @@ if __name__ == "__main__": # "record": ['SignalRecord', 'SigAnaRecord', 'PortAnaRecord'], } - # model = train_model(task) + model = init_instance_by_config(task["model"]) dataset = init_instance_by_config(task["dataset"]) model.fit(dataset, save_path="benchmarks/HATS/model_hat.pkl") diff --git a/qlib/contrib/model/pytorch_hats.py b/qlib/contrib/model/pytorch_hats.py index 593cef635..cdfae0284 100644 --- a/qlib/contrib/model/pytorch_hats.py +++ b/qlib/contrib/model/pytorch_hats.py @@ -18,10 +18,8 @@ import os import numpy as np import pandas as pd import copy -from sklearn.metrics import roc_auc_score, mean_squared_error -import logging -from ...utils import unpack_archive_with_buffer, save_multiple_parts_file, create_save_path, drop_nan_by_y_index -from ...log import get_module_logger, TimeInspector +from ...utils import create_save_path +from ...log import get_module_logger import torch import torch.nn as nn @@ -37,14 +35,10 @@ class HATS(Model): Parameters ---------- - input_dim : int - input dimension - output_dim : int - output dimension - layers : tuple - layer sizes - lr : float - learning rate + d_feat : int + input dimension for each time step + metric: str + the evaluate metric used in early stop optimizer : str optimizer name GPU : str @@ -87,7 +81,7 @@ class HATS(Model): self.optimizer = optimizer.lower() self.loss = loss self.base_model = base_model - self.with_pretrain = with_pretrain #### True if train HATS with pretrained base model + self.with_pretrain = with_pretrain self.visible_GPU = GPU self.use_gpu = torch.cuda.is_available() self.seed = seed @@ -106,7 +100,7 @@ class HATS(Model): "\noptimizer : {}" "\nloss_type : {}" "\nbase_model : {}" - "\nwith_pretrain : {}" ##### debug + "\nwith_pretrain : {}" "\nvisible_GPU : {}" "\nuse_GPU : {}" "\nseed : {}".format( @@ -122,17 +116,13 @@ class HATS(Model): optimizer.lower(), loss, base_model, - with_pretrain, ### debug + with_pretrain, GPU, self.use_gpu, seed, ) ) - if loss not in {"mse", "binary"}: - raise NotImplementedError("loss {} is not supported!".format(loss)) - self._scorer = mean_squared_error if loss == "mse" else roc_auc_score - self.HATS_model = HATSModel( d_feat=self.d_feat, hidden_size=self.hidden_size, @@ -167,7 +157,6 @@ class HATS(Model): raise ValueError("unknown loss `%s`" % self.loss) def metric_fn(self, pred, label): - mask = torch.isfinite(label) if self.metric == "IC": return self.cal_ic(pred[mask], label[mask]) @@ -212,7 +201,7 @@ class HATS(Model): def test_epoch(self, data_x, data_y): - # prepare training data + # prepare testing data x_values = data_x.values y_values = np.squeeze(data_y.values) @@ -222,7 +211,6 @@ class HATS(Model): losses = [] indices = np.arange(len(x_values)) - np.random.shuffle(indices) for i in range(len(indices))[:: self.batch_size]: @@ -263,7 +251,6 @@ class HATS(Model): if save_path == None: save_path = create_save_path(save_path) stop_steps = 0 - train_loss = 0 best_score = -np.inf best_epoch = 0 evals_result["train"] = [] @@ -271,31 +258,24 @@ class HATS(Model): # load pretrained base_model if self.with_pretrain: - self.logger.info("loading pretrained model...") + self.logger.info("Loading pretrained model...") if self.base_model == "LSTM": from ...contrib.model.pytorch_lstm import LSTMModel - pretrained_model = LSTMModel() pretrained_model.load_state_dict(torch.load("benchmarks/LSTM/model_lstm_csi300.pkl")) elif self.base_model == "GRU": from ...contrib.model.pytorch_gru import GRUModel - pretrained_model = GRUModel() pretrained_model.load_state_dict(torch.load("benchmarks/GRU/model_gru_csi300.pkl")) model_dict = self.HATS_model.state_dict() - - # filter unnecessary parameters pretrained_dict = {k: v for k, v in pretrained_model.state_dict().items() if k in model_dict} - # overwrite entries in the existing state dict model_dict.update(pretrained_dict) - # load the new state dict self.HATS_model.load_state_dict(model_dict) - self.logger.info("loading pretrained model Done...") + self.logger.info("Loading pretrained model Done...") # train self.logger.info("training...") self._fitted = True - # return for step in range(self.n_epochs): self.logger.info("Epoch%d:", step) @@ -447,20 +427,20 @@ class GraphAttention(nn.Module): self.softmax = nn.Softmax(dim=0) self.leakyrelu = nn.LeakyReLU() - def forward(self, features, nodes, mapping, rows): + def forward(self, features, nodes, mappings, rows): """ Parameters ---------- features : torch.Tensor An (n' x input_dim) tensor of input node features. - node_layers : list of numpy array - node_layers[i] is an array of the nodes in the ith layer of the + nodes : list of numpy array + nodes[i] is an array of the nodes in the ith layer of the computation graph. mappings : list of dictionary - mappings[i] is a dictionary mapping node v (labelled 0 to |V|-1) - in node_layers[i] to its position in node_layers[i]. For example, - if node_layers[i] = [2,5], then mappings[i][2] = 0 and + mappings[i] is a dictionary mappings node v (labelled 0 to |V|-1) + in nodes[i] to its position in nodes[i]. For example, + if nodes[i] = [2,5], then mappings[i][2] = 0 and mappings[i][5] = 1. rows : numpy array rows[i] is an array of neighbors of node i. @@ -471,9 +451,9 @@ class GraphAttention(nn.Module): """ nprime = features.shape[0] - rows = [np.array([mapping[v] for v in row], dtype=np.int64) for row in rows] + rows = [np.array([mappings[v] for v in row], dtype=np.int64) for row in rows] sum_degs = np.hstack(([0], np.cumsum([len(row) for row in rows]))) - mapped_nodes = [mapping[v] for v in nodes] + mapped_nodes = [mappings[v] for v in nodes] indices = torch.LongTensor([[v, c] for (v, row) in zip(mapped_nodes, rows) for c in row]).t() out = [] @@ -481,7 +461,7 @@ class GraphAttention(nn.Module): h = self.fcs[k](features) nbr_h = torch.cat(tuple([h[row] for row in rows]), dim=0) - self_h = torch.cat(tuple([h[mapping[nodes[i]]].repeat(len(row), 1) for (i, row) in enumerate(rows)]), dim=0) + self_h = torch.cat(tuple([h[mappings[nodes[i]]].repeat(len(row), 1) for (i, row) in enumerate(rows)]), dim=0) cat_h = torch.cat((self_h, nbr_h), dim=1) e = self.leakyrelu(self.a[k](cat_h)) @@ -496,13 +476,11 @@ class GraphAttention(nn.Module): return out + @staticmethod def cal_attention(x, y): - att_x = torch.mean(x, dim=1).reshape(-1, 1) att_y = torch.mean(y, dim=1).reshape(-1, 1) att = att_x.mm(torch.t(att_y)) - x_att = x.reshape(x.shape[0], 1, x.shape[1]).repeat(1, y.shape[0], 1) - y_att = y.reshape(1, y.shape[0], y.shape[1]).repeat(x.shape[0], 1, 1) return ( torch.mean( x.reshape(x.shape[0], 1, x.shape[1]).repeat(1, y.shape[0], 1) From 667f69ef8f2bc4e181be6d125ce181c72f82da7d Mon Sep 17 00:00:00 2001 From: meng-ustc Date: Thu, 26 Nov 2020 15:20:14 +0800 Subject: [PATCH 167/241] Update to hats --- examples/workflow_by_code_hats.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/workflow_by_code_hats.py b/examples/workflow_by_code_hats.py index 4476a1f32..67b917f17 100644 --- a/examples/workflow_by_code_hats.py +++ b/examples/workflow_by_code_hats.py @@ -67,7 +67,7 @@ if __name__ == "__main__": "loss": "mse", "base_model": "LSTM", "seed": 0, - "GPU": "1", + "GPU": "0", }, }, "dataset": { From a108f753d526877353a08a03a5309607391afc91 Mon Sep 17 00:00:00 2001 From: meng-ustc Date: Thu, 26 Nov 2020 15:22:34 +0800 Subject: [PATCH 168/241] Update to alstm --- examples/workflow_by_code_alstm.py | 9 +------- qlib/contrib/model/pytorch_alstm.py | 33 ++++++----------------------- 2 files changed, 8 insertions(+), 34 deletions(-) diff --git a/examples/workflow_by_code_alstm.py b/examples/workflow_by_code_alstm.py index eabce3b07..8fd9e3565 100644 --- a/examples/workflow_by_code_alstm.py +++ b/examples/workflow_by_code_alstm.py @@ -7,20 +7,14 @@ from pathlib import Path import qlib import pandas as pd from qlib.config import REG_CN -from qlib.contrib.model.pytorch_alstm import ALSTM -from qlib.contrib.data.handler import ALPHA360_Denoise 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 - -# from qlib.model.learner import train_model from qlib.utils import init_instance_by_config -import pickle - if __name__ == "__main__": # use default data @@ -73,7 +67,7 @@ if __name__ == "__main__": "metric": "IC", "loss": "mse", "seed": 0, - "GPU": 0, + "GPU": "0", "rnn_type": "GRU", }, }, @@ -97,7 +91,6 @@ if __name__ == "__main__": # "record": ['SignalRecord', 'SigAnaRecord', 'PortAnaRecord'], } - # model = train_model(task) model = init_instance_by_config(task["model"]) dataset = init_instance_by_config(task["dataset"]) model.fit(dataset) diff --git a/qlib/contrib/model/pytorch_alstm.py b/qlib/contrib/model/pytorch_alstm.py index bdf1e3ea0..065963e73 100644 --- a/qlib/contrib/model/pytorch_alstm.py +++ b/qlib/contrib/model/pytorch_alstm.py @@ -9,10 +9,8 @@ import os import numpy as np import pandas as pd import copy -from sklearn.metrics import roc_auc_score, mean_squared_error -import logging -from ...utils import unpack_archive_with_buffer, save_multiple_parts_file, create_save_path, drop_nan_by_y_index -from ...log import get_module_logger, TimeInspector +from ...utils import create_save_path +from ...log import get_module_logger import torch import torch.nn as nn @@ -28,14 +26,10 @@ class ALSTM(Model): Parameters ---------- - input_dim : int - input dimension - output_dim : int - output dimension - layers : tuple - layer sizes - lr : float - learning rate + d_feat : int + input dimension for each time step + metric: str + the evaluate metric used in early stop optimizer : str optimizer name GPU : str @@ -116,14 +110,9 @@ class ALSTM(Model): ) ) - if loss not in {"mse", "binary"}: - raise NotImplementedError("loss {} is not supported!".format(loss)) - self._scorer = mean_squared_error if loss == "mse" else roc_auc_score - self.alstm_model = ALSTMModel( d_feat=self.d_feat, hidden_size=self.hidden_size, num_layers=self.num_layers, dropout=self.dropout ) - # def __init__(self, d_feat=6, hidden_size=64, num_layers=2, dropout=0.0, input_day=20, rnn_type="GRU"): if optimizer.lower() == "adam": self.train_optimizer = optim.Adam(self.alstm_model.parameters(), lr=self.lr) @@ -152,7 +141,6 @@ class ALSTM(Model): raise ValueError("unknown loss `%s`" % self.loss) def metric_fn(self, pred, label): - mask = torch.isfinite(label) if self.metric == "IC": return self.cal_ic(pred[mask], label[mask]) @@ -197,7 +185,7 @@ class ALSTM(Model): def test_epoch(self, data_x, data_y): - # prepare training data + # prepare testing data x_values = data_x.values y_values = np.squeeze(data_y.values) @@ -207,7 +195,6 @@ class ALSTM(Model): losses = [] indices = np.arange(len(x_values)) - np.random.shuffle(indices) for i in range(len(indices))[:: self.batch_size]: @@ -248,7 +235,6 @@ class ALSTM(Model): if save_path == None: save_path = create_save_path(save_path) stop_steps = 0 - train_loss = 0 best_score = -np.inf best_epoch = 0 evals_result["train"] = [] @@ -257,7 +243,6 @@ class ALSTM(Model): # train self.logger.info("training...") self._fitted = True - # return for step in range(self.n_epochs): self.logger.info("Epoch%d:", step) @@ -334,11 +319,9 @@ class GRUModel(nn.Module): dropout=dropout, ) self.fc_out = nn.Linear(hidden_size, 1) - self.d_feat = d_feat def forward(self, x): - # x: [N, F*T] x = x.reshape(len(x), self.d_feat, -1) # [N, F, T] x = x.permute(0, 2, 1) # [N, T, F] out, _ = self.rnn(x) @@ -371,7 +354,6 @@ class ALSTMModel(nn.Module): dropout=self.dropout, ) self.fc_out = nn.Linear(in_features=self.hid_size * 2, out_features=1) - # self.fc_out = nn.Linear(in_features=self.hid_size, out_features=1) self.att_net = nn.Sequential() self.att_net.add_module("att_fc_in", nn.Linear(in_features=self.hid_size, out_features=int(self.hid_size / 2))) self.att_net.add_module("att_dropout", torch.nn.Dropout(self.dropout)) @@ -390,5 +372,4 @@ class ALSTMModel(nn.Module): out = self.fc_out( torch.cat((rnn_out[:, -1, :], out_att), dim=1) ) # [batch, seq_len, num_directions * hidden_size] -> [batch, 1] - # out = self.fc_out(rnn_out[:, -1, :] + out_att) return out[..., 0] From 056951605b6395c3765624a8ea82a4993e20c023 Mon Sep 17 00:00:00 2001 From: Jactus Date: Thu, 26 Nov 2020 15:50:42 +0800 Subject: [PATCH 169/241] Format --- docs/component/data.rst | 1 + examples/workflow_by_code_gats.py | 1 - examples/workflow_by_code_hats.py | 1 - examples/workflow_by_code_sfm.py | 2 +- qlib/contrib/model/catboost_model.py | 20 ++++---- qlib/contrib/model/pytorch_gats.py | 2 +- qlib/contrib/model/pytorch_hats.py | 6 ++- qlib/contrib/model/pytorch_sfm.py | 73 +++++++++++++++------------- qlib/contrib/model/xgboost.py | 30 ++++++------ 9 files changed, 72 insertions(+), 64 deletions(-) diff --git a/docs/component/data.rst b/docs/component/data.rst index efcd81ffd..fda3e0db0 100644 --- a/docs/component/data.rst +++ b/docs/component/data.rst @@ -292,6 +292,7 @@ The ``Processor`` module in ``Qlib`` is designed to be learnable and it is respo - ``Fillna``: `processor` that handles N/A values, which will fill the N/A value by 0 or other given number. - ``MinMaxNorm``: `processor` that applies min-max normalization. - ``ZscoreNorm``: `processor` that applies z-score normalization. +- ``RobustZScoreNorm``: `processor` that applies robust z-score normalization. - ``CSZScoreNorm``: `processor` that applies cross sectional z-score normalization. - ``CSRankNorm``: `processor` that applies cross sectional rank normalization. diff --git a/examples/workflow_by_code_gats.py b/examples/workflow_by_code_gats.py index b5bad31ec..ac413932b 100644 --- a/examples/workflow_by_code_gats.py +++ b/examples/workflow_by_code_gats.py @@ -17,7 +17,6 @@ from qlib.utils import exists_qlib_data from qlib.utils import init_instance_by_config - if __name__ == "__main__": # use default data diff --git a/examples/workflow_by_code_hats.py b/examples/workflow_by_code_hats.py index 67b917f17..15e5ae130 100644 --- a/examples/workflow_by_code_hats.py +++ b/examples/workflow_by_code_hats.py @@ -90,7 +90,6 @@ if __name__ == "__main__": # "record": ['SignalRecord', 'SigAnaRecord', 'PortAnaRecord'], } - model = init_instance_by_config(task["model"]) dataset = init_instance_by_config(task["dataset"]) model.fit(dataset, save_path="benchmarks/HATS/model_hat.pkl") diff --git a/examples/workflow_by_code_sfm.py b/examples/workflow_by_code_sfm.py index e9a72883a..5bd91ded8 100644 --- a/examples/workflow_by_code_sfm.py +++ b/examples/workflow_by_code_sfm.py @@ -78,7 +78,7 @@ if __name__ == "__main__": "dropout_U": 0.5, "n_epochs": 15, "lr": 1e-3, - "metric": "", + "metric": "", "batch_size": 1600, "early_stop": 20, "eval_steps": 5, diff --git a/qlib/contrib/model/catboost_model.py b/qlib/contrib/model/catboost_model.py index eb97fc75b..bba006c35 100644 --- a/qlib/contrib/model/catboost_model.py +++ b/qlib/contrib/model/catboost_model.py @@ -34,14 +34,14 @@ class CatBoostModel(Model): def fit( self, dataset: DatasetH, - num_boost_round = 1000, - early_stopping_rounds = 50, - verbose_eval = 20, - evals_result = dict(), + num_boost_round=1000, + early_stopping_rounds=50, + verbose_eval=20, + evals_result=dict(), **kwargs ): df_train, df_valid = dataset.prepare( - ["train", "valid"], col_set = ["feature", "label"], data_key = DataHandlerLP.DK_L + ["train", "valid"], col_set=["feature", "label"], data_key=DataHandlerLP.DK_L ) x_train, y_train = df_train["feature"], df_train["label"] x_valid, y_valid = df_valid["feature"], df_valid["label"] @@ -52,8 +52,8 @@ class CatBoostModel(Model): else: raise ValueError("CatBoost doesn't support multi-label training") - train_pool = Pool(data = x_train, label = y_train_1d) - valid_pool = Pool(data = x_valid, label = y_valid_1d) + train_pool = Pool(data=x_train, label=y_train_1d) + valid_pool = Pool(data=x_valid, label=y_valid_1d) # Initialize the catboost model self._params["iterations"] = num_boost_round @@ -63,7 +63,7 @@ class CatBoostModel(Model): self.model = CatBoost(self._params, **kwargs) # train the model - self.model.fit(train_pool, eval_set = valid_pool, use_best_model = True, **kwargs) + self.model.fit(train_pool, eval_set=valid_pool, use_best_model=True, **kwargs) evals_result = self.model.get_evals_result() evals_result["train"] = list(evals_result["learn"].values())[0] @@ -72,8 +72,8 @@ class CatBoostModel(Model): def predict(self, dataset): if self.model is None: raise ValueError("model is not fitted yet!") - x_test = dataset.prepare("test", col_set = "feature") - return pd.Series(self.model.predict(x_test.values), index = x_test.index) + x_test = dataset.prepare("test", col_set="feature") + return pd.Series(self.model.predict(x_test.values), index=x_test.index) if __name__ == "__main__": diff --git a/qlib/contrib/model/pytorch_gats.py b/qlib/contrib/model/pytorch_gats.py index 7cdfb571a..72cd5c36f 100755 --- a/qlib/contrib/model/pytorch_gats.py +++ b/qlib/contrib/model/pytorch_gats.py @@ -117,7 +117,7 @@ class GAT(Model): seed, ) ) - + self.GAT_model = GATModel( d_feat=self.d_feat, hidden_size=self.hidden_size, diff --git a/qlib/contrib/model/pytorch_hats.py b/qlib/contrib/model/pytorch_hats.py index cdfae0284..05f89ced0 100644 --- a/qlib/contrib/model/pytorch_hats.py +++ b/qlib/contrib/model/pytorch_hats.py @@ -261,10 +261,12 @@ class HATS(Model): self.logger.info("Loading pretrained model...") if self.base_model == "LSTM": from ...contrib.model.pytorch_lstm import LSTMModel + pretrained_model = LSTMModel() pretrained_model.load_state_dict(torch.load("benchmarks/LSTM/model_lstm_csi300.pkl")) elif self.base_model == "GRU": from ...contrib.model.pytorch_gru import GRUModel + pretrained_model = GRUModel() pretrained_model.load_state_dict(torch.load("benchmarks/GRU/model_gru_csi300.pkl")) model_dict = self.HATS_model.state_dict() @@ -461,7 +463,9 @@ class GraphAttention(nn.Module): h = self.fcs[k](features) nbr_h = torch.cat(tuple([h[row] for row in rows]), dim=0) - self_h = torch.cat(tuple([h[mappings[nodes[i]]].repeat(len(row), 1) for (i, row) in enumerate(rows)]), dim=0) + self_h = torch.cat( + tuple([h[mappings[nodes[i]]].repeat(len(row), 1) for (i, row) in enumerate(rows)]), dim=0 + ) cat_h = torch.cat((self_h, nbr_h), dim=1) e = self.leakyrelu(self.a[k](cat_h)) diff --git a/qlib/contrib/model/pytorch_sfm.py b/qlib/contrib/model/pytorch_sfm.py index 4ec61430e..7fbbd7c6e 100644 --- a/qlib/contrib/model/pytorch_sfm.py +++ b/qlib/contrib/model/pytorch_sfm.py @@ -31,6 +31,7 @@ from ...model.base import Model from ...data.dataset import DatasetH from ...data.dataset.handler import DataHandlerLP + class SFM_Model(nn.Module): def __init__(self, d_feat=6, output_dim=1, freq_dim=10, hidden_size=64, dropout_W=0.0, dropout_U=0.0, device="cpu"): super().__init__() @@ -75,13 +76,13 @@ class SFM_Model(nn.Module): self.states = [] def forward(self, input): - input = input.reshape(len(input), self.input_dim, -1) # [N, F, T] - input = input.permute(0, 2, 1) # [N, T, F] + input = input.reshape(len(input), self.input_dim, -1) # [N, F, T] + input = input.permute(0, 2, 1) # [N, T, F] time_step = input.shape[1] - + for ts in range(time_step): - x = input[:, ts,:] - if len(self.states)==0: #hasn't initialized yet + x = input[:, ts, :] + if len(self.states) == 0: # hasn't initialized yet self.init_states(x) self.get_constants(x) p_tm1 = self.states[0] @@ -98,64 +99,65 @@ class SFM_Model(nn.Module): x_fre = torch.matmul(x * B_W[0], self.W_fre) + self.b_fre x_c = torch.matmul(x * B_W[0], self.W_c) + self.b_c x_o = torch.matmul(x * B_W[0], self.W_o) + self.b_o - - i = self.inner_activation(x_i + torch.matmul(h_tm1 * B_U[0], self.U_i)) # not sure whether I am doing in the right unsquuze - + + i = self.inner_activation( + x_i + torch.matmul(h_tm1 * B_U[0], self.U_i) + ) # not sure whether I am doing in the right unsquuze ste = self.inner_activation(x_ste + torch.matmul(h_tm1 * B_U[0], self.U_ste)) fre = self.inner_activation(x_fre + torch.matmul(h_tm1 * B_U[0], self.U_fre)) ste = torch.reshape(ste, (-1, self.hidden_dim, 1)) fre = torch.reshape(fre, (-1, 1, self.freq_dim)) - + f = ste * fre - + c = i * self.activation(x_c + torch.matmul(h_tm1 * B_U[0], self.U_c)) time = time_tm1 + 1 omega = torch.tensor(2 * np.pi) * time * frequency - re = torch.cos(omega) + re = torch.cos(omega) im = torch.sin(omega) - + c = torch.reshape(c, (-1, self.hidden_dim, 1)) S_re = f * S_re_tm1 + c * re S_im = f * S_im_tm1 + c * im - + A = torch.square(S_re) + torch.square(S_im) A = torch.reshape(A, (-1, self.freq_dim)).float() A_a = torch.matmul(A * B_U[0], self.U_a) A_a = torch.reshape(A_a, (-1, self.hidden_dim)) a = self.activation(A_a + self.b_a) - + o = self.inner_activation(x_o + torch.matmul(h_tm1 * B_U[0], self.U_o)) h = o * a p = torch.matmul(h, self.W_p) + self.b_p self.states = [p, h, S_re, S_im, time, None, None, None] - self.states = [] + self.states = [] return self.fc_out(p).squeeze() def init_states(self, x): reducer_f = torch.zeros((self.hidden_dim, self.freq_dim)).to(self.device) reducer_p = torch.zeros((self.hidden_dim, self.output_dim)).to(self.device) - + init_state_h = torch.zeros(self.hidden_dim).to(self.device) init_state_p = torch.matmul(init_state_h, reducer_p) - + init_state = torch.zeros_like(init_state_h).to(self.device) init_freq = torch.matmul(init_state_h, reducer_f) init_state = torch.reshape(init_state, (-1, self.hidden_dim, 1)) init_freq = torch.reshape(init_freq, (-1, 1, self.freq_dim)) - + init_state_S_re = init_state * init_freq init_state_S_im = init_state * init_freq - + init_state_time = torch.tensor(0).to(self.device) self.states = [init_state_p, init_state_h, init_state_S_re, init_state_S_im, init_state_time, None, None, None] @@ -201,7 +203,7 @@ class SFM(Model): dropout_U=0.0, n_epochs=200, lr=0.001, - metric = "", + metric="", batch_size=2000, early_stop=20, eval_steps=5, @@ -234,7 +236,7 @@ class SFM(Model): self.lr_decay_steps = lr_decay_steps self.optimizer = optimizer.lower() self.loss = loss - self.device = "cuda:%d"%(GPU) if torch.cuda.is_available() else "cpu" + self.device = "cuda:%d" % (GPU) if torch.cuda.is_available() else "cpu" self.use_gpu = torch.cuda.is_available() self.seed = seed @@ -243,7 +245,7 @@ class SFM(Model): "\nd_feat : {}" "\nhidden_size : {}" "\noutput_size : {}" - "\nfrequency_dimension : {}" + "\nfrequency_dimension : {}" "\ndropout_W: {}" "\ndropout_U: {}" "\nn_epochs : {}" @@ -286,14 +288,14 @@ class SFM(Model): self._scorer = mean_squared_error if loss == "mse" else roc_auc_score self.sfm_model = SFM_Model( - d_feat=self.d_feat, + d_feat=self.d_feat, output_dim=self.output_dim, - hidden_size=self.hidden_size, - freq_dim=self.freq_dim, - dropout_W=self.dropout_W, - dropout_U=self.dropout_U, - device=self.device - ) + hidden_size=self.hidden_size, + freq_dim=self.freq_dim, + dropout_W=self.dropout_W, + dropout_U=self.dropout_U, + device=self.device, + ) if optimizer.lower() == "adam": self.train_optimizer = optim.Adam(self.sfm_model.parameters(), lr=self.lr) elif optimizer.lower() == "gd": @@ -414,7 +416,7 @@ class SFM(Model): def mse(self, pred, label): loss = (pred - label) ** 2 return torch.mean(loss) - + def loss_fn(self, pred, label): mask = ~torch.isnan(label) @@ -422,7 +424,7 @@ class SFM(Model): return self.mse(pred[mask], label[mask]) raise ValueError("unknown loss `%s`" % self.loss) - + def metric_fn(self, pred, label): mask = torch.isfinite(label) @@ -436,6 +438,7 @@ class SFM(Model): def cal_ic(self, pred, label): return torch.mean(pred * label) + def predict(self, dataset): if not self._fitted: raise ValueError("model is not fitted yet!") @@ -447,7 +450,7 @@ class SFM(Model): sample_num = x_values.shape[0] preds = [] - for begin in range(sample_num)[::self.batch_size]: + for begin in range(sample_num)[:: self.batch_size]: if sample_num - begin < self.batch_size: end = sample_num else: @@ -457,16 +460,18 @@ class SFM(Model): if self.device != "cpu": x_batch = x_batch.to(self.device) - + with torch.no_grad(): pred = self.sfm_model(x_batch).detach().cpu().numpy() preds.append(pred) - + return pd.Series(np.concatenate(preds), index=index) + class AverageMeter(object): """Computes and stores the average and current value""" + def __init__(self): self.reset() diff --git a/qlib/contrib/model/xgboost.py b/qlib/contrib/model/xgboost.py index 203e71b9a..039fd2c80 100755 --- a/qlib/contrib/model/xgboost.py +++ b/qlib/contrib/model/xgboost.py @@ -30,15 +30,15 @@ class XGBModel(Model): def fit( self, dataset: DatasetH, - num_boost_round = 1000, - early_stopping_rounds = 50, - verbose_eval = 20, - evals_result = dict(), + num_boost_round=1000, + early_stopping_rounds=50, + verbose_eval=20, + evals_result=dict(), **kwargs ): df_train, df_valid = dataset.prepare( - ["train", "valid"], col_set = ["feature", "label"], data_key = DataHandlerLP.DK_L + ["train", "valid"], col_set=["feature", "label"], data_key=DataHandlerLP.DK_L ) x_train, y_train = df_train["feature"], df_train["label"] x_valid, y_valid = df_valid["feature"], df_valid["label"] @@ -49,16 +49,16 @@ class XGBModel(Model): else: raise ValueError("XGBoost doesn't support multi-label training") - dtrain = xgb.DMatrix(x_train.values, label = y_train_1d) - dvalid = xgb.DMatrix(x_valid.values, label = y_valid_1d) + dtrain = xgb.DMatrix(x_train.values, label=y_train_1d) + dvalid = xgb.DMatrix(x_valid.values, label=y_valid_1d) self.model = xgb.train( self._params, - dtrain = dtrain, - num_boost_round = num_boost_round, - evals = [(dtrain, "train"), (dvalid, "valid")], - early_stopping_rounds = early_stopping_rounds, - verbose_eval = verbose_eval, - evals_result = evals_result, + dtrain=dtrain, + num_boost_round=num_boost_round, + evals=[(dtrain, "train"), (dvalid, "valid")], + early_stopping_rounds=early_stopping_rounds, + verbose_eval=verbose_eval, + evals_result=evals_result, **kwargs ) evals_result["train"] = list(evals_result["train"].values())[0] @@ -67,5 +67,5 @@ class XGBModel(Model): def predict(self, dataset): if self.model is None: raise ValueError("model is not fitted yet!") - x_test = dataset.prepare("test", col_set = "feature") - return pd.Series(self.model.predict(xgb.DMatrix(x_test.values)), index = x_test.index) + x_test = dataset.prepare("test", col_set="feature") + return pd.Series(self.model.predict(xgb.DMatrix(x_test.values)), index=x_test.index) From f7e375ae6803f8f408fd14721530a2daabd99220 Mon Sep 17 00:00:00 2001 From: Haoyu Wang <48108414+javaThonc@users.noreply.github.com> Date: Thu, 26 Nov 2020 16:08:21 +0800 Subject: [PATCH 170/241] Update workflow_config_sfm.yaml --- examples/benchmarks/SFM/workflow_config_sfm.yaml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/examples/benchmarks/SFM/workflow_config_sfm.yaml b/examples/benchmarks/SFM/workflow_config_sfm.yaml index 743c6220f..04f796150 100644 --- a/examples/benchmarks/SFM/workflow_config_sfm.yaml +++ b/examples/benchmarks/SFM/workflow_config_sfm.yaml @@ -31,21 +31,21 @@ task: kwargs: d_feat: 6 hidden_size: 64 - output_dim: 1 - freq_dim: 20 + output_dim: 32 + freq_dim: 25 dropout_W: 0.5 dropout_U: 0.5 - n_epochs: 10 + n_epochs: 20 lr: 1e-3 - batch_size: 800 + batch_size: 1600 early_stop: 20 eval_steps: 5 loss: mse lr_decay: 0.96 lr_decay_steps: 100 - optimizer: gd + optimizer: adam GPU: 1 - seed: 0 + seed: 710 dataset: class: DatasetH module_path: qlib.data.dataset From e2935f214a94211d05600318bdbbdb92489412a1 Mon Sep 17 00:00:00 2001 From: Young Date: Thu, 26 Nov 2020 08:42:54 +0000 Subject: [PATCH 171/241] fix some typo and bug --- docs/component/report.rst | 4 ++-- docs/index.rst | 2 +- docs/introduction/quick.rst | 2 +- examples/workflow_by_code.ipynb | 7 ++++--- qlib/data/dataset/handler.py | 4 ++-- qlib/workflow/cli.py | 2 +- 6 files changed, 11 insertions(+), 10 deletions(-) diff --git a/docs/component/report.rst b/docs/component/report.rst index 8ea3d7abe..7d8053c78 100644 --- a/docs/component/report.rst +++ b/docs/component/report.rst @@ -1,13 +1,13 @@ .. _report: ========================================== -Aanalysis: Evaluation & Results Analysis +Analysis: Evaluation & Results Analysis ========================================== Introduction =================== -``Aanalysis`` is designed to show the graphical reports of ``Intraday Trading`` , which helps users to evaluate and analyse investment portfolios visually. The following are some graphics to view: +``Analysis`` is designed to show the graphical reports of ``Intraday Trading`` , which helps users to evaluate and analyse investment portfolios visually. The following are some graphics to view: - analysis_position - report_graph diff --git a/docs/index.rst b/docs/index.rst index 3a7358288..1e43cf99e 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -41,7 +41,7 @@ Document Structure Interday Strategy: Portfolio Management Intraday Trading: Model&Strategy Testing Qlib Recorder: Experiment Management - Aanalysis: Evaluation & Results Analysis + Analysis: Evaluation & Results Analysis .. toctree:: :maxdepth: 3 diff --git a/docs/introduction/quick.rst b/docs/introduction/quick.rst index a367e2dde..32752fd83 100644 --- a/docs/introduction/quick.rst +++ b/docs/introduction/quick.rst @@ -84,7 +84,7 @@ Auto Quant Research Workflow - Run ``examples/workflow_by_code.ipynb`` with jupyter notebook Users can have portfolio analysis or prediction score (model prediction) analysis by run ``examples/workflow_by_code.ipynb``. - Graphical Reports - Users can get graphical reports about the analysis, please refer to `Aanalysis: Evaluation & Results Analysis <../component/report.html>`_ for more details. + Users can get graphical reports about the analysis, please refer to `Analysis: Evaluation & Results Analysis <../component/report.html>`_ for more details. diff --git a/examples/workflow_by_code.ipynb b/examples/workflow_by_code.ipynb index 1b4183b29..692e52078 100644 --- a/examples/workflow_by_code.ipynb +++ b/examples/workflow_by_code.ipynb @@ -31,7 +31,8 @@ ")\n", "from qlib.utils import exists_qlib_data, init_instance_by_config\n", "from qlib.workflow import R\n", - "from qlib.workflow.record_temp import SignalRecord, PortAnaRecord" + "from qlib.workflow.record_temp import SignalRecord, PortAnaRecord\n", + "from qlib.utils import flatten_dict" ] }, { @@ -129,7 +130,7 @@ "\n", "# start exp to train model\n", "with R.start(experiment_name=\"train_model\"):\n", - " R.log_paramters(**flatten_dict(task))\n", + " R.log_params(**flatten_dict(task))\n", " model.fit(dataset)\n", " R.save_objects(trained_model=model)\n", " rid = R.get_recorder().id\n" @@ -337,4 +338,4 @@ }, "nbformat": 4, "nbformat_minor": 4 -} \ No newline at end of file +} diff --git a/qlib/data/dataset/handler.py b/qlib/data/dataset/handler.py index 4d3d88c38..825d7e76e 100644 --- a/qlib/data/dataset/handler.py +++ b/qlib/data/dataset/handler.py @@ -243,10 +243,10 @@ class DataHandlerLP(DataHandler): # process type PTYPE_I = "independent" - # - self._infer will processed by infer_processors + # - self._infer will be processed by infer_processors # - self._learn will be processed by learn_processors PTYPE_A = "append" - # - self._infer will processed by infer_processors + # - self._infer will be processed by infer_processors # - self._learn will be processed by infer_processors + learn_processors # - (e.g. self._infer processed by learn_processors ) diff --git a/qlib/workflow/cli.py b/qlib/workflow/cli.py index 2e087877b..7562daaca 100644 --- a/qlib/workflow/cli.py +++ b/qlib/workflow/cli.py @@ -29,7 +29,7 @@ def sys_config(config, config_path): config : dict configuration of the workflow config_path : str - configuration of the path + path of the configuration """ sys_config = config.get("sys", {}) From 7d092f39c83e01886d4b1f4d98a9242e5c6d50ee Mon Sep 17 00:00:00 2001 From: Young Date: Thu, 26 Nov 2020 09:15:13 +0000 Subject: [PATCH 172/241] data.rst update --- docs/component/data.rst | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/docs/component/data.rst b/docs/component/data.rst index fda3e0db0..22565c39d 100644 --- a/docs/component/data.rst +++ b/docs/component/data.rst @@ -143,7 +143,7 @@ Filter Expression dynamic instrument filter. Filter the instruments based on a certain expression. An expression rule indicating a certain feature field is required. - `basic features filter`: rule_expression = '$close/$open>5' - - `cross-sectional features filter` : rule_expression = '$rank($close)<10' + - `cross-sectional features filter` \: rule_expression = '$rank($close)<10' - `time-sequence features filter`: rule_expression = '$Ref($close, 3)>100' To know more about ``Filter``, please refer to `Filter API <../reference/api.html#module-qlib.data.filter>`_. @@ -169,11 +169,11 @@ Here are some interfaces of the ``QlibDataLoader`` class: - `load(instruments, start_time=None, end_time=None)` - This method loads the data as pd.DataFrame - Parameters: - - `instruments` : str or dict + - `instruments` \: str or dict it can either be the market name or the config file of instruments generated by InstrumentProvider. - - `start_time` : str + - `start_time` \: str start of the time range. - - `end_time` : str + - `end_time` \: str end of the time range. - Returns: - The data being loaded with type `pd.DataFrame` @@ -181,15 +181,15 @@ Here are some interfaces of the ``QlibDataLoader`` class: - `load_group_df(instruments, exprs: list, names: list, start_time=None, end_time=None)` - This method loads the dataframe for specific group. - Parameters: - - `instruments` : str or dict + - `instruments` \: str or dict it can either be the market name or the config file of instruments generated by InstrumentProvider. - - `exprs` : list + - `exprs` \: list the expressions to describe the content of the data. - - `names` : list + - `names` \: list the name of the data. - - `start_time` : str + - `start_time` \: str start of the time range. - - `end_time` : str + - `end_time` \: str end of the time range. - Returns: - The queried data in type `pd.DataFrame`. @@ -220,7 +220,7 @@ Here are some important interfaces that ``DataHandlerLP`` provides: - `__init__(instruments=None, start_time=None, end_time=None, data_loader: Tuple[dict, str, DataLoader] = None, infer_processors=[], learn_processors=[], process_type=PTYPE_A, **kwargs)` - Initialization of the class. - Parameters: - - `infer_processors` : list + - `infer_processors` \: list - list of of processors to generate data for inference - example of : @@ -238,7 +238,7 @@ Here are some important interfaces that ``DataHandlerLP`` provides: "DropnaFeature" 3) object instance of Processor - - `learn_processors` : list + - `learn_processors` \: list similar to infer_processors, but for generating data for learning models - `process_type`: str @@ -253,13 +253,13 @@ Here are some important interfaces that ``DataHandlerLP`` provides: - `fetch(selector: Union[pd.Timestamp, slice, str] = slice(None, None), level: Union[str, int] = "datetime", col_set=DataHandler.CS_ALL, data_key: str = DK_I)` - This method fetches data from underlying data source - Parameters: - - `selector` : Union[pd.Timestamp, slice, str] + - `selector` \: Union[pd.Timestamp, slice, str] describe how to select data by index. - - `level` : Union[str, int] + - `level` \: Union[str, int] which index level to select the data. - - `col_set` : str + - `col_set` \: str select a set of meaningful columns.(e.g. features, columns). - - `data_key` : str + - `data_key` \: str The data to fetch: DK_*. - Returns: - The retrieved results in the type: `pd.DataFrame`. @@ -267,9 +267,9 @@ Here are some important interfaces that ``DataHandlerLP`` provides: - `get_cols(col_set=DataHandler.CS_ALL, data_key: str = DK_I)` - This method gets the column names. - Parameters: - - `col_set` : str + - `col_set` \: str select a set of meaningful columns.(e.g. features, columns). - - `data_key` : str + - `data_key` \: str the data to fetch: DK_*. - Returns: - A list of column names. @@ -356,7 +356,7 @@ The ``DatasetH`` class is the `dataset` with `Data Handler`. Here is the most im - `prepare(segments: Union[List[str], Tuple[str], str, slice], col_set=DataHandler.CS_ALL, data_key=DataHandlerLP.DK_I, **kwargs)` - This method prepares the data for learning and inference. - Parameters: - - `segments` : Union[List[str], Tuple[str], str, slice] + - `segments` \: Union[List[str], Tuple[str], str, slice] Describe the scope of the data to be prepared Here are some examples: @@ -364,9 +364,9 @@ The ``DatasetH`` class is the `dataset` with `Data Handler`. Here is the most im - ['train', 'valid'] - - `col_set` : str + - `col_set` \: str The col_set will be passed to self._handler when fetching data. - - `data_key` : str + - `data_key` \: str The data to fetch: DK_* Default is DK_I, which indicate fetching data for **inference**. From 36b11676bf25245e136d8f09565c3621ee4b2825 Mon Sep 17 00:00:00 2001 From: zhupr Date: Thu, 26 Nov 2020 18:57:38 +0800 Subject: [PATCH 173/241] Update graph --- README.md | 18 +++++++++--------- .../img/analysis/analysis_model_IC.png | Bin 40935 -> 33497 bytes .../img/analysis/analysis_model_NDQ.png | Bin 24122 -> 23728 bytes .../analysis_model_auto_correlation.png | Bin 52763 -> 48415 bytes .../analysis_model_cumulative_return.png | Bin 67775 -> 64521 bytes .../analysis/analysis_model_long_short.png | Bin 17493 -> 16704 bytes .../analysis/analysis_model_monthly_IC.png | Bin 18052 -> 16922 bytes docs/_static/img/analysis/report.png | Bin 167121 -> 164378 bytes .../risk_analysis_annualized_return.png | Bin 53840 -> 46842 bytes .../img/analysis/risk_analysis_bar.png | Bin 15013 -> 12926 bytes .../risk_analysis_information_ratio.png | Bin 57493 -> 55540 bytes .../analysis/risk_analysis_max_drawdown.png | Bin 58177 -> 54377 bytes .../img/analysis/risk_analysis_std.png | Bin 48011 -> 48396 bytes docs/_static/img/analysis/score_ic.png | Bin 107436 -> 104343 bytes docs/_static/img/framework.png | Bin 209724 -> 277395 bytes 15 files changed, 9 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index cd0c8542f..7fd86028e 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@

- +

@@ -39,7 +39,7 @@ For more details, please refer to our paper ["Qlib: An AI-oriented Quantitative # Framework of Qlib
- +
@@ -159,19 +159,19 @@ Qlib provides a tool named `qrun` to run the whole workflow automatically (inclu 2. Graphical Reports Analysis: Run `examples/workflow_by_code.ipynb` with `jupyter notebook` to get graphical reports - Forecasting signal (model prediction) analysis - Cumulative Return of groups - ![Cumulative Return](http://fintech.msra.cn/images/analysis/analysis_model_cumulative_return.png?v=0.1) + ![Cumulative Return](http://fintech.msra.cn/images_v060/analysis/analysis_model_cumulative_return.png?v=0.1) - Return distribution - ![long_short](http://fintech.msra.cn/images/analysis/analysis_model_long_short.png?v=0.1) + ![long_short](http://fintech.msra.cn/images_v060/analysis/analysis_model_long_short.png?v=0.1) - Information Coefficient (IC) - ![Information Coefficient](http://fintech.msra.cn/images/analysis/analysis_model_IC.png?v=0.1) - ![Monthly IC](http://fintech.msra.cn/images/analysis/analysis_model_monthly_IC.png?v=0.1) - ![IC](http://fintech.msra.cn/images/analysis/analysis_model_NDQ.png?v=0.1) + ![Information Coefficient](http://fintech.msra.cn/images_v060/analysis/analysis_model_IC.png?v=0.1) + ![Monthly IC](http://fintech.msra.cn/images_v060/analysis/analysis_model_monthly_IC.png?v=0.1) + ![IC](http://fintech.msra.cn/images_v060/analysis/analysis_model_NDQ.png?v=0.1) - Auto Correlation of forecasting signal (model prediction) - ![Auto Correlation](http://fintech.msra.cn/images/analysis/analysis_model_auto_correlation.png?v=0.1) + ![Auto Correlation](http://fintech.msra.cn/images_v060/analysis/analysis_model_auto_correlation.png?v=0.1) - Portfolio analysis - Backtest return - ![Report](http://fintech.msra.cn/images/analysis/report.png?v=0.1) + ![Report](http://fintech.msra.cn/images_v060/analysis/report.png?v=0.1) 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ÛQdZMDi|d8QM5){JLoN6^D_ju_>L zEZdR$cyWxs5jV}5%wpF+3C}bohW$DZ&8#b|mJ@h%8Km{Oc_0fZ|E39b$RA-VSd}q- z$T2}HkhmH2>RLnjZZrOAd3DixbHD=4uvvm+9{!ifJ`z&0AOY@*mIsK*U+$5!%iEc@ z;64?80tVrT;^}Ulz;4@CFG6{FFnJ8KH3=z=@0;ZB;znT=1gUn$Nn7F!Daz5)#~bo_ zBzj|ZUfwEu7Uh@q%dY@s~9(N_eqa$%$#|k}WZ8)5kz6h?<%+*LYD9!RcT)SH3 zozOL*67-6--GzguZQ)Bp&+gaUcxS<`*urk6{)u(Kx&bckPnAP9Yk-~P<4nVPGVNaE8x-oL;pNA!Vzx83)J|@pUtG)wCyP$;?VW zS$I!i!xN#u9GSnoHDJK;^ax6q#$@fhH}H<_OW(POTRTtp zUOi-SxaqJJOgyzr@~>J9V!_tm``BmucB`WU9y{dC$elE&O7@F>3Cd3sO%}yH^qX`; zkcMNxHI(cF>KRB`=kVQtwtGXE;OE{R8ZPF{4j+mP2ppUu83lQQtvW5Sqfxrwe!y7Z z`gs1rb^MiT7r%vN&h-OXz?>SgVdufOl$cx;|Lewbw1a=(Batn6#=~uu?H{Zb;KG&^!|(IrzNhHotEdpH z4oLf74$qNK1jsueEa1C2X8TKLczf)1<_?9mMz({aq*I1;-}sbIlp^d9OzsOezAH}l z-BRm}t1G@C0-U}y>YY`Fr~MCiD3~*6Z_nMY2qup?qZJcsOk7Tymf5yj@!UK)Hfok2 zg34N~J#gjVPHWHKHb>D7WusH6i`NQ%$^Af z4PHdq|6sxkHwAH%|LLwgXaeNhb;+a#kGAFOzF`$#n2+uz?C@0dLtNm&j}5%2XDqPa`IhXjAD|ohr>=Ou zBCEKUSDt$4*Pd_GPnd~x)qB3crj($J?88zwcNznmt zZ&zLUH5Ncv$qL$Hsi8)f8fSOffW9RumxHXW|8lL6;t7B~<6VC_pKKfFxtcfjMUyZ1 zPziQp3?3qJcd*Z;(;&CDR*W_~ebLfBi{LcpN|w2vbP60-$(!-q>zWUWYW*xcPaHj!f}W_e!UtTorEf!!I+VRSeU%26cX&m(?#C*aPwAdUM4le7?l z+!hsvQU!VwV`GxbfSb++FH&({8-|7OaWSfGSE8KWUpz=dUsRyUvK@Y7&?MozrPX!X zF%#{hy-@7sGRa#VJTP`RQGnKIZM5z*UFQ(mcnUaxvXRe2Vm;nLwBA0L_nsbmxV^DA z{BWL#$zW5ixVhmkADne_Bdus|LHX{OQFgif3F&9Hbt9rP>j*u+<&cisl*BLjcOOTBo5Do(`Kn#`TOtwgA(RRzbv*ul69=^*d%u=jo z@_Za?LUM%@F6oKgKO-s;znV=QgFz+M2Q7DkrjD#L5Of3kQ5Dz+>5#0ALpgAxN5ip4 zE$V!AYaQG$G0BsE26^mV(3fjfuyu116q&Q=3}*enAO)-3rNxyu zi?(q^!mD70jmCMWxr!()n6QxO9@_b)$ayZcz}B}P|LZz*Hl7;H%lm80rzBkWs=?oK z-p%tAYRL4gfjhi2x~j1f|I0iaP2AB=0uOGaG}%u{MuX|B@?SR;2V--c9Q;%&vJ`SR z5u#}sFIOVd_jeB8-Z}VVzdn;F3FIco(J6V41Nn=e$_%&dIg78*XX}Wn74?$E10@K+ zb2{rF(0|$1Q0+XT?qEWl)T&iDnH{9K+x2Cn7}!^8SL@1868D7*VU8Ie+g9@00tXJ( zD1bHn%cuFRwqvt^Kxq=SE;2cYz8e{0RDenx*trzbA4A;O?y+cGpjjTWrs+9o6kl0~ zu@r6vUFAk|kK?5(SF4UM+J8~7G^&fr%9iDN<`Fd~UOh9i-hE8)Z)@-~;-yZ=8w328 z_~VKGgBXjl%K<30;grMIiwwvjzymM&kyo?(S#I%NF};~a2rtyhAniOSv;3Cyl9$VX z>st-IEXm4zy^`;?K?&>Xi$gm_kK<Go7h=6GMLXckUSS`ZqNr%s#nDSbGD1%+Ht(!^F&p?ywdM-vlCwsyP9*kUGw=V z45A|cL+yp+`2(UT$Wv!IvH_ld<^dIua`_c30fK(y=z46Rsb`|UMvd|>PNL2O?SKEo zZKan)*(}gb6o0AsQ5&F3>1{IWyqyb(iiNA|5qkG!dZkf5deU8(`E#fItM|ql0Ur~g z<}81@p;^3g%Fj3U6cQ~YUKNuUQ1)Hoo zR#-f4ag&%ak?*moV4&8ZWliP0`D1V;GofWE#B z9}o1?Z%$>aMO1N|*b+J1h>2KRkB{6TT`25s)!iUwrk}$PY;_tte!ebrzFL9z;F~An zV9&a18Avgx<=2&;!k_2m%#Up}S#)to8TF+4yM8zdt>{6evUN6f6{VNl4kb0PMxcOY ziQqaZq1Ki-OTz?#!3llSfn@>Ux^xbLA{W*0j{fGx>THq+EZC-PbS&}xn7>Nrq%pp3 zX;a+aeHS#sp5<{yVlrFsKc2#PuUdLO4`VJ93cA0V#@F3E~e8536Du#vUUe~h; zC^L)N<@m+c_b~8>=G-rQhuaqyq1jv$BE?|9QCR$TJxx5mEsU(weufZ7#)TZ~ULWDv z0s8kIgLaeE%!GBJEg$2T#4^W)B$xEcG!N0+sL$AI{p>vIo5 zQG2WYowpqL`Onp2(w6`8+VqYZu9j?b+}*WtuBQ>f7qa*EX}ZQxq5709gKwX}kdqqW zhJ!;-aoHt4&%=eXl7d0~7YBDPX~LcuY{Hl*l|Q$U?)fp=uCJZ}RPn#^j%+6pL?|+J zTjRN7Rf%19UMJYy+_SA(?fh;KptRoewAuK&^s^_R4%+E=(39&u>m9W|581a4r-mnu z)Fv4!?8LTx9liYQv<`SE5Jf6KlhD6?S13h}?3}_lm+?Z>Ab*MJd3y$#xuBN35;G6% zC_>cVWAE7HRAJ#80-OB@NyXUMv5o0r9W-f#FKYJ-#<60Go1Z&qUjTjcDO~ZHvgt@ouF<#`~Y?01FUk z%d58glaN+!h7E6|<2Yo&&Qx{b%H)FuA9Wx~1?eVBvkEM_lh z2;|WWrM)Ug@=-+Fkb}1NPs`bDZ`PV5_f8)|0$SZoX4#JUqsDODb750Ld5daWtXxe% z4$*Mp5N4|AUelSSe{tW;Qfp>qp(X3D?fKJj>pmT!srhoZN=h~-Bc8Oe-QmV&AhLLZ zaK#)rpw?EpyaBm@=6>K_moky56{61; z92!`4u%kVBT#)rb!`T(lussP_uLqxmmqX^sx_0`wPPX@m#r;hvz;(xo1vH(@= zU`uu7wuw{hYQARbu!+fA<0%t^KiEbO`TMQp4<@!YmS&aK#>>oJ^ijNvb59|L6HCP+ zZ3quJO3Gw6gIb;mJNM@dh3wx3Tr<^zM)WhlpUlXBQ>sWP#73Vx%}8G@q5i;W@CAKm zEM~B$YwbZIB{}5^HO+C#!>~l#eSXtkwN@ZBhbVC}h*#GCUw(m0ZFUm-sly;9YhYdU z#m8qhsMCwUjIu7GLQNjh6U66)N|~w{uVcLDBW)5wh_}C30U=x~y0y^!_)E=)8&Q!) zwmmWk)sc(i(-o*g!A^3b70Uyn8y{fQsf+U1GaBTNW9j|d{dg19A|vn5T?x5Sis#;B za6@)}^4}YCUHCFL5@m&|JD9vZ61O_EJznI-YkfdcXkD8)-_%vSmsq~vec;L(pVuP9 z4=e&wYZ;;}0$r_(-?ETl7}@AwfDUldKFspuYOlQbV!^I^c>V;o_wva#3B}Y7zpFU( z$k+b3NdSTuat z(C0LbjYG~DIFYWTR=g8kN?YDBBW+NmE;_vC0QOG5Bl}C-*9|OGD9|-$T)ljV_(qz%;p}To zy58JXN5#~jdkJCtdjjw-JXOnz<2a*ckHg^yZ1)edIdYc<3 z*C&6;S6ka`3^Qp|_#F2Mi^x@&Land9JCxBQ0Aa1yyGp0= zNz&niXqTN5ZujiEie4r2+}tnh(-+4)zHnr6`!~%y!i9xOT*7z#95r?ev68R-CMJM?25DH~rWGeRiBY zXsc#h>}iI-?B}HG@vo1PEn^%o@l6jiv$^Kc2KyLj5!cIaE~U|Gl=Q?T>=C28or(hq zw_DF65+FO7dIpnrOMI}{z0zu8?q1%*5SvX~Lmk80Ep22XtyW4b?0bt!w3OP3zVrTa zGWf2e;*tR0XPtvWqV}N(^YU0)MyW))ab)QiK9aJXged2dvux92^;~Y?xM;MrG z0nXhV4$rP!VOufkS*=g;M}StoLmA#DeWP~QgTt3eO|Bxyw?QC5)A4Pob~?CjAq34b zG(!ZFdYZKwB|YkHtV#{DOT1p5(mlB0q%dr5nBEgq7Eb-*F~_@wGmVpL1RqQLk22)# zcL}8J8S5dHlJ~P13TUD3r-NC8o9xS1Wtnr32F|`3ms_L)GAwqPfK%ktV<#o45P8=Q zrv|DS9!==;Jj*#7!yJ?sg?8`0uvLp~st`xYymUSDz~O9;1H{Xga?&zv4q`_be4WBm zL0mmcQF|@%;!td-xY!!H)SoB2OK@M(`O*83049cWMkl1=OsK!Fa4Ws6V?ca1XkiR2 zZ?rl0t4jNGV2y0r+k!ekuHJ}2hCN4i$V8Fstl+w5V;_FLtsi(p%TM(jyfdZ+Ga;=# zTAbzU;1PA5ldj%;+U1A*M|QOGn}Kv%7W{co&1=%*(n^1ysXQ&1i_m>`G9GX2P zS_ZSoD&3FpXNRe@op_M>c=W1oR$%^a{W48S!9F>~+x!iZHA&9%0`Vf?w8EsIcx}~M zU0rsc$@?h)>2h~Yo_NCy|q@fLd`bQR_KtGS6USt0qh;5o);M2iU2vx;+gX3vbSXtZ0P-Wlz$5k z$yyM%6S21AU2138c&>L^fp^=-{z%0;fwtj{6Nw$W^pVjp2#ywPz!DE-yt*$!ySaiSQzy&_U zUNW+IIs)N~{Mg%E1qtx;ef%hK!Oc+S*0%TIm=j4r-%2D)&-v_Bww+_~jhg*B@2rw! zdEUD1N$&ukt`}Lh|4Monp6b>}AG@>3mBh=A#V*{n66d z;Agf0NxW%^jNe|;moZvNUG@mTKW}V5ZI>wQh9q7y_BBQ&O4y$79CMjwXxN+@-PAs% zx%=@P^6qNlW2^IZP-pqn{jrYPp0tI3S31_jzWO~Ts#{eFm&JOZT7VM)b7V^)K6qcM zxsAu_O|WZFUqk0XkAhwGVY3Gn19(if&e{&7Y$$GAcZB3da-SmA%YE^l^LnM30Un&T z!j|7+5ChA~yCQEL9q#ZG+d!6b_xC?=4a~2Wv&C{S`iHc@lG`%4F|V9iWbGI^x3rYT z(lcAe>YNZb$<--qTLwi=TNmXf`-@1d8Dg+z=YyL^ox8padPiKG^JJ1F4iZGJ9AQEJ zmy(WUjtywr`=dIZOIq4OpB#X_IEEhx(ln^mb}Qgba(p4M9>}oQ@&ebHE-u~MtY6&a zeJ#;BXZ)rE&*?c)!Br%1mh!+Gl@HS5ks;~#=M#* zEiBZ{o?rkTZiqSq(3$j&961RW&_!P;B#O$_%go06E_76bhkE4d*8QBf{YjQ7Z|NKs z`s|x+L3@6Y7T|m$j6`p~T0eB?IlfeIM(^<55;*2xQ6$&xhOvjNwPCamq*Scogs%Gs zU$t}r*IkPq)TA|5nrBpNQloFFx-Zg{Sl4%hnsMj5y#l1me7-%nhZ@Dxvp8u}8x;S% zT0j1~MR7=iD!N4sJz3$`tPGcf>BADYR%Oi<9D%^&pU%B;!pGy3ICCjY70L3K`DHev zIf2gIyG5CgI(Opl#+WNYzvV8mta@JO9n7$LS?(V>%_o z>8r@z)rAM^ZTQyFv+Fb5z6_=1JY)ukg5yni?lLx;yDM=Ur8yJ}`G|{oz<*31gW= z_l-T1*70PM$c9xNTeCRdoc7yE&MH8#E=saw+mY?bDSMe5Oy8R zxXDGXw=aCTRBBEK99ge0bvW=4F$oVtBQ##~H1*QsdS9L5*m}|52L+;orCp(P+RL6k z%2ET6Y*WluO>_X`7Va@B)B-JdFI{waPJpJ}X+p1jn}={BPKLUW=!crNJP3I(cMd@N z{FSRDN0A>DQTB^(fmpzyCq&7-qNI0Zva75tU`GrluGa|b(@oYr|t1-j!2VmaYjV1c>}m%LjA1o^#N=@;1x^*}*}j+Z5`jZ`vL$ zl+@K%@A&7EG0WNOGO=S{6!n-GBsX;dO$u}3rw?hf4S~CbOs}V?&^-qaj=Vymw6Ub+ zVS<`eI`cq;E>ipmVfL#k4PZywh!Eih=+zgdH~q;8Szk}D*AsIm>g;HzgkvPRq-(w! z2-NlUyIAgTTiipAl;YzWwtg&j4H-jYjBH&h8zUneGH0s;fV0pYmQgGi=ZI_mk@3_B zr<1TpUG`l`+SFZ@3kPhUL)hC5Uh-7$*f~6W9bi$rZD42dUD2ZM@fgZG4_pi+ZvM(9 zlhwMGY6T2x3lyv7T^p>mD zZutk59VWOveKcWs3e*=;ctct#c7Nm%;|jYI>U;*W6!s}W(=h2f@@j@x%Q<@rx12UQ%-TO^>3_^Dqz5MXcNsWO!ak=zLe+Bod*P#trIb_GbKet^ zPqZ7SaXgz)05`_LSlbE}u~jU6;#L9?K!c;w`7{?&hSJW$^RUISOQuB{*}^NVg(sMn zyP7{lyY=PvqG#d`>dM0abTXECnqB9WLg`e)*Pq$iNbG2wo>t}1=yN;^mWXK#Dk^q0sWK%1p|A1}ZwWYx~yyKL;6bmf{6TaiyCW^jrmd0E>=xI(AuLgJ@ztQn}kFJ>!^T{n^= zz>C&=2n_dxgJt*h6LF58uPc^!X<~E1yob|>O z*drAf87hLkq#}h~dR_+Z&ud%iZQ>7^TY1Z7xTLlA4)UhOA9fBy>st6sA6!ow8zbdp zHr`^0Dlgi5Ab3`rF}6({vI*aMUGYJ+^0L0O*aeOCv(7GhN zX+(ok**qM^FGnf$%Ctc>d17iB?cd8>TJWJ$piIqOPDzN1eCmDiYfs~}(@Nc*zdtYh z>aQx{PZW?2DM@i>mRG^yxM8(SM!CG2WGxu+6pC1~#*Rcka=Nfic@Wue{QJrg@39ef{KBxtp~I>g^gr0NLG z3}K$=Hu}8J@KDlA!C~y&O3$3&O6QfZU_Xns#5t<)86)IrqTIrju?1=mOXmWebw#qz z^0#`B>#2ll*0#P$+muKJaFlqYaG|~O&I@|P+6*qgdMLuZ0Al(Qv)PNu`bj(N&@!x|U z&-tF;dH;N#H-4BA_TFo)d)@1f>$*0G=IO=VqVMo{e*>W{d7_W4&i!L+4?^>RV+LvR z$uaaOb^70Z(6EHne#x*05E1_6E0E+%c_V4wf#*!8UQ{`y7&Yd-qwfN6i*0f?s+OWa zRU28?P^2@$TQ9Fu0n?Fo4Bol#x`S@rPz?aQ++siDVcY2hQ7Ie4KmoA^xfl>PE-J(^$vdl_+X03jg?qAKlf>_>@T_RlF z2NeHjW{ECK9oQ$s zqv^RttB@7`ynZiv!npotV{0qTvxbm{-L-<`a&#FvC5eA4M(GXC%iTJqa#uRhgv_>5 zzI~9wJduTI3R2r;W#j!|DNSsKK64$u|Kzd76VVRVtA@~idV`2=19Qqg(Lq7F{Q%kQ zy=s0ZG)H14SGL8aX;$sJkf33Dvt2L+C`ey$@^KNC^xsWgCNB9Pq9ZHNb-n8SI9P6V z2iq6N4V~2UyXRB}83YQhyUJ~sk}b8!j+NNI?@NB4o5>|nGok-59b#9I$vJ_z z=={>TLCvjm*C(b=MS=&WjtaS}9$5!Kgl?neNPMdlNDN5zrt0TppoQvZcnsd@kq%Ok z(a16?3b^YXJI|MSSoMf~0r7hkcin`X9o{Wa`iQH$qPF8yapT(a1ops!6JARj7pJj- zpI&Ppum@eMW>e2@uYC{Pf?PN^{^@|^f`40t{XE;5B{Aodm2#jEl}JL_ct(BgaZu0< zZo|?Cg0sN)=>Ei@pM5yfy$>rlDAl@+b|gI=7p%@VC#83xk;l^bDlb0DWPZICWaO@- zVW?K*yQMj#ERi{=N9MEExn!E4Ch@ZcotLa1I0rO_p+2Gd-NE?qeK=Kc5bNWLJNS%d zxc@QJ&7V-$mhFNm0(z)5=SEvVlY)M)Rvf+68T~>{jA!98yTp8doZ@gO9Ky-!?vT#64^eb0m(WTDxyx>ANzSZ5?Nh!Pe6nXaCuz%%p;g>6XV*s%!6&EwXy>v@N*m{#PLAQ8%7!xLFj22`F6AOo!OoA|?+yJ+_8l z*UQ5=l}zc}Ku#-p3r9kw92A@koX^YiljQZqX6P!KU8}CM8@~|N@FFX~nk73huYdR1 z3_PC?QvK{!lrFY8R4^%{IGj!?I|_`DkaeXFoa{aaR~Y2CK*Ns=WPGaE=Bfvu>>O!L zR`;-OX=~CZ-5dhHpA(r#^EgH!4JgCoP~2iCA}j~Zw*E0>|I-?Uf1Yp3*lRPVOv=i$ zbG$uGmwULbJVKc?7J*)ps?M!@(`T#CNFAFP{C*PM<*;rxoZbg=o#;wUcB-~ikU*gg7`wYsU>-#v=+g4#eOC*B1AYuhVn5Fsy_lKXLpsSZdOObjBy zXHSU++Qpjv^rT&@4!bp$EwPbB4DoWCV`0PP=HlGJ$wx}QIXh#~X}9q$Ux zbc+V?j)liHh@X!t=$?(Wb?n%)S5}dir3YYtt55 zNeqB&zmvnAN#}oXHpYC>?LU`g^%+yoz_{DMW7`uUh5Nc8IqaA@)Xk14uS-r`IdAq<9Q2Bsed~T@wt7 zOL&OROhD3Ft36EB!tiZ%bs9rBWG^WhwZ}9H!VdBVZBW)LMWr880Cwt4x{k^S@md+j zREwB^_8NcO1uT<--<^{aD}f-d0?W6o!Uiwoa8L_I6DV53jtzVR!V3TGjq9H0cJ25J z)^hx$o!{F^)F=vAX2=WH<=y@DVKN|c{M2uqlpiMzRYvrCn zNOg!}bsztbg9eXssLE%A%Fl3)K(@p;g@KpN#y&Dn?do_>5AD|xwGPr6e` zQt`EWA5W^vaH-#)3}qOfd3)>~!gu1{V4Uec?VH{3d7PR#A5l_t*Pff(;0DJ6GG|I| z=bV<=nD(RCF)yR44WfB0w=MyOJ8@$V_A?%xusd^9&ihGlU^Ip;?wzo)hk0; zC|myw9HUh2&8m^8$g=zK(r2wJgyM3BlQz91@u00!h@!a{cVV=&9*utetvCoT5-ZxZ z8fu!suqtW2!`$RzBfFQlfo%wdulCZ|sB=on*bm93mK7#v-20w&y;skU`(8o})`k&4 z4G$xc)DkjkRDPk1m8Gti*f!K(VUCVGJ>8az&Fn3=nktO-7m;kLh>O(B{i;le0&|r5 zRMOm-cW9u2c=MI@O@ulvQLBRu+$XN~_80!VB5vDQ*PN_9a?*5xVC@ls^V%L{Zq>@S zZeweDfKdFQ4eeW;r4y@Uf0-Wr>jyT|J%2+QXVZsEWQ6a}>4U>c+kYs2Zdr576nF*J zW3*AHS6VxsSIXd?x+76zT-m$NUp6rYsn~o0*T=&w-R9Q&gD=frdB3eRHR5fUb9^X# z(To}o95*x9mFTUMS(KzC_yLDn|g5;i?+4j7734mfGF0#4r;_>`|(es^)J`^mf@*#Ku+855$1FSkuia{lg*&< z>eraDp6M#9$)hFWPsWEu1HER>rp!z|*}0va&=%b@DD%wtliG1JvsyJciAys`%N zy>@h>U_B}&JZBeI4#2jmCjgwvXP}8-vsszFsoV>1}(3Y0tw(%(nylkk3HbEs*%& z`T~DV8Jc+=UaIxA+QXGJm&~8Z&aHnL{E(lQ{guOFF@!xMz3F@1L?`^;~T@|x|I;%5t@=L^wm$< zCI;5&(SKuGa_!kzw~(sy=$4Cd=5ZV3&$UjHzM{bCPBh=;(sZQ!aZ2Cn zA^3eW55b44f6dP`wthQ&k#A3S%}{E8KK_Sc1S4*Hg0Nj$OwEpsUDGA{j%>cx0*CQf zDeFyR7A5D*S^a*P!JQ+&Ch?~`Nl$uPi${ym)t;fF`(p@oNcfN3l1}R*NU=}yy+jgz z>XQ$Cy{=jD2TU#>V57HIkCF=TV+{FI?|wgiJ^+)eCFv+y1YqS@DWk_6ZINUbqh0Be z=LQ_@H;Uf)gw`DC_SJgmk6z2eG}$K5FvodM`xMHVXDwb3@iDIu_6fDfMCjO{`^2fJ zPHF}orz92*JAanfy#PrYJ!k{E?f;}x+FYmT3i3OYsSbTMF4{afZ|09_F(oz+fy$+( zPU>mIZ^9z=Z=uY#DikBh3e3UkL*M%u4JVaK3rCuk-!xbNRiyukp=Hy(%~$jZnVcIQ zD_vmtN-|$dTo!OIqwg&h_8QM3V`IyYh1=|?+Y7@hjCvFH0S{rHG#Y-5O=*JVyLX51~Np5Nho^edIWn6#&%LfPx$ zSp%KwmPi1Q$9gBa)*x|dC@l1Y7R)$BSiY}_tfo>`6^(>tB%au%i?oz|f`)qZk9LeTXqTEJt9KNB)URaVr_P>M^G#@QWJ zhNdt{J{C`2`!J!b@;>Uwb)|fBce&M6r1(&yb5=V}0G3{Fob8xC8L=ytEw~3XPt6wX z<fY16dn#u zy)GI{nFc7&uCmbWkbV|*@JRAxE{{BVto2lPMOdxyKpfj9(>bdV+4}jl1DQ;gZJ5jv(0Tq02rZCL1q(#4L1gGB}Mj2b~YRJ2UJXA|`voHv-7iT3Ng(gcz} zGXSZA3y&sK_FY|O3n9b$2MYU_x`KJpBG_is8n43KM!KoHT#sy;Y@^ts*Ca2?W4#mh zi;^1_F)OQH2)y0?;^F_YTivKkx8ai`fQ)miHSeVV;LZl&?w)ew>TdoIX~8y@NuQnR z92L?j4yz(hmpHE@2%!x&;Q~aja-?AK7CnVfm`kv1rQqX0;wTZqW1D&PB) zi%Oc5g@f}E!urSOe*;EgRLby@zJYr3%xkOO{IXx$jJj$_IN?c59SKI(Dm|m zLU!$zm9C&5j|~p$1%JE)8o;;dP8$F=XAz%1o0GV%&Gt}gd#CiBg9M+k_NjT2vlsQn zx5=~bNL~UHWEd1C4nXw*-ldfUQFpdX>wV)WMDdTOp=9~X=(Y~Dj6${Upq5)>MZZXN zeRW}z@tMsazOoIVT>gfa*<+GuebdP(2=(c~QxvpZz~XU<+|XB~ss7QkJ#+sFzqN)3 z^vYo>Xa4xCu>e;@JJ zCfBJ?h-gjbWnrOtjY1d18}%dT?3T4sRB8H97lr=0mcVqyd&5~8&iWDyDW)5 zkrT^il_Wl@clUksfEcsC#le$f&FJ72NA!x3*i8U=L+~z^@y^4c^%27_oZ|$>7|M9% zeC(bFmGnTJfbsdc+0FJo|2xC388V{+T^3d>;UoMkY>9#k-Bk0>`kDknf)bQ!7Uj7M zldoU<%l-0r_rQ-w=ArdRX?;{<2`oY=6G!t z$X%_TM;xTOMQ%?IZLR^XxuZn2CWaz0iayXu<*GZ4n-%}6g!^k8LE%Q zwinzZdoj?l3`v7ZN-)m?WlGe&<9Vs?&fSPxFHX$wJ%+B=Eo{UX(lCzUpZM=e4gQg5 zY{r6z0d#A`v>Za@JX?<1dJ*Fc4j{6XUa0y8U*2zlrKXBJ1@IG}_e3Tt$6R50&LbA^ zjWM^_fzQyF_;#bu0C&$lZCO|2Lgg(YKO2(=hv?2G#}{l5Cb~b~yMch#O@`0OXx$D9 zTknR?<*kJ=clF&2EvmTowQj%T^7!gjZNOUMqyCVFry>B-LkOn#@HVPWq&zJ?auR8v zb(3Fr>ElkZ?H^xx+A0OqI1<$0JIKN(hk1SXV#%rz#L^&_Ba^-36b4kzJ4hhf7|qz`)=H&L@ScBQb={@yvbloW9e4OLfa}t$peBs$nSK!L8j+a=LvWAZ zQfhcsrS7ESe5J2XWBUWrsyW<#eIJHTEdF*zfPJv)tt}4R@S}0D>No2fSRw(^E^l9e z4AXifK)Ik^Q?R&Jb|m64Fr56U=7@M4ENc})*af9uU(7~bqc0@zJA5$AsV3&fN_XQB*OHX z9p$_U8Q_(S9y>IK-<(^3%fImO&}0)7qQHLW{h-xi4q6^qp1|W3bF`}L05JD)-Bt_# zOO}7^MWAb4(5VHGiia313dwzW9!4J=j!V7~sT`4s&Gy8M7Vst`ZTU4|V-GV0ml{my z{m8#R-QWDt!K-VBo^tO`r@KhD2Gqfwth~q7+$hSOYugz)K=XYp<_T)+_2UKAo3Y!e zLk^7R%5tg_J((-iHv;6HQgZKZw2&oiw(Sskj0!$lxEGLsCb>t~hO*o@tY1E~M#g)7 zxvEAa@|U6vfRIAW<@W+*VJ)^YXfu%G>Ine}$755L7OYz@MsTU`2kg@XwfWm!X@SSo zSs1@5=LRF(@rvO?wdd(g2}}vs3Kzu~fs)gR*`4lhtji4sJP;B}-N}4i%5MK{@kw-W zp;Iqvh+}UQQrjI+SZsl{%!Ca}7inzc!MuibhouRI2OVNfej-KMSX*<>iX= z9FeQMRFq^q0FH%-YtCP}cc1d8_}Jh9kI=yD(y|(Zi)Mp>uAToooZ&V@;YeWa361A` zQe|rXEAJ2|D=+z<0mFtl%V!n-8MgqzVH9jKrDhu`%r%*hGjwlz}5``Mo%(^=xFeou*G z{kqmz$#F#2h`N4=%g$(hoM-Iy^_SCa9oMnRiVSOjwo5-`1H2|D|0OPWdmakyaVkmf z?l6he!DRJ{%y#QqJ^rNjk!DP~r>Xh3YL3Atqp#AD*`(g*C!9H2u&`Rq zz{GGp9}*0mvhvdpP+95W^o^r_<+1}YyiCr$e*%9j3uD)!R#%tP4mqitTk_yxE24s| zJwHh66*t7EJ6b>fkQ#aYUA2Vqa zW7kiKu2B2;(dyb{Ljp5WL(v0&3IH}9%%nl)dsHo+GuS!aC5>K#!c5<}? zR;y2gU7fH`u)TQaGKr2qrkV(E(LfOw;6|GTl>S4(N1)e0$6Ud#yH*?z<1cupu~PBT zABYfLK5VOt;P1nsn3kjvb(Et^;#qcbjqR1 z98{VbtEq+%LWW=LRe)ZubOO~Cfv;-uB*#F!QuymrUFEv{H zs5asXD%|nh^XSSYEMEbbP0HWaOS1x z7?5TET17wr=IbYyEXO{6l~Cosjqf%~H0^txXGI{=zQJpY@*%Z=o5wqM6 z?RXd8?0<*h6CRo+v-_6Cz%;e{iNxU%o?!{>$J>0-aIHN89bN%TqCivva<03W z`b-+|zJUe!PL1a|zlz!px6;5GBpu5pW?B-L;OZyrC!L>TdvVKqcW39rh)w8Hak;w& z?iC=8|i&utN(sSEgHMD5F|H!h2PG%N=re7SVQp>vI)rNC<()!bV+OBazx`a~Qm7WirMTSruk1N)>RA?B!lFoPYOS4h8i7AZf z*eG?Rb`JP}b2jru_iY~Wbr8C%$tb9Sww$@}wpUMRw0$}2YM`%N~={@^Ii?!iB z&FA?Um*>UwGy>I!_RaHoIdwz?9ucOFyD6N#LLUQKddh;9z;rkkvs$M^s`c)UF?D^? z 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�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 VzCS8bukGR;Kyw?j8j~A${s;2HEZG16 diff --git a/docs/_static/img/analysis/risk_analysis_annualized_return.png b/docs/_static/img/analysis/risk_analysis_annualized_return.png index 1979ca19b3735c91547bad0b474f688897cf401a..18e7a90aa091c32b686b9d7aca36f2cd8e23e560 100644 GIT binary patch literal 46842 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%0DcwD`v3IjFWVZ-N@mXzBI`{-Q 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 I*}q=@A3-U0m;e9( diff --git a/docs/_static/img/analysis/risk_analysis_bar.png b/docs/_static/img/analysis/risk_analysis_bar.png index 1cce1f340ea414d90a13c899571d329ad6dcb59d..c90650a6dd20a8eb530096155e3505fab0013422 100644 GIT binary patch literal 12926 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 literal 15013 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 literal 57493 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 diff --git a/docs/_static/img/analysis/risk_analysis_max_drawdown.png b/docs/_static/img/analysis/risk_analysis_max_drawdown.png index a6881022252ccc9f4d6531fdb1598199c2b1414b..b7f1ae130603e2bcd62c465d9cf1942326f46495 100644 GIT binary patch literal 54377 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 z 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 yKc;t7DiRnIdCjjrQS6}uRO3WSI^(Dg7wD zd@1B*!rGM|_~Q1k7k;c-XiZ+F-#2P+U1J-NA9#FHz1ZZC^WD(+-c7hlrb z%TfovE=+!M{zF}p+K#$c(PVQGyVSA`-*RvH&Xbu=Ya15eCl2hf5N!6EqisJPWPH*1yDNN1l zoYwDoUND{x@1+a-w~Q1sV8R#*SrAeX#2p*)1V({cKg3`+Uf|Eo*=s#DP(&5~Ch2t+ z!o*F{Co%>GRS?k!UJ%LNrkF1d?^X8EMx23)96&~)%eYY%j^BV;^85y(NQxNbRMvaf zT2evDm>-C>HjBS>U$a#*A~kr0z;QidyWD0dcB6CZFk1lSxKR(5?AW5*k%?zq^w||aEeq2PUzK}cfxDi}+A0~El1Kmja7lZH*E z?%6*T0FT9<(Zh<%n2(vM;0O~vaw?ckD@zAq;Vp&_e|mP;1sPv0inVv$JOG^_fOlXb zEwC~*WV`YeuF1S}_{KEOUKU8+uJTeY)j&a$yMZ)_7{wdEi zLh{DZ4AcF<;Lo&%Wdx=yHzim4n+@-bKK z9a(?8DDa#q)_Vq~z39>>9BJy>WyapNbK|9EPC66R%)v<%J~wsFo^O^4m%WT#8v+d} z#`5!tAiMC0W!0DeY z6MrTOfu6Pq@j=L~qpa91^Xh3JEzhh3Q zknA{7xv@d6hdyuCoh9QZmuDmsoXe#`UX5N+x7%XqsakVYM!-k6PmL7Tiyf#Boq7Fx zv2C=p)=1qu+qc#&u@D)og^m?Uj0vosOsLsYigA^iXQilyge@WTc48C=Gp|?XmUkt$ znm0%ZkXx4Na1U`z?G=H$llp@olk8>g><}FugGU+6@2*WIqJFO+x1pegj5m2$N+Fe^ z=5U`(a(p{XGi!a0?B&AHe4dZ^Ngw&vxXkwWdSuN)AGb?VjUCms9DoA^ClwvPGt)PG1iYfQhKY|p>FAIv;#6MpF~IDx*i z-bD3{{P{$6RcMZ)|3&4U#ajJ#^EfwB@+4Lh7j?SJ?VXRJ`}Iui_jXKp2_o>{<$Ql- z=G|6-wdRDIC)?_3)Wz}h{VN~xijGto%jLr%KRzd0Jj19UWVm`@P3{iDZVujS$}+$O zp|=b3Ub|44@^Lx;=rHww)oT-ZV7U@?*A)Iycb@%VM36H`;sfE54<$FPoi8bkV2S{2Kn)7Gc~Hnm9I>l{DD+!x=Fbxcz=&Flc)# z{LuOX3@6DTor0<&%d;UXz1bexz|Yd(`K+RXlr1l{X|UABUxvo!bbKs@U-T4@$EaQn z#?;S+cYsniwv6;==Wq9wv zEWH0}Ps4RCUtk?%evlMfI}D#xTX_*Tui*jBt`e6U!Qb+yS9+X)4m0P0Ukg+(%A3f%svs=x0J4HttAwx`=Lz1)CLqmQkovuEEr z4gErWXJpGSf0#=ekLN?$2^DD}$hc9*i=nH|02shPB(VE$MA|oz{MTv!=-mFNP{cPy q{MQv$o%mnJ|4{(`zoB0W=YqVB0P8LyX|`*C&BN`aYqiTyDgOd*4Uj$n diff --git a/docs/_static/img/analysis/risk_analysis_std.png b/docs/_static/img/analysis/risk_analysis_std.png index 73d782e206e06e1f90032fe1b52078ed9cbbda7e..6f38def26cd8df3ab8f93c568791ee7a23d6d68f 100644 GIT binary patch literal 48396 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 BssI20 literal 48011 zcmd42XH-*L)HbTeW0&5PDosKc5b2$vy(b;p-69Bem>owa6t<}>GxykV$KcZ&1Wp+kr0balYS zhYlSf9y)Y*@x(FU&O57!MBvBCyE;}rhYm>z(*7L|%+>Hebm;scUGP=Yz@deV$Y8Gi z>bK$H?{cfqmFVAcRdUy^>06wVESbBeaA&Sdvi##?_MYTq#OY>=r*m9JC<8ygxKJ1G zHRje8&p`@dS-(!&+d;){X6A)lf}~YKMTd?cIW)^Tv#IBfdd7?ROUvm#gyjQWU>~&4 z&B+7Y=|3+IF~a}40H6Eo0!rI8@VSCM{O860FH5exp8O~vDj)#CzYzz%miC?Z-ek*} zS+g?nODgx>2OR zaLuo8^~@c}1r=$QCQ3^E&P|ij4+&kfPKI?{^%o|32Bk95gh>@%*Pz ze);PQ6GEpe9ZROFeV+5oD0ZuA$Nk(ou1dxHQlnk`z2tx2 z`9Juh|Erh$pPc&tiUWLMOOduuz91rUgx$@-LEt>@$&)9)vCk8e#Cx{36mh#>zV#RN zeEw|txtWufH@Os6%kGcU($dQL^y$-^r)>G5pej-B%~#you^gVbsnu)yPnp^Srj}vU z9fc8x!+?fSvY4kJ!Jj{xt^tnB2nU~oYN46__2U>|U$p-tA^*M&*duM(6x+X7(~O$& z|LdL|&HDexy8qXE|Jo6-?%sTSm#GW1NkFusxBnEosIb5_JsJCmotAU~2so!>;qWgW z-7u$Et`ilgZuaej475^6TZgHQ{b3aT$uj&M*Y7CV|45=vfM`2aP-pc~tRyH;YbbLN zRKf7{jpln8n#YV4IPBqnANa@dovGy4+;bZ#v4hD*^+Cpk3uk+5eFgfO#Z;rx2X%8T zy*|C7&%>sL)@ka6jtx$lGo^-ln(r@EkqbIb-M?PZT-I#tM66LWE97=8itA3iL9M9V zD*T-nPcTnbY;6^&d*(&0@Bx36N)D9PE-RC{P4TL9)LK`Gsp-3daV=pf28zx>9o; zTPY~L^r-oQLe$Gny$`mL<+|x6E}8M^n#oqWl`J=l&PL7L4(4^>IUoR zq-rwc2qc%qi6-O{_?*>E8KZveh)EHw0=-SdTid2NU#0E z?C};AvI_5cQLM<6VI_({yX`#R5SXpLt3>b|bM%=Sw}4ir{6URViW+~AP}r1Ml_-$g zwX0Seo_;!8L=&qP&QO{=L?y5+J7(I}BBe8%0)pyb=^DHl!89ZIApW;I07mdioG9Mi z7g$&Krr~qJmU`vo#-ha!(QeS#=T)C|)SNZ?ejGVR=d4{WOQHW}Y zjHTqZ54n#deOE>q>rpVsMo-u#C>SJTx26{?PAq-&i4mikCt~HU$!>v*gPqJ~3PQuY z8oQnBd>lV!pZTv}4>h0C0DN~*>87XI^f|B2JI}0lDt0>*&(V9N1uI(+xXDCa2dtB` z5Bl+Lj^$%w!S{|Xvw>^qnNF9a>5lt;0#+N8VnR3!GMHNK8upvy@MWHjcOmacH@B)_ zyJ#gJG|%s_7&jZ|Oi-|bhBYF%E@Q#VdG$3D{LirQ(_Q>|r=l*^FYCI9O<&NBs=3NJ zzGa~6+NOnm=WOMlN;d=>aErLrSBke&xlojPu(kWOCv(G4nGK$1tJGeqwy9Zc6%uP)TD7)DulbW_Z6t{V^p|vhNMoig_-;VmOiPtf&};SN z>v8{ea8T{b;7vZEwl#bI_CI=pX$a!KA%ma}sg_&nLSiJOdykZ$WFH-uVt$BB`!4>h z1G_n2cU-M_)8+)P!oTd578sSTJ5*?e_Y{DtDeg=_10XB#nEXl-v@;0?Fuqmz7vHkF-KcoJWnEv=`{rJhJ zvk^IBy`vaa(`Mu^9%TeA9b7{Tv_=f?ByT0m6(QyAoV6ZDRQ zM&XB*f&fGP*2`(=1B=qVqnI;{^q>mVXg{n{N0f=lIO+wA%*SK=ZM7wT@m~q(huQA5 zIT_Upv^v zx(SHOIz2bVcLYZu$g5v90qOkLa4WNKcgC?n)$8P;k$&)L72JSt$}GH;fC5EusSq^M z25s#oyfWh>WhLdW$CsY)!x2*i&iTI=8c0hzwL|Jumzr|ipEN&73;GVUpr3z|Nskuv zW%D{b#Q_|v7+s>SZuEMJG&E6kE$G9xwbd7HzUc4QB*>($_$#%R6ZIgE=i<@+9=I0D z7q!n70i}UmDegOOS2yF22YCc^%q$tf50sH3RGEO!u@MSKSgQj~1BbgoH|at)PyR<)+Gwm+=oO~dYa!8+=GF<%W>N+9nc#vZh${(QB0&W1?ZkQ^ zn-ocg;3^)Zw_sbPn-c_mn|!AfJmf?yxYTXy!-wy%3m!iC#;m8wr;Ic#7cLS#@3!O7 z9v*z*&V=>ot4%0V&R{ERr15xM*_g2rcyN&J;b3dsTvGY?@r~v1E+1a{djCCMZl}+l zS#O!(Z7IC)R!8-+dd-RTjbXffH(6%0QhV@gdeVU*v`QtsL5V=R_zha=weVh(egwPx z#*v#os`f}UpltxW{AGj>ncV71beM8qe~8m#aR^Z$Yb1nq5c;LLDF98 zle>GM-{dXyc;M!26Ehs0DZ0+}F@{3U<|uCRP{keU^f4*kG(lrR6u#c;El20kik=Hg z6o2EKz84M7n{hr?YfiHjqrrE*ZD)9ocY+6VKP$)I7~VnnbkwuKBe(=lXPx#T(Q!fj z&laASytEW@f1PJpUE=O<)w|bdw)Kzq9gZ>!P`t{N)vK#f7Mx0Uc{%h!rauW@x|Fll50*ssF=iL$dJlkDkZHS6OJ>HW>Lk8OneS*((SdX_w zg!wxwQ*Yc+#Bu+OLq7oEpa|4%MG}IJU9fZGD0*F@kF?dDl`rVtRL5`&RccIJ)TZvq zk#ci~+=r9C51kwo5EUN$nmX(ehmYXV|0_{7wf;<0$?`y=YIE3H2h|Qf$?762!oF{{6pM~)on`SRsF4h4~LU=qKFn^OEbB&EcQJ9F0Ur4gsX zX#R(u#}8y{Esw|@3}}xPsFF;AVng#AnN1~XA=tYc%DS9)1`v&_5t`k$g54RK;+a0` z#|M^{Y56Tz$w#YXUQw@i!4@tF7~>7w;av+6%(QP!ZYjpS^VOvt^p08G!w!6KS=|fP?WLe!D{>>v#doYogJou3H ztH5g7$U`?YWM3venC=LhKB1ipWl4!Me(BIwPolt(4phR2dJ^U7Rj|=#|;pp`_(gW>*``AXR3kqN-3<-{)k}+HG>nUok=zNTf^8yLt5L zzf^xw92vKosHlz`t_an_zsO#23iWEuZ|74?K+3G8T1wrT^}EyTFk5kOaZS10;c?@~ z`NH}GbMV#{J};w1xq`|T|Iw|{%h!cN6MOj38ByYC=X(FTRkG2*JT*Yi5>iG>&E0JSGIiilFcbVlsIAa3IpakeZSHyMkaUY*-s

B1i060kYmK@-;KCst$SN-W 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 diff --git a/docs/_static/img/analysis/score_ic.png b/docs/_static/img/analysis/score_ic.png index 6e1d37d2a6530351052c3972d7778b0d716f1b30..a5739a9babd609c93e2768e678c420f75314dd19 100644 GIT binary patch literal 104343 zcmc$FcQl+`+wU-XO(J@WNJEeWBl-v-AsEquAQB};8EuF@o)i&5LJ)lrj9x~s3DG;F zw}>)~UPpIs@;vW(zq7vYobRmj$64zRD`U^z*S_lSDt91EQpF7S)m@qwWW2qeOP`j6yQs)8E`bP4oOSyA`Z$WnZO8MDFuiQfsd z#2YJ7IcztQM$|~YE$47SQf_#w&?e2cP(ZEP_54jLLH(s6=JN|Iu$!*mgx+7itZ`qN z{JxJbvE6bd-g$1KoPOnP7k)b?ib`xVPReC2Zc~2NkbA?UA5RDVAp z-2eO=8T#G7KEDK#_}6#HNHBc=tiMDBrT=FyU;&Eb|79yun9}L#I@(;pJaR_Jk6Li{ zJ6-jDk&i!2%M#~@9)F=eJ@(J4PsoVIm`itZ9~>VkmQijlylJ`lDa+KV`U&UP=5>0s)(G(EhpJpNrXK@ZOBT_U%LUJa0_tRDUtD zRHE7Jy*lz3Wgt9WLg`$L8rg6E!vYZel#0R}UraxJ)bG6yhPEZ%BXsRmhKPxe`NI%D z!*Uye{a0!Z5u@72oet=MR}^Q=`omd^L~*XL4C|Z&66Di6#Y2BR>68*=D=8VPIzbS` zx4OI+|1w{ME*CQlo^k#UZxgQ7G}+SPVvZfSm2=`!o|-z4+i9$IBucPa<>uUz`*X!V z+s~XwjWd4pgz7eBWA_mHoy>Bfwmc#r_|3gDQU1AWxyFX&NWyI;dsV{J7rIk{{r%-@ z?0kbU0f@dw1yl+8J21|vyXDRMQf|vhZp{Z}rs!)X=2)Y!Fp{w|75T$Gd-MQ}>Q#}- zNJU{pc~P%F+bWlkX-zRY($=ghN=&oG0@Y=_pYw#k@Nd#63wxK>TdZZcT=j^)!L2c= zg!-^oNpKSTh(sJ>`bHnu=$TV{jC^(H1NOFvHwyn|65!A_cZ*!-0J_{SpI#h zza{XuiT!`FyZ-}w|0}8#|NSEWJxTrhR{z0){NHc&|0eYR;OV@x@&+lB*6je9tZ*dIen7l{?ZsC zgDHXFAPGU~A!SO6KdXVCd*E2$f>8RTUVjbOXa8(-b`<&@KnjWj(&Mjq{QYd7x@#l+ zr~3e(Lg@iA4*fNT-kd%~s=#soq9*6x$z+1kpOS-_f5ybk_GdNlbB5856vGFSc%w!M z?ElX;L(0HGD30FL-k~0G>5_~8Te8k*|8vsIP=Ij06cLm1d$u&8k00$Q8kG>Kkgmkb zIv^Yf{^z`ZW5pZ0h1BTE`HDc}_9lgTY)1KJmHJ9fLXt>oSx@8SHt%$}5pebYgj0kc zgJI(VZ=FfxS0QmO55*Tm;wh0;@ z58x~Bg}_)FdggnaW|n0#N&YhmxssvF1!6clOw(1JpeX$9|CHgM&?KWu!R&CV2nM@k zF!)S0PPGFN3uqe_CR6QgJgH=V=y!7mri7W79kDVomaMLQmUwH#?KML1$X%O6B0)Bw z>i+QSe;4DY*wl@U@t8-`Q?s1+?&IYklkv3%#n)%T8_1iDzsUV9LS-*9;5@+2KoWNF zjGSq*!InCO!@kd@H@yCX^Z_pd2!ITh!gZ=bbC-tgaxRTX26S4hUg)>vNBp}2va3M~ zW;ka-r@A@rYGQdoyE|`2b%bn2_)Ni1^@vYbTB#uoC%`WA>kFMFvt1} z?TE)y?ns5sG3i}rl34k1@b8Qx#b`5}nq1$ey}XoVK!)jZV!ZFDq|vUQQq!=%SLe^t z8SG5EHpj$u!0z5?yll=CcM}f|5iUNd8?fL|@RYq;N_uAMXcZ~csSFwO_r_Zi(v5I7KMxy;|h?<@Z+vpFnZrkS)~6TT&PpQ{6ciy&;~gw|c&VN3rN7owhkC zisO$=N!R?^)lAwLEl+tW^gYQRC`PRGU6){G4lt(b|ZmD>ccDG2rey zFz$x)en|p`N$j*o|v9s?s`hnf06)4a525emk#MHt1#mm9&sEP z1viZGx}DM>G|m3l+_@2bdQ zl_r}Bjt4vhma8&qzScy#Vn^W@4`iy`y!PoNyl#2?m91E~G1ZSR3?-#xSf78m9py8_ z8dc*kxNT?|&;boT{OJ#-WEkXmKK`@}xr`o~lvwow=x1F8H^J^cc~I7-gTQWiT`)y= zzCbp|COz@<{4h2@Ys`e{2rsx+R_xv)dGfRxnlqs^9(GZ(%nP#|lf)t!b(^M?iYf1jrKx)9+Qsg*dAfuV(#^ajGlh!M3?^WS@PBX$N4h}N<4^fB+W9QS5 z43dz(b>=qD>~ljkH{|tGbS^`U0Xa^zt6gaVh?VS!kb%2PL$-1+p&b@i8Gr!8%>9pZ zA_L43@T4h9Sf0lu$f^@cL3EASje1w#J?Z3jP;$->W#n+PF_km+G_eAlfXYPjnSuhb z7SNE-)+0BEu<@Dok6`1Rt`hdadv9`y5A{g4F)gEy3j89(Mh@mKA^1+c2Tuqj6q%47 z2VO2pKPEG&@NTe#ju+kj*6Rf-Wpe`+q;apMzJ3B|x{b|+9f1lz2nRdsuaRdfjw@)oh)5QBr zFW4fLu(N)z63=Wq#LRuRFr!{D#~|(aHD;0REIKyxrsv8Gd8*7)3i$7v^|^H?%c`?= z_miszc5>mt+h1Zom7XCK#_O@xUI)4mEhb^QD#9BB+z?{>??GYqnzI0U z*)OhUWqp2G5Y6z!td2;HmAhxc8j*8v;@&CJl8dv0#7k>w?X{<``__ne4uh!v~G~9QdXC?qvJ#o74(qZWy@ETm-PvX9b3**Ryw@2`OBY zgVzkeaTf|i{<%J#y=JWvInG39CBkg+P1Ne)$hA$M&Bc&YRil3x>$|942CXibVcRG% zL$r^n=o~4e2$P&7Zv$ONWA9_Ktbg6m zGZFLiL`_2)JOhtp;@WbU(F~)H$8^={38{$LkPj zGFrw5-8~94Ov%9iz6z3P<*{?lH7>~eyu#SVB5mBB(XlfEnX#{B7>IPCTy3r61w8C| zi~+Ma>8XiNTcJDEij{dDh}f(AtsP5tX$KgY3gg!ue#heGdB0{KZ@KL~n1R5hkqpq_ zLdw{<#fg|w)p@%H3ts4in(NAg+pL(-`|=>P+j*F3V^`FVExbA{<%|>w?q%pZG1y5} zZu`>tIPN9Hy3{t{HBMuFKzzsV)W&ze1e0#RXWadMxOn(3{ND5>a`?{4OYSMuX~+e^ zIa?v;G%6!OaiVNNp;E%K7REoQ0-% z5_pKa&(ypFccI55JV4m}ROx)M|{ZpgnWh4?p?mO9^9IwKHI%8qi(Q+Ij(D9M} zdU66~U7^WN9Hntr_gtZsny`XH(co#x1%T@#FYH%>qugDmS6H75%<;U$lS;&rlbiwd z#KNrlvy%uA1>?tPqYp&J6LHTzo^*@YCMNQeG(Uy)VCDPj@4SIXH%^JW&RQ*yS1;); zf2qrmg$Q79Ktm|thFtI+EN$y*3BC(~s z#lQ^1PR|-vF44v|Jp0r~4F+_0nH;{N9W%d!&nB4t0EqD-L%VUq`l4A@Bz;7FdjfSR zv`Iv|Z ztd=E_@{~=kChjdN$=~t<^=s^D$OoLdJsRJr3GpGl($IWJxVrmy=^T4O6 zFA#~B3m^wVwm(Rse;a{{t)G^NGjtvvKbggxbe`Jfkr!zxYE?w?A zBE04|*lAd897*h%ff1-H^>@pKmw1&A3>Y7z_km7|L9KVV1s(;j` zG^lZ8^}Y2hBtJzNecbG1AaRw#(IB)G= zap(C(pDGOMcw!P=CFL%v$0cLmKUc9W6mT6TW({raT#w|9FTp43DoXBt-8e~lZ)g5< zY_x4+FDRo$`tpwm2qDbH=gE>c|hh0oj84zQDOpSVD8W5_6-z5H(Ue>RgWO>sScOl}pT%UsIq*+aE5A zz|!@K=s3}C74rF%7%b`D3d`v|gOAGc_ z0%M8^?REFhiehLL{@+xvpxQxF0=7CzmJ!2AT;l}q9QZen4)fzmUaUGjlw!+-=4_z} zIZz+sPYK9mM^QEyfbQ}W5_YHx3%}v-B-;Yh2hqL(hh7%P^s>ST6KY)61o+G^Fhi>y ziwFl5Tsuuk^f(FQ-EB2naumm@w)ech3kDQ%GOuaTY{TS9-z~?kmPnbA0=nK=591nr z`$`Kbl;=7B{oYxjq8dz~m3>}{9=Ynm$A}1K1`^~5Sp}PgQM9A{l63O8Q_LVTq4I%f z$JkXc;a5k5Qq6H;iFj`gv;#M&F22auC*fu7Wi?T80Yhvj{hKbi*=xs=Zgacc&?X7XO?H_#|E^+*r_oj2p<4}i4{~lP>ly1+gWab zBPC3T5QcxhIQDVbUB2_a`-Mw4YX8}_S`v#|-sY*u~H92gmf4HEN~pb0SU-ivcRlWfX#n(Q1`&?0xe!v(jSo$hcXt`9be-dRqR5BG`b>Y+g?u@XQXB|U4;tYz1kL>=JF%WCp%p!k6+N(rm~Oa_6H#QYM7BW z*@@9g#4}v}__c@`Do2O%T)G1kMT^QOo~-(Q{iG$@xSWj8zo_F||8ORhGj}w0EnEpI zjJgYSa!&PW(B$9=&dnj9WbyERs7L20J-Z;<_tMSS`9lGD7=kTpQ3`~64aP-uq?cQY zh^Mw%&K~V9@OWBmK!%Tidg)+_f6T(zTlKAlFw&`GUUr#p%hsOpzM`%9hL@ja$LCT^#?PeV#eEl>d5NS* zl?8JXsPIz&g`GiG_I$`WsGK8Yxa79!cx+)U``)w)+O!1! z-tM<~w%IM8;$*geG9Q0T_wdcHg;QQPE{~=pgui8YOu9X9BQ;mMI$WB5LNtgLaz#)Q z?t28hovkIlvFP0dN~o=ejf$tfO3xMR7xO8R_|~e&`|Y5Om%pc3{X>U<#VF)~iT;F- zWTcDzl@(i8f=S`*G>9PV3RJc2b8K8%-0Q4zhn{_Sg;*~q!6`9fV$&2XTSiIV7=yOI z4_j>C&08@s!@W5K;$djuxBwWgZ)Ua{E~pFx0fr19Gt@{2v*$vOrsvbbr2_^fikvYY z$6pEI%daQ4=(mMmMZW9^Q0{NZ)cLA(=PFD~Gw*hNg0obaD@zZjzo&r-WvNnORz;v+ zUky+4@2icA{DCwqF_CqZRG8?#Tc$=&-t(*a#{^1!*O?F_LMz)^4}fx@Lxq^2ahzw= zgGOI)uIcH@n7=pI&%~(vRs`Z{C+_l{<2?Va$;aI+&z7AeuKo<+ z1FUX$`%YAh%v4ydPj&K8mcJ2=eeVkZ4DOGpvj$p)e!@>~jRtm>RFaj1SG4{!3xENK z36|Ly8V}7}geB&6ChrSqJl&8q_+M0`%<&V&jte@nAB1*6^gV8nN(V%eyXQ0(J50PHI9uql{4zTq{OFRZ7; z<3P!YX;6d9NXURoo?`fE-crdB*YVsSw=w<&;Y?tOqkJ&(mOl%>sURx%I~EtDhY~Q($DIqM4DGrg83q zoo=dx<%yGn5nDEa1in%@yPtC?o_8=bYpFFBYb(rC;t)$tJFnJ_xVaF&r$xvdUEEl; zxJ$a7^wIDO+Zu;3!>hik<$5NRw6#sXEowwfwa*ykiC65?zu2QAxtKr68+qSa^r?98}nqGBdOlWh8c5y*zWX zL{fZ$UvmWaRq9G?9qKe^u!bAb`dm-bjNFITwLR&%f&IK+d(N>-u3g$ zC&S{xi=Y!DYimsEUaRliHwlMP#R{2b+-(ank(-Se;Xr7|+dA+9a8ch$;G$VN=@lz4 z<+U3q_?Hm)MA!1r8;7Bp;${!Ld7;Zo?qORJrH7uRFd-rbeBsF`wUGf%*~NviC+%?? z(aCw>)Cz(3|_(&(dmC2C~%XtQ%MQ*_M;ZI!TQ$aC}yU!S+x>vI}9 zfRHB6fV}tJ_+KVSiOzkwa=|L~8~4Ym@2W2!!&4p5RwG5eB(I2K^n0rI$DEGp& ztzF-&PDqHUrHoSE7;W`j)A)^LLSt*b?3yVb{osM!`AC;Va)Q`wh7v7CJ<>(SY@9q0?if6& zaq_k7b0IkKV&^c+8riHt|Dx97Gmu1Ijg;$c-8<8jJGBl1tB^jE#>pnFavh6w_Q*7+ zuEiif-0WmvI%zQhGlr2b%^9Jp4~^+6=R&6R{JJLeE9Z9Eon?LCdMqj-rNZ~ho!=e# zP32PLkrQ^gx1XX#9AsxX(55Hs#HzjkqL(nNFAY&#%d2lD8v6&aonaJGm`s#uN!(Xt z`%en!qb3^5s$Ytv@DjvYZ4k|T;5_$V;Sq1DD2U+gfoBf*MQ{B#OND>cN51NO&Qs`j zJ$G_-52_Jay(n>>e_Vee+6R&n!@YlbIndj`M0hLJ#jXoqaG4NXu2qSxe8>dzqdt6Pf2Z;S6No}IrP;KEvGA~j;i-#?!Ty_z$octBEiU)Y7v8^ zNW`S2r~QB?JM6$dJKrPT0WDu>Y{CKw-CW5x?2&xwt03Ut6}H;w4BEa0z~CD7qMS61 zHH~CuPv=Xh3iioMy6OS`Ih+?rXhdm0;fu0eBbrG5cDs^%>Y5m;B7ocKJn zBhpz=NwnaFiy;<>wu7z?luJ)F$72Zw4c2eQGnvd_cRUQdpH_me$D(B@DKS)~q z%H;m;4L%>4xnr$C2{(^Dt67UVaGHx-m+P<#0|!h!g`EsLEjVReBB)Ls3nBn1RXhLj z>#5-nj?eqUm>^7+`#P)Frrq^w24HBagpDP1 zMm;G`o6Q0Iu%=Z^ikDL1n-3_GIHBhdPgCB z9_PN7^!SZLdO-y%YR*XJ-J)_uYRz0%Az0A5NGfHla49RM!Q1^hY>?dnFPg|$(@dC{OrVGp6H6!Ax-Vw^LN(L=mTDYOYl=+|(JLJ#S*7xb`HHYp z+W)v`R1C?g>Y9Ao2F z`MALFlnL&_w$>DpE+3Qe$!pmiXqFss;NN;{QeI5K=8_Rkas+1Q0u@Qt_5CZ-gD~dT z^%`my-OvuS`!c0UK-D+$-~duTIWJr*P=^lGEvFo@`S~qp&Yw1EeWD*7KM_vPk@Q2p z$G0=m`B(xu%tMW%I9V50mzs|0T(`2jN+HHtK~c`M^ge1ercrd~!lsPj(DWx`6 z6Z*2zWTRHpkN!;g?!ZJRz1|qNe8A%BNiYp6{vJOit{4naD3HTSneE-!Vd?g!FOI$J z+t^ns4fC--j-d8mK(b*WS&cs~_^jmf35;ZDdJv!At;ATrwXbfToWz$) zGQRo;Qo|3VS7Ypxct~*D{*kj|ioVltynMfW%9B?R>UK?-GH`WTSZoYIFW<7AV8AU& zJG8spul^=f{d*5cL3&$D^ww$;Qw}H?F;ezX15hTE=Sy%g+ix8Y9NPi=Tn@<(eRPH6 zXAN}Cg~oN&+lz|H{2qs1BAydM{?ZDsS05GiIEH^pZwgo16;f)2kf>Y&@;LzIc?TJ^ zXsYF~yFrC-8V)#nT9RXEOLLLzFxrs9D<8%^KOKvBh??cyaPt8#Ux8s|nfL++sogm+ z!Q8z?^Ncn+-`PeWi^d$Y*e80X{C)7U*@xsfprt0^Ece6D(Kub;8JsSjaX-G@bYXB5^<&}-vSmeb!rTSl3jLbt&)>@`YhIhiM_jj4 z^137*RAXeS957Nzg3~mO)pdTiICx4y;-YyS6T)cb&A5p14Fzy`$ z(t97d#vqP2pCTo^`os+qW`*gm7#&mi={4uR`c4J-`%I{bOH~jp?l;eKkOVI#RUJD{ zjeDdcX8YEw;GCX=4cZ=5OeGHhjiwK}!M!bOu?vxboE;vAjz9JLo%Kav#O)2v?O$e` zC$AcWXMO6HX2l02hh#VW1Sbn6rSZ_p)Fk?2>>9%I=Z)hIrDLs#m9{CrqJ#;()r`bq zh~9xzvDNvDQyH?}YRN%jWkl z(&9p{_p0lUL&1$z6%6K}J*L`QKb%HlP*DOuPmDc9uCYRw|FRVmGHwrP@ijHD`f&G$ zZecqFHu&ucC4At+yAyvgrK3*^WAE`6wF=Cx^s}>8ZM~j5_jptFxLCbMZyY7l3BnMe zxArbAlscLB`z^eh@hRDSB&B3hMstf42mpdGu^f@RYK(891&6)v-66kj2_6Tk=*9jq zc%%mqT5oMg9r#Q8B-{E+%e)`GB^=HBqCH&K)5<6$-%K}4Hp2>2e}btPOhr#t)Kp;| zAE}$_c6|_-{>uq!lxl5YrtBEU+P5?W14UpK4F|ab69_&!_+c-Z5c}mC-0!-rLgBFc z^NRfa7u&_;v}AdM*&-!h>a*rM18qC93Z57Zvy}P?R(;we?2=HzbC`K#(zWZC2Dic6 zOXr7_fG}BY*gkq`RAy8EP{iD9i+T>D0{-xHcz4J}I4EHLAvE5!=iUperEi=^kKbBa zE4EMdJAw%GW?!XyM(f_gOTWtayOBsVk$lmCR(r^nI<=beSoQ~%UW18+rhf2>f6gcS z)MSF#IoI_eRkHx_UUPC8(nPzl8>0)$U6bn6FI;PxoGk11Gt8upf*<;O0@iRQQ&)dS&e16uQ_s zFC(NAao2_vexFUjsYOcN_In!53cLl(!H1!s1CaCP=G9rRMPv}$$mgTVCB5W?zSDB_ za*k2eL4KxlmxMA(3c{9r6;!bFjr}VHq0lYi#N5(rqrSSgX@6c%NKQkb}lc&+tIli7;B-;@_Pjpe)Deq0(ymd`V~sy>+LnDeZ5SD(Bi^^ zA*C)-;<+I0DO*pC*B<&y+IT{A8Xh`p1%a|b1Uw`f9ngEr+sN<#f~y!gt@c^4rGc| zL*s`PZnZiN8yY9YMU#xl z+KUxt&|d6TsHf_Gkc+snJ9uA4uv7!G@QSWSE)gYVEg+?1Mg`rbUUyqwf!`!fs?9xvl$f4elt zv;zNLKo-4aR6@xDwdclIhScZw7H`+H0DYwJR(V{!BD=0zQD5A^szHR~q_xe7l&8N@ zaZc-SmE}~O_|BKu<&CP8^h5NBCQz~j;Y>4Gr`?={ZcrElaPC|0i>EU3wn4~LY<^mZ z{3d;>sP}7(g}#z6+WHJ#HM|g8!OS^G&$l)z64)B_TCtuAsV=3xb!( z9!ZB7II5zI%0v5%z#aEO%E1_So+UK1S13$rempHY;-S_?UrXdW7gUTp9Tm*>O=d=i z$ubv=asf>E&7W@3B}iKA8W=B(7NO&jS_4voN38WHtxjC&+VW8~S170FL^$Mxn+}Pg z^g06N*~3=SZyk|5Ae=Sr-qaH^(N$@br~0qjrBTr(66m=5!iTxi*5fo<3|_v`Z!Xe# z9GFOL@@C1+$0y7=Ve{rTE)(iJC1!P4ffI>3%}dPtgdLLzdR`rM^q?M(LU2kt&fb{ zSUN1r>~_1%_^odi=J6!SWi(BCtEx#e9-sDNCj>~j;>$eA$A5)qRy3u3>*oQ|M}^*{ zB|*M-W#^Q_z@oh`ERVGVQ2Wl^2v=}%v1S?2accNRPL`My3!nOrJI^9Tp$84)CF|TW&H9S&@3Bc!B7*P&h?k_(+&r+>1o~w ziNrSuoq#v1onTZ&mRnTrlV3(qpC69JgCr7t{Nbe;H99*oD@Y&>TkrSi8q>CAg-tGC z^~rIgLLAi`7^V%5GU(AxRdoV2PQ?dueLQa9-PV&8DRRotp5U5|{i7btva8K>=^@d$ zq<62VaQS}gK_M00xUdonrm|NFFv_Z;m0SfyM%q!g$Ib39$hV(XEwMv8exl22O~w5N zjgUb@Q6xZjmypCa*FF_Ezg$RhJ%KW6GcnA7q7NZ9@UaPL0AlVq>^7!#!wqT9B;O{a zx`*c?=RJlW$cl2CiHIiqG&a6i>b>_PkUBQ?1va5d#UGh18Nris{&O0`#I{OVLbI%f zk&~K!xyL&i4DXJi3cpWe1sSHnGCP}ZW?5nkW*4JqSc0b_?hW%;tD-98wya9Q#jaMc zq4WvSk`MRx=`O#y`gUzv=GvQ3iNrR2tExFpCEh)iZk?J(wNI%V_kSXVyV);(37GAY z!k>Q;P~9RpaU?bc=uc z8)Uo7=fn^veia^?+fQaMBQHj1tE;;YFtEYg_3dZnM@ys>=-6P!R`~1+d{KWn&bA@1j**;T0>|qxh*PIDBdwT1j=7NgPP@m zh?91w%Ea)u9luCsS=(q9+?3uCh{ij-z$B2wOz%SZ3rxQwH#co%y)L^E2|+3AdULgC z7n}KUxi!H=d3!XD6kg!(VY|YUTVE7qJxh(h9UxtW4}Fx?Ly7D4c~38+7Y|)2PLI8D zb)?X2d80kRRq`p_R;pv0uT@I#qnixUj2JwHpGwiJs-f@T8+oYvLH7C1$}a1xdStEE zQtJ5Nh=>mIw#YIbOi)yBLqpzItJbjKy&+RuCQO&BTNyLPkgF>~+l&^^LKgff;&q|{ zYs&_)uYnwx@O47I6+y4arEsQ&zMB+Y3N4P%nxMk8m*(;Am+#&y37azNaaTVSLWNWwKP_2hm-3nAs}(rcdL zfQrZ?#7&Q9RLp&@wT2WRm4;F8tGB zc`Fj^nH`siXvBaBs)0W-wtdDUsH%>%H$}5!bCH`C$+2p~6hfYs-fM zTaqQL@>mt--c}p*M6Ho!p6HxIEVN^BzVZSlm4CkXU`56?%s3=+J8y(d9BpN^{%+Z) z&uu0gFk2B&rR(SYIawP{4mmDN^v}ZQJ6 z=n_fYxX2D({j7K^d6wdF2lNgu-%x}Eo_4tBDnB$cnBuartW1f08a{C7ak$a*czHZ?ODoKw!QGkdcA*+u|=m_7^hd==F(ni?tI zOSYGw(7N+a-Gz|-Gc*Yj+oTf4 znVnuC_~*_Y_&e84wX`G}epky}2@l(P<;*PPv(Sr>gN~!8eqLD+GNES;+RPz?N3J9U zrFCl}nk>65PDEeYe^ zBS+YZXI9Eyz%UUdoZGaaVKzwAIT71WD9;mrr6P$9`q)V(RE)(M+SY24)4!tl@utTZ zJ#O33#e@N8H2G!X(A?{d$7HVf&A5$`jXINDN`g?-Tgb=Ur%r998}{B|E>K~c+Ap&| z7m9L^@&w0XvIt~3kc2%ACkdGP=HgPIRRE)bGiHN?ot9VgMhn-dWCEMB7W) z6V5r+Q)9%u0|1D<^D^P=wijbdEfO)>%DM$dbhpO z3$iW@rJWXhe8atmK~W-%JTPpDO-+V^<87g#RAgOQ#9_B~KycTR-9&<{m%j+x;v-XE zWKro_E$wS=(;690J)jxcwT3b>N+=$2=#i<+^53UJbiHcl&dgD^LxLDFhC@;y32|Bf ziW?y+p*-V$KeaPfT!Nd*Ea2=wBeRGBpYbY8U`%cJTe{^z6eZ9qRPP;nS>ZVT&32br zZaj@&@8i1U8>%Yooa^# z?RmI6QXt`X4|*OGin{i=kL&JL`WxLPlrEAI!cv*CeP6_?#R^C`R5?1>HfzjHma@yF zN*-iXCt`<5fedc_!ixJha@O{i3tcu`t2d}Ul!xwI(m1IaQ&J+uxQiC;Qr%$Su?gwV z6zCL8Ov28LVhkBQs)lzp^sne z96XvY9xG~*p4N+16ZksPT>D6LPrQ1J0ce>EA5s5lIThdytY;R+>hCJBQ(3Cz>*-iblsMnwt(gTG*e$B}|x9Zhoh84*4ig#i1uUUw)bYGYioF zEVTb>Vh*z@JudiqSI_V900E#E>svkW*nuGk`L^Rd!xf-vSVo5N7**$L8C6#)w9vl( z)7!ISDDt+}AeaIt<=v!*$6g+z!PzacAm$tW5YJu z=F;k8zp_V_)38x$+;zc1%T;IOcOecj+yMabt8JS$mYGG1_p`H~2Cf1Ftn@Ny7LXCm zPnz!o$ZFQE$3eG}vY{T(>dOQnbFEwtzm+ZSqS}nUlJl1HIS>7ka+UTuZi>JVzjWQ& zOnds@=dZC3YCJgK>!%Lo(9q2LJp0;iuH@zI(tBg!Ye6=yC4K_o7p$^Linly;JxmL| zzoYh70&TY>mUlL{?=4;8aYdk+b4@CL;XU_IjH6A+>#24ca=VRZu4h{hg zu=2L;qXJy&*;KvzuZQJfKLlaKxHMWJg)1@M@ixp4f5%!OS_K;>+I(^gzaNONc5$sP zkPuuHYw>RRoIg`PYl4dJQc1Ew;|)EZ>~wx(aLitKv<>O|^?5yJT8Fov0YOck{|# z=_J2fO4PW;pxCFxoS$;&0{f-xagVl*>pxC}hkbS^C)A01+4@9{{n3Ua^H3n`$8H+3 z>8zX7Oo;`HPzp@+WD;HF()ppnbfoJWd!JI{msXZjn8vFMtt~&Zf^gmWWQvve*3 z4Z%FZ(QiwL`$@Do>}z=)2_w7EOg)>MVB>R#f%&-k(TJ;QyPtMI61K7spBKYT*d;wa z<#1v9@vhK@IPKwNiudZQkJ`dL%stZ7DX4}X#gf)>oi_6uCr?IlpeC{>wuIYH$7^|y zSLe>X%ms$6MW+oSo`4?I3vJF~VwWi4DI(d&%E&xvr%-om=Y zZ-s{H8PmK8tp_rfJ%%~;;-y#UkUONK+($rtQVLWE#taiKY~FlB);V;+Cfz?qvb%w6 zXT9i4?e*MSYl?UK3=jyH+}&)ozIRrT9S0%|+%0i2XIPDoYX# zdbGU4tizF04J8P3bXWcBFduEK1TZ5uYa%bxTnvq8eKCS%_bkp=n}6jgo+BVD_}$Q> ze*Fg+4w-i@G)<|QXfTudxsy^sjT!bj)_j4u?NXOPi*avSpIoRZKZFF{R}gr5bGY%K?%S!}x~L&*ytkrbzw88exSe?&b>R@L#rD3tUgTc zlikcBTc(!BQcklMuo_ok>WKlvx@iTKc48)99b&30Z*6OiNG^D+_%uupFI;Ql21cxH zIz`vZzu*(bOTPeho{8LOYpb<*x7eGtLi{S*UcEdG)hm-?&Pd^&>I>`E%Y|6`5B=_O z@#ikJ9q1$t_C(|>7(Mq5c&d~h$_<+hEUK?R?wWZ0HNb~9p`VPfE<|Io6Hd$6)BMo5 z!_QCtTEGS)*R*`lSF_$bH;+RMYRHHBNFXMZL@K(yU|&S{q>kC)3!F*S zg!k$wX|EnsuJNmzjc2Myofku7K&y2X7ZNNPob8elUd=h)4VBERonh#mZ3 zY1)>qWc)+r=$5@~OuE9uoaO6dBaj03#;iQaxxuzhok*W`sbaz%86$>H1-``Dd)3xLa#eUy z8lH!UuHpMkgDg73Nmf&`CCN*V4{x+U=jmM`e&rRNoq=L`%Fj;{Mx{n)XP@`r5-nCe zoN|9se&}K`&!VCIY^XbV6KD4PB8jP7$+dkU!bAFBUD?)enscX?v*coH>kf(A2LxTH zPYObi+YatBj3ss$AR@{y>`4Z!^v9I`buf05OFQiN!qq3J@Ib5tOXD9fADmcaZSE&*k^R3AYB5|Eexb5spOCzB_K7rq`L$J zL>dG}j~0+F2|>C>H%N`{hG)P3^Ld`vd$Zm5x$8QwWy{N1}*G5iK|D=L)=_XU-*kz>a^dG>~2E|`~{X23NIQrT8^*xEm=(vR3aHh-U zlD^}iaIHZheP@2%Xw{0k1Uh4JcSL3h-~C$)t3QT0xmJT>-nLdMVh+P#Zbkh+uQhG< z{Wz;EeP?=v-vu{|UVARa!HWuno}xX6_WOS**wT66o?khGOVGDM@jZdcLRyK6Zakx+RxrN^9rn%6Hd#>Par#O9f1^Q*QjWryTmir3j0{z_gTa8KWr`xt z8KjmiavEnI3$Y)d97tA~oqM?iR)0Fne3uT;m^8_6BL`HCYe}DR9(|0o1ZeM3M;m+- zSXF7C;ug%FxpH<>v>gF2h%-NQ2+s&TrPoF#x+tmcKHFR1^rTu?5pF?2Y;jaM`|;(@ zW`87*Qwmy-xyoI$mf>4I;SYnl8jU`{Na3V{>+JN&w=Uc<=U$~Uss*kQ&60jemJ!YN zod4Yoy&3;GExd>^P?j212ZYK@85F!x`t^$dCm`^5xiPC-5traUH zM;(~0pHLlnvi#);xYo4@6G^Wk_w}Jf=7a@!K2tmJsH(GSJipRFU=1HWk{66Y(O~zE zI%0|Sg(BAQKfY!WG&E4q!59P7Dblv6z{%ZrfI?#2{vX63L7*x{q;AK%sT;4m?z~YW zWM7&G5pE0`HW!V_>g;mIU-+Pg+T=cbf)GtLc)A@WYk4OTAj-!rh`uzAg7@A~IqoMp z5_r&pg){~xo;zMvUDrOt@wNR~?xwyLs9o%`K#EHC(eL8pF|8$o&zUfBUcaj%mwTTU zG{0`yD82K!`33VXq5KNR&|e9Bglxd97p1*Mn(I?xT5;Fw*#KC;{H!KrPL!zJ@JNF6 zMpP|jqviW%_Y52aj;k(eLiUx{4#A|Hl-oe)nRama_b1URBTiqxTz9zNh=+0^4m>nqp~L?iy>WD<>SlpQm-8OJhn&`>c~GM;xGbsI{R{V$Iq+2%Wrzd;7J89 z#9)+!Ic8kGddluh@zJH;LLh_Y&;USwhDI$=)*1_fcS)gFq@#tX0LPmjyl{HTdvDrY zo>@Dzz+I@LDU$lZK#P5I$^%y@b^<%^_4@eZGx?$jyTxd9@$TKR>e;GtmW#RXviR>y z^19haJ}jwDe$Qrk+c5DU9aS^1b8)>ipJl)cOBtg0d8|BfhqB};&_9eY>whWSk}BhL z)|IhQ|M|mH;~m@Z(Us#6mt&!*6*)&Ob{>;n?Q>^LIOQP}_MbE>1h@+SWPk-VVENFN z-r??cqmL@AXrd*EnJAdMQ_1^asWV;HL`9-ye=**V^_yRzjERk5As%v0)msZd2~Q@< zea%)it@-R4#s;npT*>T+XLFp@KrwhwbQ+Xg(6kM>`11b#A%w{6s&qLQ2;&=iughNx zG}54yRpowl8asON!>QFW`Okg0$l1C~`=qc0;1V4;BjmpQ9H~NOaU)S?K_ezj5uJ)e z7cnET`1OhUrpU=1@U0Q&d^(DDmoKL-8v_9eb&)789pyFcle%*M=g{2hwm^VDLET~0 zALouj{*Axz2?;0;sYIM^WQZ8-e>ODx0k4RLx23G4Fa0JF)2mxJCnm1TnOfAH=(#s) zq=BHrU24{l(@ z;z-+7)CL86>|`ymbFjFNN|Q>kk44a*QH6tY(I{lY#AY_1qks8k9Rv%Z+P{*1*y8cr@?M#=90t-@7@DFH;C7P z+Y96mJD+qF^xTfiBtc=@#X|)GNm&{{udNNdNMxHO<-N- z%~*SKZPlx6pzyrC5V9Gh++3`{xrHllF!wd1HMZ0$Get`;EIv=q$}nZ9_2d4>WJpwD zDGuBqQrXrG>O*dHZ)4NM_MNgZGr%ywau$t zAfX@w*0W!2?-&|Oz02WGR?pVyQynqfSCYck>bqY5NBgPP0yXX=`qQ^V@#Ibp9u)7e z|Ls3xMEp^;mIO|uXDc$KbuW{;>GH`ZmM8EJAPu#}0*_EVbP+0ANGDmM{?iIX3BnIP z6K}?P%L2F95-NK8YUQ2JfTy(7ey!?HT0!6Ji#Er%dXoqEO2()`87lN8PAxDbj`mSK zvimTRmy?IczlUom{VELGFu1LbaV{vW3_j${$at^oZ%r0NEdTW-#4+7PT;>5Vs7mZs zDy$ExAg74;zS4t|1Rj}R6=Y_iBZr0BC+~~qR?Ssmc;;naMb%`$ zF$b{}uh`LI`D8 z%B}o!*)Qt!Yso?g)m%;;Sq{aN%2cZ@P2Kw|)ZfDFFh8-Z4g&nxw zRpm9w)vx>fq=}YtD7KHnO^9yk3vr}b2>FJu0wbp}TUBug z8wd<}p{a2jD-sSNwI-!Yuuv8nm$aznxCuY9|3Xlf0S_y zR|{!{9qb$C>EX-YFUMYKNm|sKkJ!Q}CY~|l<^GaOo>uTC;^JQ!nu$;ef>e8q_|f&d zF~BSAdGnfjA6I($!*WtFTXzM(LfeKlfb{T{~P7lwB3b2W-vSW|=u`<1OJBv$sAd}ld+z?S+;2$_gZ|#L$v%tKryXNGVtWP9LRXe9wOT}NXh!j z&I3P9NWc)}Z#iLljgrC&LO~Ac6;@dqgF)f8mpKPb{{R;}Pb3G}P?z74Ej`)8za8QQ z{#ZFIZoGy~{|Gj@s4mswk+lA~>%T;uWXqr_I_er8s}pL#xpKx`cn3wl*gd1%4S%EJ zMP}R1yF3gaqz}N2gA$?&=qhB`PDkChKuF9H4Z1~T*O`(fY`3Wh`C;5+OwC+cdyr3Yey z@N}2p6t?St71?HzmA@447hXJlh5nvrfT?rU zz2RCkbW6%M3w`#m?e|O|oYpk`ph23>hf;=dG{>D774KkwAyr3Hnb;OXen(@g8aT_k zIZa*$YC{?y$iH9kQHRJtlB!T+_kFzxA2*bYD$O-JuDeMEuHE}}$eEe@SVqmG6RL;O z=*Bd}(>f0V=g^^^k;653D6S~u#*zTli+p_e1d)u82@-tx{1Xoob}2sPRJ<%eFlI#f zNbkSB?Ns^u@jxgh;5_r4N5q2apz~MqS20a?MDRj#*!dgbO}xp!0b@rF;_8~q8j}3{ z67O-hf2T|-OG)x)+}3{F=!_cl5c6!Z4+D!>urC_2sMqU9oub@A%TFjS$2}g#0bKoNepsB zX=dpHX(INRE)O58uCVbUm7)ZoFWVM3Gbrlw(CT(He)0VflnFwY@T;>HbcD^n+0~sd|RbA14`=~{|$*OM= zk&z6v0~x0}WJpZ+%AtJAu1M1=8 zQ3-6sq@xQyeD3V;eeApV3Z{=_C`!>o*-QOEVamDIN6bd%qp;#4F9xoO+?e-a-p_K~ z`Tn%V0GT?aC#`yZRgd#5O2vi^H>7Rc$?VKZno963S+}s`8#diJ|8L(k;%|5+4g`~V zPv{$J((dI#QMlbMPrMMQZQ5%Yl~^-8+S3&v$!Ou?)B6zuHN{4Vh*NHDbg6o!ubEiLQI!)Bd>#Q?v#IMGao1ua@J366 zLnf;!&O#ozLxnWe!fEdOQlA?)zq1U!>L0ApWK#-98OJ&m?0V}J)~YjPD^ny|&El?l z=3;)zq_r#2tGH`KEW;obA9u?!%Yrk)X}L_8!?N<5D?S4*rY#c|h4<$Y)+NCz5;qSj zVL0S)R+GvLw7S)wQHC>D$654+k3Es?kHYa61fOx?b1ySU;psCj6w1ToC>xKp(9woU zvBsFy;y-0Ra23~^fwpagH$geZ1;P-U99}Veo)w4CKk?|2c3LUDy&NMhJF?AqvgX1` zLP0^DncPhv2ZyQX8?pFQY6;4mG*Bvlt1LcAJc-2Msk?ES?ZeCw_pgkriDd{L*gHc?v3caXr^Wb&?MpZ8ztii@{!7n0pEfNsQ;_jbj> z&DH@=q>uXj1?U8cxF`=hKcc{{k?~%L$!_QSTPX>5k$*IqAl_(iCd=*X81;-E1Lyu_ zjlt}MjxiJmRGL1IB|*z!rb6s#+dv98_%paZ@|mWxdY3nuJ~bpjrGa`PoHYuv{o@^8 zJszUiD)uuK3Zu}M)Vow2$~CmIj2U8}gtuf|L^keL^8h7$PU)6tzOmUI&yns7>Q%zU z1yB)ipHa523=t+`Yp6GY@$2PD3b2sV>3*{*GY|On$c)C%J5Aa4a*RR4>_c4CJJPbx zky6RuKe|M_h<<{PYTQn+g8Vsj+kQ0(-ZLEd7HhbpY?3Y^?ij{+j}@CvLw5&<`Yeb6 zz100OQEzkMS9%aEXvH@#t~c7B%im-fEt#~7#%(_Nk8p^A2~6(Dvsf-$N#E-ga0L6T zs7jyj8%lV9Q1%)7WCNUuUGpviHjTXruXX|{;uhg>Ex9(rdbA<<0``V*FUU2D@`3x zQ|LS)l--~`K&JGT1o1Ob7rRx~AXq~SavoVzIBoa6n z9b({~umz(XKA@+4Wjf3FzGur#y(O0HgHsgJ&tRob6IQv<$LqOG7)>6Q{J!gkl0tG6 z2Wside%X}LQH1^$$Hg3st?Gw5OtCP)x_GPMw#9RQNnPiuD|FR*Mu3YBH$ zTT*l%de`~%tRv#ixBit8KRAXwckU$h)pO3id6zJR7iMKr6L@lGfPFB5 zGH`UGES79auK`cv<=3Y~Eam8RV=gF2sA#Sxj+HTumf=KFunH3`dFZbl`bD+5zkrl) z!G6}4P>O-)=SdI4i^N}7|0=M9JIMJ>laJ34^hr8P=H#e#lR1KhgW|QNHA6m#JE)8um$KbYld2(yjVpWc!rS z!Dk+4to)2EC3SWiKGc6Z0=95D#MuSU-Gcw|rpPID=>d;b;F!^k;;iTRgs)x#g;OJ0fTzDN0Z zMF&c3u5=;cL9|U5KJZL6*pNo#&oBD48}SPTBRgtBt2nwlIr?VQ_S5idkpV_=*`R#7 z+l7qR;Vz`ZhAKP^8-5;*OZ69XnaZyez1bQ=LtZvoYqtX*%iQ-lAFzK-aM>QY693xs zRo~M8n)+lX?MwhB5MeLFVVe(qEV}I#O$BD2si2a9WS>4-7LHx$2G1{2xCS)`ROI|8 zc%-YrtQgk#vuB79`;B9s28`g*KxLsGHi3M$^?^214MNaaJCbY@~_ z1+M<~k^#($+|B>ePvuX#n3D64KV_?q=PHGDd!$Xtx%Of|sNoG`s3_d8Sw4Yc$F|zn zH{T=Ns-H4t*@i_L1N7M=gEJO1Mds5!8Td@mxwj1w zaFV%2v+UCowyI2*SY*EkN(lULKN_0n7+l#$>gw$9UskR3;+KJzT$>|l;u{97fe+V5 z`t~1Dpwb1sFbexDx(#F_{JDdTbffpnET2qSv!jPo@6!(zpe!~g!ObnP9i`0IPye=+ zB`CyEafuIu3pr;|qJL7zuZF8xBTxxtR`!M7+>1|W9nx>=j)|C38lP>2o?ZP)0%lxb zEJ#3@i8@)|sLeKO|34O>qiNa$RPkK&(tiJ)7}$j4}te!Fk{g12ckD5GCuv&hH`WOL1 zZE_QX^C^?gE}BX(2t>bKB?zt$tYZVu$V>MU(SE+}F^CMF-@+$7fz(fhQ8GIY)E;6W zPwR^ON52CWzJt7XfLkmfVzmf#=t7@PeenFi@7iAn(jSt{Xi?M4{@b^d(|)A{@Fz#f zo@5`9V4pv!yK;YAf0|ie)|1qRW;svP(U3a9O=D=N{w#D1FHH z!(E-OpRQrCI_8(?7g6v#?d0B*DT+u4>z&#OlQoAVR&6K%_N&ucCa~#K&T&I}ga$9U zz7L!*Bk5UjohpCe#F?xOWhI#iH^`Xnq}7;8_`8U`{_ZpIHox#zcahUj-25$@u?i}k zAW=+j0ux0I_O#kO8p<6XuobiE1~pfdMII84e3M!!tMxQI`t3ZiWRlH$7bp(}^6K^! zs}fNS`%^MKt_%z;6yy1Riaj@^0cCaUSfieV<=8(i<1Bz0ta4sHmHE~3)W#vZxNbqV z;51U0n^?q-HTr7FJ#6{kGjI&in-~?o3+5`kE(xnNrWygSg8nG&B%iVfG`FUC`f_Rl zvamNx(zKTZgIx;MmaB2-9(OewbU4Uqe1l?DG$b@`dr{|TFt-~Utb#LV|3}GkrjGA` zB=Q&2Z(Eq-yZ9;WfBARko<6A^Y#6F8+>w)@ilBFzo>-Ox?Y<(o?1vWGTL+K7eJww? zY*9)4J*Z>ALh$&6PjFI5ETfWo%uwB`DeBBPZ)N6;(1cBk&)e;zh`DbGTri5ZQd9w? z3keJ&;k;d!WQVl^@dz4_|F`gv4YZ4>LxSeu3?d3KOa#vg>88ogE6lGj3ARe1K6FMx zR8r26`WOI#J%fG`GFDycWPA8wFgg{r_?B810qs5Ir9M91bpPh;I$K=N;kzF^9Gqtv zKWjimUab1Z*z*TY3J;ck?e-P@zbPjW;_iXo6as`)R?*x9t_8c)IW&)zAOBCu}>kbpB>dupW_P~Hh(!o>t2UN z9P;?r^w#zo1FLTsC_^$@cJpL?B)OQ^*evr%dHr~r(&{vj>6pQpVfB?Q1M2gYvxvRd z@hedC>1O+u~1eJppZ?@NxvZ!E64dIrQ*LJ$pP zKZM>yQ(%^AOz0y=3=3~DjZ6q{NA9OgQaJg{+*Gy)K)R$L_BMN;>saIORq%#25$&Kh zOtJLa`)I%@0pi&A?Gl~nd9LFfwxMsV?Acn9C=mwGKD+hVk1`URnEX#)l78a@D^I31 zdh=Dd*k9khbbMBE#Zhwk<5#CJ*p#GWK@wbcm|A|?=9DGzM8&{ADO)KMd^n&kr)T(rF(>g@rh* zV<5Xw0fJXUK)8Po^LhWy6o==7v@uLKbua84wi-Fvmh%f@ zxPyM)fLjzSUS-aRq$Xu*Zp5_fFBYxN+``2B-5x#>oMfAX2+aeA6?9vLN|a=cPgZ&kL4UTr zBX|pv%6ulBF@>d2P?-OUU@p_0sY$8{+tw}Xx(PN-L{K8m^9>szFed}$R3JET+TG9g zVPCWk#qU|c>%i%%5OP@A<9oI(cSJ!c)eTZR`8LUBq>#;}YYGwGPcW=7BzYIY!}8_I zFf0Fn1XgblNop!OJ}>8%g8(f}8z`b7Nd@~z11r2WYnZXX8{^M;u@HAgSL{R!xcK~) zo7m72qaWuBtAe|?2zmF3b9xpK(Pjk$QAWNm!#3Ah9Y}45B(K_Q9IGEs!x>Qb&$hae zP@@~fJ6*lhvq>*Vh-h??69BsmW10qu2X+VKajdouN5YSq+Jz^wWO5#)r6OsRGZf4c zNVS@q4hK1Jga$+%Q`=fwEo;AkDv0B^1!s!`%O3c^eJeT<)09PC`6(j&d82aWk0(li zI#@X~AP0k-IQyHN_EIwbh=nEIFgoz70tHyZs4W#B(MFCGx7+V5x=kOQWk&i0 zo*LeQVtKVWD;2g*>uAlJO1$hCEu#dc609ChQUDi6e4$b2&8iEY=KZ8JU{N}W`0N_~ zF*J{c^C{-vw>a=*2IN@@edBE;Kj^_=i{_7$51UuS%pI+Z3<_=aDSQK#dsD8x6@!iV z{*j=6HtCk-JaanbLv<420iD%?G)S!M^C?E%JLAYj2DzO{SvxN9lk}v6QOHYb1YjBi zDW-#?`Su=kb965jZe*c7@N_{5E*tDfL*>xah%39uJymHijR2pP|;f-?%bMnb}9XRfz(_nYl{(?ReXe9I*&&{EoQb6`*g#Q^eEyr8W=CJA?S zIx1;z%nSgmuwVMOXxj{G^NUWrT{wY~M`*X}_}klFEB!k{^YGCbG+6>+LJby^us>FA zGZJL*qVD73{PniQ2#1R%g6n{vQzYLzh(`YS-MXiF_aNCi$@Z&+sx>N{js~jR(Fw6r zqZMh_YB;vkn<`}li?@sit1`zz$zswL8uTZ1wnxhID(-gm&br?-RJI4cf?b*GQ%uJOu2~-)#q6>d?S; zrTXyT43TY6vXyYw$fr2- z4p>PIs}sE!!&p;cuvo&vPz=hzBFgL~K>(Lg#Hm8rNe`BL39RAphN6*z1yzz$ZVzY( z`cU@#Ow4@G>92IdbzPJuN}V+4zv0Y8U=4`zC>G*OD*y;UoZpReNGd-N&VI^6uba%2 z^9SzneySs!yk|2IdRZo8047Fz?ftEHEEN_*vGkHUOd_>KN{#k_|Fw#Tzx1>}FU)aI zGdURD`}o*sBBY`>h~{kZ$Dh;M+6t*L;ybOKFy|^Nh6Wx{lYpD?+EmL6tN9b~m)FW` zC#!Rnf{&g{zI-`u1ZWqFVmIbQ8(33Rh#%5HG|V|OSSWgr_6vw8Tg@Z%*h`XBXTKO$t%&r>YqhW}q2BysH*9i>h_isqN z(FphV_whZ_3g}SVkx1VX+oX(6JN5)_iKKW28(g?u^-alk0si@0r1HU5$Xgh;3qPcU z!_R@);W5Uqwj?ibFc?yaKb2vQdo0$YWive_>j-TameKPkFUC! zJ&=nE3XY@)e}4sNya?_Lh7hS(1=HMiT``T@#IF)RNF6lN^8QuRx#ZPO!K+44a+F_S3JU7aBDe!M4$R^uWYg^vl5NWjFb>FH#O@f28HtIzt2tnd| zUd6k%)4->0N=~I!t%s zJoV}f`gUJ4o!%>BLFtgjKkYH$Cu8}Gz|zK4@M#o)uzdkDsj8T2cbySrzJ8y~ zp${Trnw}^9{G@b9LI6?u zJeVf#WF%+&&4Q>+nB}Wd3BM5mF#oL2EIX{-{Q}#vy$K_iRuALje;dqW83vH!v1?Bv z@>Bju=iGW1gNz*(#QcnlE}Qgl8u7I HbG*BzfYyxGo+jbXho+Q(k@z`6l1b=5$q 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 literal 107436 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�YbSKlE3Z_SaFQrSh`{>P2`0pLCTDwT+`x^U8~loqSTwJGsrLKNl%s+ zM{`>aR&S=`{5=N8+G|MDwilN5+3MsX;D>LnF7Ug#F28-8p}oAg^sTf48__18gv@Jw`0P;=t?#OI8(SDFzt9S?Yg}xQ#3L@_aa6h znW;w0V0F44ML9t&I8}SS1BJ#wpdwnF0O}H#O=Lc%VDMP()Z@TAX!bY_yja@~+PwqQ z2*%VNoVBo2jgYF)wePNisioIj7F+;VC|FFo4B5GX&MM3n(c^9$D zr^|h&w<66^PE7T@?wV)GW}`g}Fv`8YU3SD|3q$nGGH9;ou+?Omx+9!dtgK=;k>JK# zOZ~V0M2Wuqu?(2(*=xbLH#0})`_ujeH=_2tjsb=Q{JB8Fkm)1|>U+JDwRl)s_~bBd z@G$^EI%8x}=!IU$d3S=$t}ApNSm2k$O*L|9)6_1ZmVC$P{D(8b+Z4c-rpVGQAQdqO z>#Y>^aBZ4N$;Lk}QfmbAooRXSj&*=qAaS2x@Dd^pj+KI%ONU$P-JIRUQ}LMi2_y#V z$(3Rx?Ikj8Mi-JHs2ZU2tF>Y&K_Jd!@v;Td;-u%BV7co*VoLPteI06^wn4V33Le{c zmVJO^RZUdmt9^63qiRhRc*W{bkLlgz^#pQ(pcfN&fc09=nhv;+F?U~i8lmsNemWaH zG>-UN{1gA7pomQfGzJ5KKVs@8~)jD(AXtw2Zvp?WJ^rghVb)eBI-biK^ z?D@lhb42k?@%!wD=zt^vHIC7$PTlLGFO9{`+3wH1k*pqbKq=63##9d2^$kEU&GvtE z?T$`HnyoZl+>CBE?k;b7Xt^!?)&&xGfQTWmVUsy`3_hD2J*8dg*ACG3}9 z-_=_CJuqPhDMYp@@x(vlh4PUqzVo=lMId8hGbsv7o&3#nRMS zNZ9V2l6H*x+1WilcMUg>Ro~b*c>)i`91d6C;|#+0(^XjFf48*wm`b)v$h%WTc_{>kgy6hK;du8qc_R8PCv}DW!>?b$3tbW)z8eD z_Bs;?>rP;IeqK-NRjjTZupXH>YB~*9xX8T9wQ4@mpN6U&O<1zp=U@L?ACb_sPQzqm z_~_wovPzu?nk8myXxKQMc)(gfclD*!h;3sxDSKDt-m=@xV3lUh=sbQD0-HDVW=fKE zuV?}&|D}@eAcj-vwS`&7v94H`L*>u@Bq_NY-)rQwtH~|R8NnwJbnf^*!r%K`Rpa!E zNLSTmwl4FVs*8m=@ZX!Oqo2c@0Xlj^jl1N44UhR${PxO5qg1N+W7PGhc!hZ$7sRqf zpRd9dYr_$^a5@TPo9MB-Xg{RGXkWcv{G@U+kWXwX1q5WrNCc;HN$nd!VWE-N&6k$t zdl$wlm~trWht}JVX-rb zI}exH$5XazY}?T9bJ|UJQ9fVijsbI)V`v!xOr)ff7jRCuKzi6#4lCyl@b1~n=IRxH zsv1#ao#Ph%*pZIx9aA%K!N%@*I$I9+B$>y??HB$Q?Ed*O3b6oI3111QU4A(k0IGnN zSc9m0xc^|`|9GzWja<|AcOdq8Z}<>^I;fZrfIqkR^e^VVvpjaMmsoNaDDhKpU;WO}NTgAFh%|`~A9jQK3-^I^g2} zb+WhXiJC!-H^&LDBSIOSfo%>+`@hkT*1JnRMd!(JHHr5R!G9P0|6YBR^!^9G|6cqD zQ~v`h{a^R~3B~`BlD}g6SMompef{yX@ZKCz+uzr6q`!TPHc|2}oA z1b{vNA9qmE6IKov9&A3Ye=u{pnZ_F#8up0ak@|nU`5$3J4UwzncU}lP9sHa zq<9BtJw=gu%j5r)oM#O1rs~I$XNygD{x>NAPHRWZbJT~V z>3S(2@4m$NtCW8=`X8OOr58Ng`fI)LB<{m?J*WKS=#uDs%sKzqKK)Vc^Z;gmz&bB@QH7#5^ zcxj~_>&^26Qjz?L{i)9oPU9~6X69BSLcsI?H#l&Bsu;NxLQYxO;Gcma6`XfDpgmN! z2&3p8--IoErh07i)OBfhtmt~n_;cl2mczBRs6XGXj=PQNU8XN(zjbz$Mko(&i@MEOI4sL ze;uCwSL;y*;#N)nT^Zb}4MFEuwyrOb5|o$vuX+9NcmAC*$|U?d%zsMR?Ti1*;oVxV|Fld0KkodsSSTO( zF9pCjIxk6Zv3y!?{{Src-!D^e|9R^651?fKaf#BH|GotJ5=)lRX{+usyp4F#7jm&m zK#2R5(YbRYnzlC>i=l=`ro-UW{I~}8;XPvst?0V%-0_1eA)G<#!-+9S1Wu~Jnw7!D zR{#W*AT|9ut6X~OUx{Ec3+dNN`##a*su-7gCx$(q2(p8bkv=_HmhX-EU0%DWkTIkG z6>A(g263gge9X_-3NQU*OM0O1yQ2LZ+%aZ^=ErdLn>p?yM zQ}TbT5D=;^K6iwNK{sjeixV`#Jb1yTJZ~5%u)`CsLbJ8q3uF1=LAyZ`W(8i11`NXp zAY1ftr+og!Tey{mw{7Ow_Q-V&jDd3jsIJt1a4+oYPcHWGCs3DRtLvpdG+MJ8Ge!f9 zPTeTI?K6Ohg zreDVwU|v8z+M@U@FnYU0bU1@~+zA<}m6k|MJy_z0p-C0$Gf~b3j%d^03K~1D-X9E4 z?$l1;$~nCiSr536cZkW5dtWoJCFco*n3J$^MrH@^v!rwuBQJ+wa=(Pld|lt?0+?;^lN~Kv0lGnprS*pxtC}JL^Zy(rO=XU?Gk|9$EMp`h;ynR+VedEF1nz z!!uta$10Hc82U$g=x{5cuM-DJ#C0Oiw>>#~(JuOnq?sn67+iE%`1Pf9Fl`LX={WTX zJa19AHCtt&VtZt!Cg+^%6_`JzS;efS2$5xWyO>HItmEEpaAta9KsTsO*@@sNiKe@| z*6(xl^hL$A5t^$edp6x&NfBZZPz`zlff8PJ!)#L!=2o!oyx6-{(P!3pa<8>E5@nB@ z7B{t6oD9X?b0vvId=bM#k7{SPzVtIfq^J@zwFr|=m4bsz;L+{8C7fCk1jlabq{MNu zQj5d0=Vn2mt#k7

R4>G4s&`9qZ&tE&8pHrGiF|AE4~+7v_;s2OeiJ4!op0IEy|@ z<7Sx%^zZY9wiIYqIVIz5=(5h3XZk`{p75CjGI+iHbKyA%XsfbL1ib%#AyZs*f2I#p z(p*8}{Q<4idx??!w@b$3BUV{Nb9Gu^z?o*w*SpA2gM*NAyx|9E-aKh(;lbW;0Q(b_4`0?bkCwoO36)$L6sU zSV}K}e~CQzPOcg}QD9YpaOXq`f`J2x4TAAMdMbOwS+YH{Rg-hd#S0#QGTl620mEVexA<#VGOy$FF{dHZDJb&nJ`pSLx?^=Wx=CuDn>%RU zP3cppKxmuT8}!o?MG;zmGdAr>lQo=@$3MegN;(7V7apWPEJzVvX5)nlWYvEvy1e}m zu%76WqSwMzB5H6Pa<3?krsIU3kuRG{;gsspJXtiUX_^UBjiA+NeRyb$9ifOM;%L_Egi9~Cbg1c)dcMXk}R zRB+x@E`xK1t5-;8W~eP}hQ&(Q;8*sYsmQC2EVsZrj25E?)7gJ&j4!w-V(CuJNr+Jy z=@G6ZeI5Py2P56~1ctI_qs+g`keYG8SZDHhXO>5g&lM>CJ^0xJ{nr5_8-=nMAKUvl8pCRdW zTiF#_UUbaut?Ov&y!1qxRxJ|{tw_-UCC0J~VM0FTNZR^V?|`qo@U*=F7|XMm73}C2 zl!pXa7;vSO(Seu1Apqw~cw7sFzY?Ok#7lRhR&b)#G_Gi7*8t&~Jehh4{!pPaVZ>K4 z`1mRpWr=E{pnbb#xV7`+DH$=NH2*nXzn{A~uj4!9MpG+6S4ZSO93eNSbl6r@;VB}xG9@mh#C#87!z zQ65>gdUANty{OXg$;vZr#5Z}*!~;bgNAT|Q8gfdo#X;;cM<$GZ6haB6p_M13GQ@!FbArhr{l+a- zvV}J6=>lUmHowa=)vbRkis9_Y*lage%mAQmhV<$Do8vg~qv~;~)bw+yV4M+lY|Fh; zGYnhS0@93{0e3xR_*i4tbhdr$=jr1fTPw@w(r7Cq)k1lgdT=gy)?^IYpChb{^YZ1} zxubyMOLoEf?s|Ho%X-t_KO&{~#Cxp$svfJHD#Vd2$7n1m62-`zEBP2`K$y{f5oL^~ ziNHaf9Zj-Cs#UotJx}#@IA7{gKlEkP$_vdcwgqKG$hsH!24UfYO!YSMlQB)EvxjQd zPPv8w_1YSKj1|f^oHlT_I<3t+J>0#DenFw>!^QoPQjr=@B~W!564EKs1$+VU1O%+F z_okk+5sx9i+5PJA5J7JyLt zVaV}*?r8r!!*k_=5Mgy>DI4HQMe5W()%;FOY5=+s8q_4q5BN?1gSecJPy#2`Yh(a) z^cxFd^hz4NKV3;`wL75gJ+R3IlaBi=FF%*r*3m)zun3`O0%1NaU{xIV64!#e{NDIM z4;Oz#Cjt(W+c5W%EQe|~5GJu>9WD&LJe&DM_9i^_QrD{ts_D&Gx>;obKVMASL^V{m znR4c2OBYBD*Yw43m=+g18_AX})l5MoHV4Wcz{wnA3NH!<%(H`I<4tDOAj+nxvLSqZ z+!Z?0C|xv#w_fKGM2CLsVYR^J;1R{ZeI1yw7_~d4+c7>V>f9D{_>c$Vq3xQYp61U} zr4jEZa44Ll-XZXegrayplaYc#XHRW3LeRh((MhqHyw0pbF`%8 zf^di1Mo@nd?AByw^O+-cZ8Uql=KEeQ(T({9Ald5d6V{p(r{;BlDskmv(!!!Y?CS-R z1XD)Lr&^vSGk$ii6=Zbh8eYb=%w!YU=U4O*SA>lH81s^4GVonYZ|Ft5rBpFaeH?=$ zb{e7Fi)awki)B7E>dQW_`HsAWK1xh-Ex^uiZN7ka;C5YJ-C!lejm_Qj!&&$FOt$Bw zCdf45ZD7GBhXduvh9yv??=4dyLk*d?Z)8rFVh0QvYk7X48+%6Ld~hQmS`9je#*-4$ zL&iEgyg=p`=A|I@jdI(Qmd0oABO+en9hdIs+fqx7B17YC;lc|J7YsF=!V#?Q$Y4V= z-#`M*tqWLK5G`B*;A77T7qZa=qwVKvs1R}#(nrAj>n$?v(}tZ|gX`iBzJe2ta7!PJ zk=Fp@)A0b=^%~VfMihXV&H@lZLWb|grZ^^1O4-lkkwS(KW4c4qda>$%+=9DBMP7jn z9GQV$B56_#cn~8t(;t608?!GKFlXHe3ApJ#qDr=WF%4uV69PDCz&~Ta>AEtl86@>q z^LSs${DD*&%$T0{$(h^dJ)FIFZw!HN2;rAxed!-e^)u*|ClQ=N^F-7nZ*9+Ki@MXz>D2CPy|T3DdFNUDMYeN`ver0r!wQSY1kw)NMz}RGFEFbN380?+lWJY( zKugOLw5sXHUGwFnGB)&XPo+)4b-WWNiF?(`z|*rC7uWUrzSWH(Z$ew$rA_>yQQ=JE zx#d^2Go|9Wg}8T5mTCIDHle^&36C{@7$BD?f9W z)IXQfXQ&bqFrQ1BrX=w&J$Wd&#Qx(hCzy(iW_KJ;llp2X>+}C>*Cig{f&C{4k!@0MS`~-d8ZBC>enLu zyi0R$q?B_@;v72-8dj% ziuRDE)wPU)`}J1k20_Fz4182icAKK7m!o#uB;Q8SHf6_-+20^N0SBbeCqf)_I ziMg>U87~apD?WkNi1$>;ZE@`WV95hs$&iQPt}>pT18X1KTG_?j&;s`jMYoPtWM7gP z=qyct9;~eHgF5JI*8R-hwgG~UO^c(-W(cevS#}BWi!49%6 z9o(?Fi8pN;(vIuu1!JfjlS$oJO2~-#{`n^2!(YwYo{UWYN$RR-(IGABS}Tixmf?$Q zU})i11LOOAAl+PqoN`tF?VC_trX9tmQ8>^*e!k>8ZQyPbmQ!M0$uYhA1BGDExKaW4 z=lgt)d1sa==A|ohpIwL>5`D}gu0~r`wY4F)jApV*cF|_&(1Z&BI#t3b(4>lU`SACT zH~Zajd+)0p<_c3mr<6ZDW#6tCN(B#(6%zVEXuCQK$WwmLRZNynN?%4%kT2rAe4TF} zS+zUTjv90njsaI+)D`vsKBkSNDs|?w-B|sjNG>mRw5jL)*x&c4aZuhs-9D`-YBIk1 z@w8e04Jh}Fi}=R;tl)$0;aNK;P-P{{qsTR40+pIIY_=bS&XVVe-E;Vn9aVS#Hu_7D zlGDLsz8+TjoJlidVPi#B{*YN~xja&T9C!G;vOggKIos=z7!3Q5XIsjwKaCjqld=w_ zTjFI^W>z@2>P*~Y+j9Q`zycufUc&5`RYA6EF_Ja&JxVP}WXrCAl|O@?zD#UDYBoDP+&>s!o?S;Uo@ z*Z0{l3P>BuH0cCKjxdrb04_d2b;iciu5f7GayUJ zpVj=(wCU}Wbgw>d!fERCJ+Fe?(~t7t%&Kz}9izmBba}~j!k|i<(j`T9umkV~W<`&f zTWTXgZ?c`Ecw4=$<$?;OuYeSz!FLjGAhi@u9mkOzdpDM%V;pm{|EUs&94fKo`$BOz za-O*~V__=EA_QS7*7gcbQryT5(XJXUyUydDV=sRg0$fYA4iTP<$=k;vKA=Kl$-XUQ zc;5%khjTxA46@MtpTGyTi`T7ajhn2E!!-@#54jrra7EsN?POfu0F?@jxbw|C)e3Yj zD_XGv&{bi)=6xWi^O2Xf{P!OflNPqSl2_o;GdK5`*JK)d2fquZX~(^Q{84y@TF7%b zOKD>SE_ju(X)e>23mqQ@4oqBo45uUmutc~HYuW$mRneN5a(^V z-jZYfT?I7Nw=8?Ggk8IUdtcZ@U{dBxJ&L>HuuHV&$s-k3=i}15<)e4ueHO>HnrW?? zxd$j6aTG!ZPRwHuHqf%NRI7Px9j&f@j9GC7Me=2w5mDc~DLMlOiD+T<%tm`mD+N_H zf^AdI&Ke9XfN7K@bA!ZZ(IlB%^kBmgofzBEGWrkkB8|AFUz+h`y&i1(H!Gx=MCTTB zX6J0I!=;6<8F#;yhhD;g(Wm1OY<0Cpe62b18F_>+pH@X&(v6-g<}hyjss&dv)Ls6@ z|L56K7j-+&8BGY&uithMk#}ax{p`{qeZrX!Iw(`NKVh(*H-q+PPy3>kmz(%o2>%?Y+|)o{_MM*;C% z`A>L$7?F1+9ZS++gKv#B*L#SX9SEn?f2-s9=diQ_`7mC1rdla)s8W3v6uiRF(`@8Q z0E;X3FwC_Fx`eC--yXz$kgw85CxHoYZbpST-KNPl_L)}PVvN$3G#0Z5-_O-u$84_` z4`r*dmaP8z^J2d0YsQrP<`F&1)mC*;^NQ&lkmod+8%2E-$VO9qR`;;eT|NF;8!6I1 zjd*-~lpXWp=eS4+Z)*Xfv#z`8T6)zVtG^n;HZ}GZ{+bsFV@Q>SYB6>_nr-8)Xz7~GQ(g&ReUXsFGRK!iqeU04j z158RE_<3YRe#L`gcZg=Th;RG|4=?bgV~IY{{#Io|{GubBrM4XLvzeA9@$qKuTPDH} zt!Kpv?}UNlMEH7YuiVKP4Uh-|CARsa}d$R z-9XsE-;WrZ_Ef0u~E1>@+g)So$4W zLVFdkihE#Ztrn`<)>-n##$4mT5E<#W0z}$U2Xs)=<@@ih_8V*Ypj+zu6#I{YJcJ=H z3!RCs)OsFQ9cC|#C}?GLKUS%Dnjb`AJonR4N%^2teQ>lP`W%tBvU&4vYSt6gJ57hd z3WBP#iAPWT+Y8`LM)q04FiOlg@@1C8%S!-8?x9zLpi(TSPCev09WwwppQ!1n)|0%r z@2eY$jw;-!?Wy6-U0ha@Dn3j71ObArM>z3sNrROMLINLLSTS*CwI+#y?A18%IA1T{ z`}HjhWVTw6#nknDZxyfLAR>(abardF%nQNvQYnl&+&e!8(OXa$M)cy{qS}7&JC&3b zdQ!mxBAmNYyNXzSSc}iO8gv6P9kxmERAN<0gQb9Ywk#uXK6?LS%$N03O=- zD6#`xR32u$9@a?j-u~?Zc|VicC?H17t5>`2$EUf*adnnRB6-nLf1*@y4?(-a8|iB; zAoin`#GG6aTDu5E7)L@8iF*R zPQ{Nw8D*Y@NvZ6qk2JJrE3@vYD*58#vBCxn82FR&C`8(*+R*mv%Rnp^z(6+ zI`0cBXV(cQjw?!J)-d#JCnwdSO5cja=qC>e%31-oA`m=qf#BJG&AMiCl$lLf_+s%g zDa@k4xTRo4`Ot__Y;p)5FB|klo?lG0pd;f;M2u|qmx$z*FBK`p&So|(kt=9@sQS)? zs+@FK7bAO;B7RZy*L3c8YTqRkpdOKBb_P#+-j{`S{xI4PURq_5?;|*=<)S^NXK+&~ zTups#8GlvAOawvfyI?7q~=l<~R# zVEAt_#$fRnNUEL7`SwquT7&Af`<6Um6DTymyg?sfdbl)m496mH{b>PSc-O zGN>&R_K2p9-NAdN(tXwr9Es#o3hcV%ftRKC*n@K9^D(9VyborjYWt^=>!)~oyL|?q zkm9?#KSPE*-`8(I&XUO3ItIEQeP0+}c^%F7spW+TAVfQ5g+{)5aV(p%>xv_)5$h41 zni%j%*_gjpn1*ahF%7 z91SAohsbHdbPKrLz!{9TwPFq@k}M0~jPo**aq=Bi-?jO$7=EMaGmwJKQ5MQFOpY~3 zYQD^OKC)rYc2;Kn-u*b_oSEaLV_)o<1#@5iGc@8yY#?xTMZ3(bW68 zNi~w5pb0oAt_!+itrPMgJ={R0Mx2evP~5GRqrf_x_C@rEaE>R(4wBf8slR!QN!qkO z=dxf#kCuG9yrAh~hV}h=f`$9K&EzbpQ#{Mu*|@jhx6}^4qBTGK!Ag0i;)WjA6QZ33 zVt@^Y?j05ta5ssa*#n-%J4$l2RtEJsS#E4uK@njOYOm;D)NO z3%EW$!U!S~2N^2Qwk8v38&H4Vem3io?_v_^ZALWvkXOz!SpMAHkK+yJx2f4=zu*1j z{&+G=415-bD{_&9Sy3xbc`50%zpPu2eRb2?;wN$YaCtJ`!N_(++N`Hc=nc-8&{kxUkO&(cj`)aog74@j;ycL zi%ocYQl@bFp4sir=_=VTxGr3FYB1Zphqk%C(!oE?^*N!QO=d=I278~ba-*C~x%VWo zzr5)i%hyxGmi;5>^Vn?yPvDQF^w0HdXt|1xAbl{(BW(8BiQUI1kH@K`#k?3Yj{>L9 zgtvAPD%62tqo!X8vq51^5I~!21$Fa z#)spse7*Yeg4C?o)64T4ZHk{@@tZ#8In0>Zt3Ru!wM-YK_Bf-!*1j=vq+T4m4z((Y z%fX@9GPG!FWxtr*+#iw~8k(I+H_J99ekjFN(s=g?X&Ji}MG#+9qN^wKqokWf>g46e zVj>k95<(J9kB0o!8cdx*mqvY|gY9F+H3SdH^wEA9OfuvKlxUhfTAJn-&0WtZ zndzgHj$^v?#S${ZVt5k)dSmFK^~ugL(X_a>tiYH?*vAb?n;Oq{BCToc-BNF#MW1%w z*EUA?Tyz+wz0}3(>znn-A?A|?xvT)@L;c*Rr%QOzem`HWeqJQ4VUfgZNU?qyW^2bM#f`bQAanKg0?Mc!rGn`hNhpn@|J$HR;K}<;=GV6$Kj@g`ilad)j zc-l3T%l;{?Lsn(UTTQ5UukGfxJoeFrnQM?n+8OqtGX}flhiqf>gaF`_pGS#|_s^fu z?QXF&R_V4%k_q_IpIvWzPfCl{ep4t<4t)R*iroeBg*WFuggghLd}(I`EoIXNWxT4K zP?f{gxkp4t)yf{^;TvW}EY+dGxRFLqy`u^rp0O3;$rVJ#LPfjgyDNS_A>#+tTtB?q zzD;=0nB{zr*W0Wq%&jV_;g6E4zlTdUwY`$Z2k4vb$4NOsb&?0DPLmaiB52@J8@YSC zetB2c=Qzw+672yaxk3F?j_-KTb!j&V>vg3>U9297*C#fw1u-!;P*XzMtWJ^sY{o0zy;>bb z51N6ji#$i|MANy9>r>L1)E{>+#iEWZNVpF_mZV{Zpbx^XMVV(q&W3w2aUi~*@MxWz zu@r5DWDW|);cr+Hkhxe7LC|x;uB*vo6H`r!HQCqoDrGajyssr!m5`B?SN(jW4Qf}f zjOUhW0F)pa`jR+kxYej{_kPwuG{q)jv}SS~$SUUXwq{%#T5QNx0{H`HzVn3Y6wLbg z2^ah-Gj^1N*wCqc$ri@Z`m;_qSDAlaf`FEJChgwAQ6^V3pQ_J=61x4Xo|q#;?lB`< z1IXAN1>=Ka+pe#TO*&5t8WAcZ^it|9-|wAdy)tzA0*z<*{cUSNbu&2V`q5^vRHo;U zdL+>4va783GPCxZWS7zUdl>i5Uq#uC)p2AllPh0Odr~+eNc!m^}`myC$eYfr&627s#@6 zu8@ae?w=^Q zb`9@`DcxH3&t#`Rx5+aG^}TwfM3}(w6vX9_XjD;Tl^}M+`Md&Qr*zcwWYRo7%^cd$ zOKMNa>i29Uj6qprZ6m?S+Q1i)YvJ#2-E$8v5t03p!r)I5!0}h z?KMOI2*^^7BKX3Pv~g71<@}YMzVKAufdwBRs#)b|CD7PB=N$L0LvWk5c*#z)PgH=o z4vh>oD|Cx|YIL-5xZQpQ)XruJ+$-&_x|Z(3q`dm_6v%bJw-c>IaZpwH&tsd2uOJ{z zND>@q=lcuSNe6G98kUPG_UF65d37!ThYi0%c?i$a)JdywD+pEoiJri;2`Mqf1Z zfD^sU$BzT|4U*oto{vIR1bhx3;-6KLd5u=2=&mj-?UpZ&=x827e+>y6c;rYq3Pi_) znB4A6)~r!A4(s=848t*4?^AxCEtrIN_sv-WO`C3-UteffB>1hbhw)sME{_P}Lq_h6n<`VUrmLIJUlew1+Q%uUpe>qT~jPU71ib3 z;42k(u1L+$fCNwrQ&*dRc_f~|1V4e`mTiB~{pBrYfvo4YX+)4p44&N|`J0M;1MS^r zJw^r&c7@RcvBOg`<#=sd=Y&tnUtZJ|oSuo|guIHO$ql8x&@t1rCDbr0kNdW4Q(BB^ zXOx#$%i-u?WA|#FiWSEBIMqg)rqH^zvZPOcVr;o=sas}eWX@4(t*(w)@7;T5C7>|z z#=o{d5J!fT_`)XD4WR5SIYi{c;Y3Ew;GYJ4HIFL0YsoMXt%7F5F$N&sG=KG7_3fGR z8yO^@ONfy-6Shh9B?%h44pPm&d0?OX*v(YUCZ71n-A>HCd+vR5r_;XiuT{x!OTFw& zo60e)2XTr?d@eN1$sOq*u|y+bMfPkHNwnh(ey6pWmem1^RTN=CZmbpABqlK{(;2?B z`z#~B-!IO&yNh)cJO1>4FV;$$X7d@JDqgzj-fzxivke!w#UiU&sdcHtq zaXldO-09%Zhk@A!2@ggJWT$>Mgqt!n4ZIMbuRbR&hz4IV$y;(Ld&~!!U|T6ryy%QV zEI*bE?qc9h9x`{KWAVw`a)h}zQB?=cT#7#}6g5-nG(Q~Rk^Ef3Csy>^N-c$r4Awc6I%<~iapjE5G8o*k z7;6M1_9JSn-^RHJ^~C#(jA+j7QQo*@Jc6N{uQA=wte@L^~aCr81zMTAHHQ+~hKz@=0hu8Q(`?&au+&EN$SJ-;#l5c-d)ZxK&}o zVh6Axreqp1ZoEBkV6-5@sNbzYago&LtJ?aED*hH7RK+8TmM3f)s5Fk*e3rjZ1)gweuJ5Vwg7vgRZC!lPq?#1@&K?tEL_VC6z&_-ZN$MjZ#M&yK>DBQF z$&iRN=?_{BpWus&AM*~B?1h*H-!yR``M$>xGxk{O0D_qx zVTKV7THNHsuz3tSRHK!>rd*TkJ@uE{QPjNUW6VJwhD}D9oDZ<~Pimiumu{HcVI7C( z$&Q@o3pZ!9mBy)NfNp-SK&gWK8g1{eKOtrkLx_kzC(Q47e&8%IW!>-oi6d9wnA89O zixV3Bx(NG$kLTt<6YZIjEBts7?yH%TW-xNDxFif1wXwHA{mn-i-+wj0B!cLHt26tS z*-R`0p$f0$UQt+d3Io^qADPdpEgG*AGG6Wm*Yl=M-l)VCCa%n4GJ8#CAdu3*SYW*e zpFM(G@0Bf>f39R;FKrjaiIGyl(4n=dJ8vC5E%nD^nTZu+_!8rFCGG8Z?nampL1Pvh zM5`73wI_vveXH0J{zb|#|BD-UQxgq!Y;SO$6QF#0Fd`~?UEwy|dH-J*DnF17la#i` zZ>2D~!MT(W+hXb$%g@VELSyFoBBAgM)d~)W$qaHP5HX1&Ocf^@!;ns2BJGBB zjtfYJk(%ar(kD>AQ0BmqOqLGrQ#B!i?8yQ4UT_bN(mT5pOlKC3!B*k;v;@_m<>M0c zUglsmi?Tr&=ymwnzs0+_QEf^pMGjVy&KVI0O19V8XEg>>l}+mVCJ?Z3TiLmm0MJ!s zBXr^RX2?5L_5n?H#y;Y=E8W~aSxUElDE5{cT~(QC70>hc1@K$5MvA7p2q!!vqUWH6L|;;5AVs*ufsx zjfiW=IOey5#f~1)cVb@8hF?N?#Y(>%*~s^wW!Zgx0+jW$<4#QYu5&jKnk;2$0?y#k zZw9l8_&_MEQ^(CCvY(DAk-{~tRq}pn|9zoz@F453@VDL#+`J7^M;5_*s-=3W!mP`T zueM%H_yuY|W5kuKdE@HL{Q-h_M7x?HmCxj=eooZ4tGXa9yGkw;w_6=?9n8k6(11{& z(-H{H9A*gMp^@0Zi@f1@V9h}dbc3Ellm%EG_8!0Ci3uvW|HrZ4Axq|!$m5gT-e+p@ zp7$J7jJ|g~jJ^a5N7DZIZQwo?mv)w}h;1Dn9Ar4m8dTm`Ofaxheevsiuo*7tfjJ(W3pp!d`XGa#uT*7OW)g&oef5hl-a4E{XlY=Na`6k6(5I6g=CJU>P!-uHg5=X&1zx$g(; z5>p`FP`FqxBA{D%!s|hTm;GF9H{5(bFz4WsL+6$vMeHRcqVA|S(ay;SW+j-NyYwmWcU{p#Z9z~DTCjfjAWd%xWBtn=56C9Tju+nQ za&$XS|5b*z)8B6$JsS^Kn=~yPz-m*|Qe8`PfzF4%e0s?_6Y{p&>%8ljB0)wF?j+*e zc$TxJr+M8u!4(6W3`;fIKN`npceEUAds^<*s@S`4M`!VpHJJK&Ufxo21kb1MyCG9|j zU$6}{l6<+KbE0H9ubrT4aBoq5hs0pOla|W*3*WJZCQCHfi0tP|7aojoYu7GDX5l+& zg+nluv{WmDq0C0~xXpL+qjbS~*D|*;fnGmw%04w&F*U9g2GAy92b4veJqOhSHawr6 zg+~}@xfyeS^4miCV7tfzC}Z=w_)xq&qXGzGnv^1dju-SqbW`hndLd0_0@>n@w?dKjMU+)7quj9(F!Bg zD3w~P4I^>Y_2r1WrLaaP7YIp$BjctiVJmwf+oLp!S}6F(!a=e4O~Z? zNV-YvE`N_&F$xX7Hwkht3iMSDfrdNGm%`Htc74O#?++aGB@`2O<*}BxS5M2CKUJO} z&NHh~J6rg(xAd6lcj(WUfl4WFHdKi4iX`m`65;N|s-HDW^?g`s41Ue|^d~VH*sVJL zUN+tcV51jH>xdtO%a^}5RAJvfM_ICbR3r^vx8FMZBll8ZSpFeEX!vo=w65KWnrNtR zl|SX&+9&1TpcL!2<}Y_{wIhJOLSpwPtBQSX2|MQcdelgiYPRtCS8^FPccur?F=eMz z&Uiwg)D~K9xDI`y1VqQOEoBK81uq5GuyI-iy1{_dUm-&Hi2uxx7>|Ju39T1qjL9^+ z|E>SWvuS8ES$72D-nd~T$c8`;1+kbsu!a3!emK)Q%16WG@eg6#`m&(-p2V7Ig}aS` z0(o{Tu1ys>uuQ3K$i}BP*ex)%wpunlH9FF27#Fl)Uw5qF*0oHx=*zeOvDJtvceVSK zlF3NXYo})B(D?qVD>PSf;F@fJ4?BB&rY!1UewfEr{pSG&KdST~FUQwI6pg^aLF$2;i8ueV!}nhH0M?9!NzYQM*0+Ac)5z8W0w z?*%{8loGL%+Dsz-lOjDj301jmO)$Z|!fquASKhA>1Yj@3+W)`+Dmn5ND-QdCK?hQq zV`HErOAhZ-0h>acOt`JZ9XE1;2Y`UpbWgqPS`rZ0-(hVAclHZ!E0M@?O&rGs^LkA} zlTkrg2ft6LIbkn^sYB?T5ZcD=+;|ZXop9c_ zagLyO8#5MsBH(djKe1EMvv9rE=da+aii+T=QvfgMWKyjYk2y9#Ot2*=z7))9EPn{9 z+TuE)-v(Z2lRCP0Ui?OwAs+}Hzv z=#FbdN=w~#VFykR7tij^YD+7{9LZVg?jFd&lZ9>B>koUTD^I|tNq(D6$yMOeT;^0@ z&p!JFXsfZJabBNeI7dL!Yqn$`~%<0w$;6fS4v*FVH(?9vj}?|w`u z0KG%tH#M1DEfB;8IadXGFfYGjz-27`(xzDH&E@;>_YTD^TI*(YyZfSTz{@ZA4RX7( zor*#lEg^vK7w|R@9d)n1;BDxtV7q`U+h+BfqpeEbGp~fq)DW|78GiOCsIaz-0sNG0 z0X5miU=O_L-Yxv(hz6+1pF+Uo_*1x7gZ&i#w*~wE@Ld0o4XY6R-F?K_p5LsRZNmkC NWo>S6R*mrf{a;s)_%i?i diff --git a/docs/_static/img/framework.png b/docs/_static/img/framework.png index 673f10e033136dfb87cc0f28af8aa75d5d2a2dd5..92c6b61270d867041e0b2b341026de5a0899cc36 100644 GIT binary patch literal 277395 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

_m}|QgA$)=Uj#DcN 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

!qKy23rnsM|xWI|te6c&N!}g2_NI#H3&AEI( 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 literal 209724 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 z Date: Thu, 26 Nov 2020 19:07:15 +0800 Subject: [PATCH 174/241] update readme --- README.md | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 7fd86028e..4ebb70466 100644 --- a/README.md +++ b/README.md @@ -139,17 +139,20 @@ Qlib provides a tool named `qrun` to run the whole workflow automatically (inclu ```bash - risk - excess_return_without_cost mean 0.000675 - std 0.005456 - annualized_return 0.170077 - information_ratio 1.963824 - max_drawdown -0.063646 - excess_return_with_cost mean 0.000479 - std 0.005453 - annualized_return 0.120776 - information_ratio 1.395116 - max_drawdown -0.071216 + 'The following are analysis results of the excess return without cost.' + risk + mean 0.000708 + std 0.005626 + annualized_return 0.178316 + information_ratio 1.996555 + max_drawdown -0.081806 + 'The following are analysis results of the excess return with cost.' + risk + mean 0.000512 + std 0.005626 + annualized_return 0.128982 + information_ratio 1.444287 + max_drawdown -0.091078 From 2fd982a98fa90fbd0b2e819004a9e0246bb61098 Mon Sep 17 00:00:00 2001 From: Jactus Date: Thu, 26 Nov 2020 19:40:41 +0800 Subject: [PATCH 175/241] Update docs and delete estimator --- docs/component/data.rst | 119 ++-------- docs/component/recorder.rst | 332 +------------------------- docs/conf.py | 2 +- docs/reference/api.rst | 28 +-- qlib/contrib/estimator/__init__.py | 0 qlib/contrib/estimator/config.py | 176 -------------- qlib/contrib/estimator/estimator.py | 328 ------------------------- qlib/contrib/estimator/fetcher.py | 290 ---------------------- qlib/contrib/estimator/launcher.py | 115 --------- qlib/contrib/estimator/trainer.py | 317 ------------------------- qlib/data/dataset/__init__.py | 1 + qlib/data/dataset/handler.py | 34 ++- qlib/data/dataset/loader.py | 25 +- qlib/workflow/__init__.py | 356 +++++++++++++--------------- 14 files changed, 245 insertions(+), 1878 deletions(-) delete mode 100644 qlib/contrib/estimator/__init__.py delete mode 100644 qlib/contrib/estimator/config.py delete mode 100644 qlib/contrib/estimator/estimator.py delete mode 100644 qlib/contrib/estimator/fetcher.py delete mode 100644 qlib/contrib/estimator/launcher.py delete mode 100644 qlib/contrib/estimator/trainer.py diff --git a/docs/component/data.rst b/docs/component/data.rst index 22565c39d..3323211d6 100644 --- a/docs/component/data.rst +++ b/docs/component/data.rst @@ -159,6 +159,9 @@ Data Loader ``Data Loader`` in ``Qlib`` is designed to load raw data from the original data source. It will be loaded and used in the ``Data Handler`` module. +QlibDataLoader +--------------- + The ``QlibDataLoader`` class in ``Qlib`` is such an interface that allows users to load raw data from the data source. Interface @@ -166,33 +169,8 @@ Interface Here are some interfaces of the ``QlibDataLoader`` class: -- `load(instruments, start_time=None, end_time=None)` - - This method loads the data as pd.DataFrame - - Parameters: - - `instruments` \: str or dict - it can either be the market name or the config file of instruments generated by InstrumentProvider. - - `start_time` \: str - start of the time range. - - `end_time` \: str - end of the time range. - - Returns: - - The data being loaded with type `pd.DataFrame` - -- `load_group_df(instruments, exprs: list, names: list, start_time=None, end_time=None)` - - This method loads the dataframe for specific group. - - Parameters: - - `instruments` \: str or dict - it can either be the market name or the config file of instruments generated by InstrumentProvider. - - `exprs` \: list - the expressions to describe the content of the data. - - `names` \: list - the name of the data. - - `start_time` \: str - start of the time range. - - `end_time` \: str - end of the time range. - - Returns: - - The queried data in type `pd.DataFrame`. +.. autoclass:: qlib.data.dataset.loader.QlibDataLoader + :members: load, load_group_df API ----------- @@ -207,74 +185,24 @@ The ``Data Handler`` module in ``Qlib`` is designed to handler those common data Users can use ``Data Handler`` in an automatic workflow by ``qrun``, refer to `Workflow: Workflow Management `_ for more details. - -Base Class & Interface ----------------------- +DataHandlerLP +-------------- In addition to use ``Data Handler`` in an automatic workflow with ``qrun``, ``Data Handler`` can be used as an independent module, by which users can easily preprocess data (standardization, remove NaN, etc.) and build datasets. In order to achieve so, ``Qlib`` provides a base class `qlib.data.dataset.DataHandlerLP <../reference/api.html#qlib.data.dataset.handler.DataHandlerLP>`_. The core idea of this class is that: we will have some leanable ``Processors`` which can learn the parameters of data processing. When new data comes in, these `trained` ``Processors`` can then infer on the new data and thus processing real-time data in an efficient way. More information about ``Processors`` will be listed in the next subsection. + +Interface +---------------------- + Here are some important interfaces that ``DataHandlerLP`` provides: -- `__init__(instruments=None, start_time=None, end_time=None, data_loader: Tuple[dict, str, DataLoader] = None, infer_processors=[], learn_processors=[], process_type=PTYPE_A, **kwargs)` - - Initialization of the class. - - Parameters: - - `infer_processors` \: list - - list of of processors to generate data for inference - - example of : - - .. code-block:: - - 1) classname & kwargs: - { - "class": "MinMaxNorm", - "kwargs": { - "fit_start_time": "20080101", - "fit_end_time": "20121231" - } - } - 2) Only classname: - "DropnaFeature" - 3) object instance of Processor - - - `learn_processors` \: list - similar to infer_processors, but for generating data for learning models - - - `process_type`: str - - PTYPE_I = 'independent' - - self._infer will processed by infer_processors - - self._learn will be processed by learn_processors - - PTYPE_A = 'append' - - self._infer will processed by infer_processors - - self._learn will be processed by infer_processors + learn_processors - - (e.g. self._infer processed by learn_processors ) - -- `fetch(selector: Union[pd.Timestamp, slice, str] = slice(None, None), level: Union[str, int] = "datetime", col_set=DataHandler.CS_ALL, data_key: str = DK_I)` - - This method fetches data from underlying data source - - Parameters: - - `selector` \: Union[pd.Timestamp, slice, str] - describe how to select data by index. - - `level` \: Union[str, int] - which index level to select the data. - - `col_set` \: str - select a set of meaningful columns.(e.g. features, columns). - - `data_key` \: str - The data to fetch: DK_*. - - Returns: - - The retrieved results in the type: `pd.DataFrame`. - -- `get_cols(col_set=DataHandler.CS_ALL, data_key: str = DK_I)` - - This method gets the column names. - - Parameters: - - `col_set` \: str - select a set of meaningful columns.(e.g. features, columns). - - `data_key` \: str - the data to fetch: DK_*. - - Returns: - - A list of column names. +.. autoclass:: qlib.data.dataset.handler.DataHandlerLP + :members: __init__, fetch, get_cols If users want to load features and labels by config, users can inherit ``qlib.data.dataset.handler.ConfigDataHandler``, ``Qlib`` also provides some preprocess method in this subclass. + If users want to use qlib data, `QLibDataHandler` is recommended. Users can inherit their custom class from `QLibDataHandler`, which is also a subclass of `ConfigDataHandler`. @@ -353,23 +281,8 @@ The motivation of this module is that we want to maximize the flexibility of of The ``DatasetH`` class is the `dataset` with `Data Handler`. Here is the most important interface of the class: -- `prepare(segments: Union[List[str], Tuple[str], str, slice], col_set=DataHandler.CS_ALL, data_key=DataHandlerLP.DK_I, **kwargs)` - - This method prepares the data for learning and inference. - - Parameters: - - `segments` \: Union[List[str], Tuple[str], str, slice] - Describe the scope of the data to be prepared - Here are some examples: - - - 'train' - - - ['train', 'valid'] - - - `col_set` \: str - The col_set will be passed to self._handler when fetching data. - - `data_key` \: str - The data to fetch: DK_* - Default is DK_I, which indicate fetching data for **inference**. - +.. autoclass:: qlib.data.dataset.__init__.DatasetH + :members: API --------- diff --git a/docs/component/recorder.rst b/docs/component/recorder.rst index 0d1e83168..4304dcce5 100644 --- a/docs/component/recorder.rst +++ b/docs/component/recorder.rst @@ -50,312 +50,17 @@ Qlib Recorder Here are the available interfaces of ``QlibRecorder``: -- `__init__(exp_manager)` - - Initialization. - - It takes in an input: `exp_manager`, which is an `ExperimentManager` instance. The instance will be created during ``qlib.init``. - -- `start(experiment_name=None, recorder_name=None)` - - High level API to start an experiment. This method can only be called within a Python's '`with`' statement. - - Parameters: - - `experiment_name` : str - name of the experiment one wants to start. - - `recorder_name` : str - name of the recorder under the experiment one wants to start. - - Use case: - - .. code-block:: Python - - with R.start('test', 'recorder_1'): - model.fit(dataset) - R.log... - ... # further operations - -- `start_exp(experiment_name=None, recorder_name=None, uri=None)` - - Lower level method for starting an experiment. When use this method, one should end the experiment manually and the status of the recorder may not be handled properly. - - Parameters: - - `experiment_name` : str - the name of the experiment to be started - - `recorder_name` : str - name of the recorder under the experiment one wants to start. - - `uri` : str - the tracking uri of the experiment, where all the artifacts/metrics etc. will be stored. - The default uri are set in the qlib.config. - - Returns: - - an experiment instance being started. - - Use case: - - .. code-block:: Python - - R.start_exp(experiment_name='test', recorder_name='recorder_1') - ... # further operations - R.end_exp('FINISHED') or R.end_exp(Recorder.STATUS_S) - -- `end_exp(recorder_status=Recorder.STATUS_FI)` - - Method for ending an experiment manually. It will end the current active experiment, as well as its active recorder with the specified `status` type. - - Parameters: - - `status` : str - The status of a recorder, which can be '`SCHEDULED`', '`RUNNING`', '`FINISHED`', '`FAILED`'. - - Use case: - - .. code-block:: Python - - R.start_exp(experiment_name='test') - ... # further operations - R.end_exp('FINISHED') or R.end_exp(Recorder.STATUS_S) - -- `search_records(experiment_ids, **kwargs)` - - Get a pandas DataFrame of all the records that have been stored with the given search criteria. This method is highly correlated with MLFlow's ``search_runs`` method (`link `_). - - Parameters: - - `experiment_ids` : list - list of experiment IDs. - - `filter_string` : str - filter query string, defaults to searching all runs. - - `run_view_type` : int - one of enum values ACTIVE_ONLY (1), DELETED_ONLY (2), or ALL (3). - - `max_results` : int - the maximum number of runs to put in the dataframe. - - `order_by` : list - list of columns to order by (e.g., “metrics.rmse”). - - Returns: - - A pandas.DataFrame of records, where each metric, parameter, and tag are expanded into their own columns named metrics.*, params.*, and tags.* respectively. For records that don't have a particular metric, parameter, or tag, their value will be (NumPy) Nan, None, or None respectively. - - Use case: - - .. code-block:: Python - - R.log_metrics(m=2.50, step=0) - records = R.search_runs([experiment_id], order_by=["metrics.m DESC"]) - -- `list_experiments()` - - Method for listing all the existing experiments (except for those being deleted.) - - Returns: - - A dictionary (name -> experiment) of experiments information that being stored. - - Use case: - - .. code-block:: Python - - exps = R.list_experiments() - -- `list_recorders(experiment_id=None, experiment_name=None)` - - Method for listing all the recorders of experiment with given id or name. If user doesn't provide the id or name of the experiment, this method will try to retrieve the default experiment and list all the recorders of the default experiment. If the default experiment doesn't exist, the method will first create the default experiment, and then create a new recorder under it. - - Parameters: - - `experiment_id` : str - id of the experiment. - - `experiment_name` : str - name of the experiment. - - Returns: - - A dictionary (id -> recorder) of recorder information that being stored. - - Use case: - - .. code-block:: Python - - recorders = R.list_recorders(experiment_name='test') - -- `get_exp(experiment_id=None, experiment_name=None, create: bool = True)` - - Method for retrieving an experiment with given id or name. Once the '`create`' argument is set to True, if no valid experiment is found, this method will create one for the user. Otherwise, it will only retrieve a specific experiment or raise an Error. - - - If '`create`' is True: - - If ``R``'s running: - - no id or name specified, return the active experiment. - - if id or name is specified, return the specified experiment. If no such exp found, create a new experiment with given id or name, and the experiment is set to be running. - - If ``R``'s not running: - - no id or name specified, create a default experiment, and the experiment is set to be running. - - if id or name is specified, return the specified experiment. If no such exp found, create a new experiment with given name or the default experiment, and the experiment is set to be running. - - Else If '`create`' is False: - - If ``R``'s running: - - no id or name specified, return the active experiment. - - if id or name is specified, return the specified experiment. If no such exp found, raise Error. - - If ``R``'s not running: - - no id or name specified. If the default experiment exists, return it, otherwise, raise Error. - - if id or name is specified, return the specified experiment. If no such exp found, raise Error. - - Parameters: - - `experiment_id` : str - id of the experiment. - - `experiment_name` : str - name of the experiment. - - `create` : boolean - an argument determines whether the method will automatically create a new experiment according to user's specification if the experiment hasn't been created before. - - Returns: - - An experiment instance with given id or name. - - Use case: - - .. code-block:: Python - - # Case 1 - with R.start('test'): - exp = R.get_exp() - recorders = exp.list_recorders() - - # Case 2 - with R.start('test'): - exp = R.get_exp('test1') - - # Case 3 - exp = R.get_exp() -> a default experiment. - - # Case 4 - exp = R.get_exp(experiment_name='test') - - # Case 5 - exp = R.get_exp(create=False) -> the default experiment if exists. - -- `delete_exp(experiment_id=None, experiment_name=None)` - - Method for deleting the experiment with given id or name. At least one of id or name must be given, otherwise, error will occur. - - Parameters: - - `experiment_id` : str - id of the experiment. - - `experiment_name` : str - name of the experiment. - - Use case: - - .. code-block:: Python - - R.delete_exp(experiment_name='test') - -- `get_uri()` - - Method for retrieving the uri of current experiment manager. - - Returns: - - The uri of current experiment manager. - - Use case: - - .. code-block:: Python - - uri = R.get_uri() - -- `get_recorder(recorder_id=None, recorder_name=None, experiment_name=None)` - - Method for retrieving a recorder. The recorder can be used for further process such as ``save_objects``, ``load_object``, ``log_params``, ``log_metrics``, etc. - - - If ``R``'s running: - - no id or name specified, return the active recorder. - - if id or name is specified, return the specified recorder. - - If ``R``'s not running: - - no id or name specified, raise Error. - - if id or name is specified, and the corresponding experiment_name must be given, return the specified recorder. Otherwise, raise Error. - - Parameters: - - `recorder_id` : str - id of the recorder. - - `recorder_name` : str - name of the recorder. - - `experiment_name` : str - name of the experiment. - - Returns: - - A recorder instance. - - Use case: - - .. code-block:: Python - - # Case 1 - with R.start('test'): - recorder = R.get_recorder() - - # Case 2 - with R.start('test'): - recorder = R.get_recorder(recorder_id='2e7a4efd66574fa49039e00ffaefa99d') - - # Case 3 - recorder = R.get_recorder() -> Error - - # Case 4 - recorder = R.get_recorder(recorder_id='2e7a4efd66574fa49039e00ffaefa99d') -> Error - - # Case 5 - recorder = R.get_recorder(recorder_id='2e7a4efd66574fa49039e00ffaefa99d', experiment_name='test') - -- `delete_recorder(recorder_id=None, recorder_name=None)` - - Method for deleting the recorders with given id or name. At least one of id or name must be given, otherwise, error will occur. - - Parameters: - - `recorder_id` : str - id of the experiment. - - `recorder_name` : str - name of the experiment. - - Use case: - - .. code-block:: Python - - R.delete_recorder(recorder_id='2e7a4efd66574fa49039e00ffaefa99d') - -- `save_objects(local_path=None, artifact_path=None, **kwargs)` - - Method for saving objects as artifacts in the experiment to the uri. It supports either saving from a local file/directory, or directly saving objects. User can use valid python's keywords arguments to specify the object to be saved as well as its name (name: value). - - - If R's running: it will save the objects through the running recorder. - - If R's not running: the system will create a default experiment, and a new recorder and save objects under it. - - .. note:: - - If one wants to save objects with a specific recorder. It is recommended to first get the specific recorder through `get_recorder` API and use the recorder the save objects. The supported arguments are the same as this method. - - - Parameters: - - `local_path` : str - if provided, them save the file or directory to the artifact URI. - - `artifact_path` : str - the relative path for the artifact to be stored in the URI. - - Use case: - - .. code-block:: Python - - # Case 1 - with R.start('test'): - pred = model.predict(dataset) - R.save_objects(**{"pred.pkl": pred}, artifact_path='prediction') - - # Case 2 - with R.start('test'): - R.save_objects(local_path='results/pred.pkl') - -- `log_params(**kwargs)` - - Method for logging parameters during an experiment. In addition to using ``R``, one can also log to a specific recorder after getting it with `get_recorder` API. - - - If R's running: it will log parameters through the running recorder. - - If R's not running: the system will create a default experiment as well as a new recorder, and log parameters under it. - - Parameters: - - `keyword argument`: - name1=value1, name2=value2, ... - - Use case: - - .. code-block:: Python - - # Case 1 - with R.start('test'): - R.log_params(learning_rate=0.01) - - # Case 2 - R.log_params(learning_rate=0.01) - -- `log_metrics(step=None, **kwargs)` - - Method for logging metrics during an experiment. In addition to using ``R``, one can also log to a specific recorder after getting it with `get_recorder` API. - - - If R's running: it will log metrics through the running recorder. - - If R's not running: the system will create a default experiment as well as a new recorder, and log metrics under it. - - Parameters: - - `step`: int - a single integer step at which to log the specified Metrics. If unspecified, each metric is logged at step zero. - - `keyword argument`: - name1=value1, name2=value2, ... - -- `set_tags(**kwargs)` - - Method for setting tags for a recorder. In addition to using ``R``, one can also set the tag to a specific recorder after getting it with `get_recorder` API. - - - If R's running: it will set tags through the running recorder. - - If R's not running: the system will create a default experiment as well as a new recorder, and set the tags under it. - - Parameters: - - `keyword argument`: - name1=value1, name2=value2, ... - - Use case: - - .. code-block:: Python - - # Case 1 - with R.start('test'): - R.set_tags(release_version="2.2.0") - - # Case 2 - R.set_tags(release_version="2.2.0") - +.. autoclass:: qlib.workflow.__init__.QlibRecorder + :members: Experiment Manager =================== The ``ExpManager`` module in ``Qlib`` is responsible for managing different experiments. Most of the APIs of ``ExpManager`` are similar to ``QlibRecorder``, and the most important API will be the ``get_exp`` method. User can directly refer to the documents above for some detailed information about how to use the ``get_exp`` method. +.. autoclass:: qlib.workflow.expm.ExpManager + :members: get_exp, list_experiments + For other interfaces such as `create_exp`, `delete_exp`, please refer to `Experiment Manager API <../reference/api.html#experiment-manager>`_. Experiment @@ -363,6 +68,9 @@ Experiment The ``Experiment`` class is solely responsible for a single experiment, and it will handle any operations that are related to an experiment. Basic methods such as `start`, `end` an experiment are included. Besides, methods related to `recorders` are also available: such methods include `get_recorder` and `list_recorders`. +.. autoclass:: qlib.workflow.exp.Experiment + :members: get_recorder, list_recorders + For other interfaces such as `search_records`, `delete_recorder`, please refer to `Experiment API <../reference/api.html#experiment>`_. Recorder @@ -372,28 +80,8 @@ The ``Recorder`` class is responsible for a single recorder. It will handle some Here are some important APIs that are not included in the ``QlibRecorder``: -- `list_artifacts(artifact_path: str = None)` - - List all the artifacts of a recorder. - - Parameters: - - `artifact_path` : str - the relative path for the artifact to be stored in the URI. - - Returns: - - A list of artifacts information (name, path, etc.) that being stored. - -- `list_metrics()` - - List all the metrics of a recorder. - - Returns: - - A dictionary of metrics that being stored. - -- `list_params()` - - List all the params of a recorder. - - Returns: - - A dictionary of params that being stored. - -- `list_tags()` - - List all the tags of a recorder. - - Returns: - - A dictionary of tags that being stored. +.. autoclass:: qlib.workflow.recorder.Recorder + :members: list_artifacts, list_metrics, list_params, list_tags For other interfaces such as `save_objects`, `load_object`, please refer to `Recorder API <../reference/api.html#recorder>`_. diff --git a/docs/conf.py b/docs/conf.py index b91efb9a9..5359d08ed 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -124,7 +124,7 @@ html_theme_options = { "logo_only": True, "collapse_navigation": False, "display_version": False, - "navigation_depth": 3, + "navigation_depth": 4, } # Add any paths that contain custom static files (such as style sheets) here, diff --git a/docs/reference/api.rst b/docs/reference/api.rst index d99a26f49..f21a9f518 100644 --- a/docs/reference/api.rst +++ b/docs/reference/api.rst @@ -23,16 +23,13 @@ Filter .. automodule:: qlib.data.filter :members: -Feature --------------------- - Class -~~~~~~~~~~~~~~~~~~~~ +-------------------- .. automodule:: qlib.data.base :members: Operator -~~~~~~~~~~~~~~~~~~~~ +-------------------- .. automodule:: qlib.data.ops :members: @@ -56,29 +53,32 @@ Cache .. autoclass:: qlib.data.cache.DiskDatasetCache :members: +Dataset +--------------- -Contrib -==================== +Dataset Class +~~~~~~~~~~~~~~~~~~~~ +.. automodule:: qlib.data.dataset.__init__ + :members: Data Loader ---------------- +~~~~~~~~~~~~~~~~~~~~ .. automodule:: qlib.data.dataset.loader :members: Data Handler ---------------- +~~~~~~~~~~~~~~~~~~~~ .. automodule:: qlib.data.dataset.handler :members: Processor ---------------- +~~~~~~~~~~~~~~~~~~~~ .. automodule:: qlib.data.dataset.processor :members: -Dataset ---------------- -.. automodule:: qlib.data.dataset.__init__ - :members: + +Contrib +==================== Model -------------------- diff --git a/qlib/contrib/estimator/__init__.py b/qlib/contrib/estimator/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/qlib/contrib/estimator/config.py b/qlib/contrib/estimator/config.py deleted file mode 100644 index 0d782c412..000000000 --- a/qlib/contrib/estimator/config.py +++ /dev/null @@ -1,176 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. - -import yaml -import copy -import os -import json -import tempfile -from pathlib import Path -from ...config import REG_CN - - -class EstimatorConfigManager(object): - def __init__(self, config_path): - - if not config_path: - raise ValueError("Config path is invalid.") - self.config_path = config_path - - with open(config_path) as fp: - config = yaml.load(fp, Loader=yaml.FullLoader) - self.config = copy.deepcopy(config) - - self.ex_config = ExperimentConfig(config.get("experiment", dict()), self) - self.data_config = DataConfig(config.get("data", dict()), self) - self.model_config = ModelConfig(config.get("model", dict()), self) - self.trainer_config = TrainerConfig(config.get("trainer", dict()), self) - self.strategy_config = StrategyConfig(config.get("strategy", dict()), self) - self.backtest_config = BacktestConfig(config.get("backtest", dict()), self) - self.qlib_data_config = QlibDataConfig(config.get("qlib_data", dict()), self) - - # If the start_date and end_date are not given in data_config, they will be referred from the trainer_config. - handler_start_date = self.data_config.handler_parameters.get("start_date", None) - handler_end_date = self.data_config.handler_parameters.get("end_date", None) - if handler_start_date is None: - self.data_config.handler_parameters["start_date"] = self.trainer_config.parameters["train_start_date"] - if handler_end_date is None: - self.data_config.handler_parameters["end_date"] = self.trainer_config.parameters["test_end_date"] - - -class ExperimentConfig(object): - TRAIN_MODE = "train" - TEST_MODE = "test" - - OBSERVER_FILE_STORAGE = "file_storage" - OBSERVER_MONGO = "mongo" - - def __init__(self, config, CONFIG_MANAGER): - """__init__ - - :param config: The config dict for experiment - :param CONFIG_MANAGER: The estimator config manager - """ - self.name = config.get("name", "test_experiment") - # The dir of the result of all the experiments - self.global_dir = config.get("dir", os.path.dirname(CONFIG_MANAGER.config_path)) - # The dir of the result of current experiment - self.ex_dir = os.path.join(self.global_dir, self.name) - if not os.path.exists(self.ex_dir): - os.makedirs(self.ex_dir) - self.tmp_run_dir = tempfile.mkdtemp(dir=self.ex_dir) - self.mode = config.get("mode", ExperimentConfig.TRAIN_MODE) - self.sacred_dir = os.path.join(self.ex_dir, "sacred") - self.observer_type = config.get("observer_type", ExperimentConfig.OBSERVER_FILE_STORAGE) - self.mongo_url = config.get("mongo_url", None) - self.db_name = config.get("db_name", None) - self.finetune = config.get("finetune", False) - - # The path of the experiment id of the experiment - self.exp_info_path = config.get("exp_info_path", os.path.join(self.ex_dir, "exp_info.json")) - exp_info_dir = Path(self.exp_info_path).parent - exp_info_dir.mkdir(parents=True, exist_ok=True) - - # Test mode config - loader_args = config.get("loader", dict()) - if self.mode == ExperimentConfig.TEST_MODE or self.finetune: - loader_exp_info_path = loader_args.get("exp_info_path", None) - self.loader_model_index = loader_args.get("model_index", None) - if (loader_exp_info_path is not None) and (os.path.exists(loader_exp_info_path)): - with open(loader_exp_info_path) as fp: - loader_dict = json.load(fp) - for k, v in loader_dict.items(): - setattr(self, "loader_{}".format(k), v) - # Check loader experiment id - assert hasattr(self, "loader_id"), "If mode is test or finetune is True, loader must contain id." - else: - self.loader_id = loader_args.get("id", None) - if self.loader_id is None: - raise ValueError("If mode is test or finetune is True, loader must contain id.") - - self.loader_observer_type = loader_args.get("observer_type", self.observer_type) - self.loader_name = loader_args.get("name", self.name) - self.loader_dir = loader_args.get("dir", self.global_dir) - - self.loader_mongo_url = loader_args.get("mongo_url", self.mongo_url) - self.loader_db_name = loader_args.get("db_name", self.db_name) - - -class DataConfig(object): - def __init__(self, config, CONFIG_MANAGER): - """__init__ - - :param config: The config dict for data - :param CONFIG_MANAGER: The estimator config manager - """ - self.handler_module_path = config.get("module_path", "qlib.contrib.data.handler") - self.handler_class = config.get("class", "ALPHA360") - self.handler_parameters = config.get("args", dict()) - self.handler_filter = config.get("filter", dict()) - # Update provider uri. - - -class ModelConfig(object): - def __init__(self, config, CONFIG_MANAGER): - """__init__ - - :param config: The config dict for model - :param CONFIG_MANAGER: The estimator config manager - """ - self.model_class = config.get("class", "Model") - self.model_module_path = config.get("module_path", "qlib.model") - self.save_dir = os.path.join(CONFIG_MANAGER.ex_config.tmp_run_dir, "model") - self.save_path = config.get("save_path", os.path.join(self.save_dir, "model.bin")) - self.parameters = config.get("args", dict()) - # Make dir if need. - if not os.path.exists(self.save_dir): - os.makedirs(self.save_dir) - - -class TrainerConfig(object): - def __init__(self, config, CONFIG_MANAGER): - """__init__ - - :param config: The config dict for trainer - :param CONFIG_MANAGER: The estimator config manager - """ - self.trainer_class = config.get("class", "StaticTrainer") - self.trainer_module_path = config.get("module_path", "qlib.contrib.estimator.trainer") - self.parameters = config.get("args", dict()) - - -class StrategyConfig(object): - def __init__(self, config, CONFIG_MANAGER): - """__init__ - - :param config: The config dict for strategy - :param CONFIG_MANAGER: The estimator config manager - """ - self.strategy_class = config.get("class", "TopkDropoutStrategy") - self.strategy_module_path = config.get("module_path", "qlib.contrib.strategy.strategy") - self.parameters = config.get("args", dict()) - - -class BacktestConfig(object): - def __init__(self, config, CONFIG_MANAGE): - """__init__ - - :param config: The config dict for strategy - :param CONFIG_MANAGE: The estimator config manager - """ - self.normal_backtest_parameters = config.get("normal_backtest_args", dict()) - self.long_short_backtest_parameters = config.get("long_short_backtest_args", dict()) - - -class QlibDataConfig(object): - def __init__(self, config, CONFIG_MANAGE): - """__init__ - - :param config: The config dict for qlib_client - :param CONFIG_MANAGE: The estimator config manager - """ - self.provider_uri = config.pop("provider_uri", "~/.qlib/qlib_data/cn_data") - self.auto_mount = config.pop("auto_mount", False) - self.mount_path = config.pop("mount_path", "~/.qlib/qlib_data/cn_data") - self.region = config.pop("region", REG_CN) - self.args = config diff --git a/qlib/contrib/estimator/estimator.py b/qlib/contrib/estimator/estimator.py deleted file mode 100644 index 56495e5eb..000000000 --- a/qlib/contrib/estimator/estimator.py +++ /dev/null @@ -1,328 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. - -# coding=utf-8 - -import pandas as pd - -import os -import copy -import json -import yaml -import pickle - -import qlib -from ..evaluate import risk_analysis -from ..evaluate import backtest as normal_backtest -from ..evaluate import long_short_backtest -from .config import ExperimentConfig -from .fetcher import create_fetcher_with_config - -from ...log import get_module_logger, TimeInspector -from ...utils import get_module_by_module_path, compare_dict_value - - -class Estimator(object): - def __init__(self, config_manager, sacred_ex): - - # Set logger. - self.logger = get_module_logger("Estimator") - - # 1. Set config manager. - self.config_manager = config_manager - - # 2. Set configs. - self.ex_config = config_manager.ex_config - self.data_config = config_manager.data_config - self.model_config = config_manager.model_config - self.trainer_config = config_manager.trainer_config - self.strategy_config = config_manager.strategy_config - self.backtest_config = config_manager.backtest_config - - # If experiment.mode is test or experiment.finetune is True, load the experimental results in the loader - if self.ex_config.mode == self.ex_config.TEST_MODE or self.ex_config.finetune: - self.compare_config_with_config_manger(self.config_manager) - - # 3. Set sacred_experiment. - self.ex = sacred_ex - - # 4. Init data handler. - self.data_handler = None - self._init_data_handler() - - # 5. Init trainer. - self.trainer = None - self._init_trainer() - - # 6. Init strategy. - self.strategy = None - self._init_strategy() - - def _init_data_handler(self): - handler_module = get_module_by_module_path(self.data_config.handler_module_path) - - # Set market - market = self.data_config.handler_filter.get("market", None) - if market is None: - if "market" in self.data_config.handler_parameters: - self.logger.warning( - "Warning: The market in data.args section is deprecated. " - "It only works when market is not set in data.filter section. " - "It will be overridden by market in the data.filter section." - ) - market = self.data_config.handler_parameters["market"] - else: - market = "csi500" - - self.data_config.handler_parameters["market"] = market - - data_filter_list = [] - handler_filters = self.data_config.handler_filter.get("filter_pipeline", list()) - for h_filter in handler_filters: - filter_module_path = h_filter.get("module_path", "qlib.data.filter") - filter_class_name = h_filter.get("class", "") - filter_parameters = h_filter.get("args", {}) - filter_module = get_module_by_module_path(filter_module_path) - filter_class = getattr(filter_module, filter_class_name) - data_filter = filter_class(**filter_parameters) - data_filter_list.append(data_filter) - - self.data_config.handler_parameters["data_filter_list"] = data_filter_list - handler_class = getattr(handler_module, self.data_config.handler_class) - self.data_handler = handler_class(**self.data_config.handler_parameters) - - def _init_trainer(self): - - model_module = get_module_by_module_path(self.model_config.model_module_path) - trainer_module = get_module_by_module_path(self.trainer_config.trainer_module_path) - model_class = getattr(model_module, self.model_config.model_class) - trainer_class = getattr(trainer_module, self.trainer_config.trainer_class) - - self.trainer = trainer_class( - model_class, - self.model_config.save_path, - self.model_config.parameters, - self.data_handler, - self.ex, - **self.trainer_config.parameters - ) - - def _init_strategy(self): - - module = get_module_by_module_path(self.strategy_config.strategy_module_path) - strategy_class = getattr(module, self.strategy_config.strategy_class) - self.strategy = strategy_class(**self.strategy_config.parameters) - - def run(self): - if self.ex_config.mode == ExperimentConfig.TRAIN_MODE: - self.trainer.train() - elif self.ex_config.mode == ExperimentConfig.TEST_MODE: - self.trainer.load() - else: - raise ValueError("unexpected mode: %s" % self.ex_config.mode) - analysis = self.backtest() - print(analysis) - self.logger.info( - "experiment id: {}, experiment name: {}".format(self.ex.experiment.current_run._id, self.ex_config.name) - ) - - # Remove temp dir - # shutil.rmtree(self.ex_config.tmp_run_dir) - - def backtest(self): - TimeInspector.set_time_mark() - # 1. Get pred and prediction score of model(s). - pred = self.trainer.get_test_score() - try: - performance = self.trainer.get_test_performance() - except NotImplementedError: - performance = None - # 2. Normal Backtest. - report_normal, positions_normal = self._normal_backtest(pred) - # 3. Long-Short Backtest. - # Deprecated - # long_short_reports = self._long_short_backtest(pred) - # 4. Analyze - analysis_df = self._analyze(report_normal) - # 5. Save. - self._save_backtest_result( - pred, - analysis_df, - positions_normal, - report_normal, - # long_short_reports, - performance, - ) - return analysis_df - - def _normal_backtest(self, pred): - TimeInspector.set_time_mark() - if "account" not in self.backtest_config.normal_backtest_parameters: - if "account" in self.strategy_config.parameters: - self.logger.warning( - "Warning: The account in strategy section is deprecated. " - "It only works when account is not set in backtest section. " - "It will be overridden by account in the backtest section." - ) - self.backtest_config.normal_backtest_parameters["account"] = self.strategy_config.parameters["account"] - report_normal, positions_normal = normal_backtest( - pred, strategy=self.strategy, **self.backtest_config.normal_backtest_parameters - ) - TimeInspector.log_cost_time("Finished normal backtest.") - return report_normal, positions_normal - - def _long_short_backtest(self, pred): - TimeInspector.set_time_mark() - long_short_reports = long_short_backtest(pred, **self.backtest_config.long_short_backtest_parameters) - TimeInspector.log_cost_time("Finished long-short backtest.") - return long_short_reports - - @staticmethod - def _analyze(report_normal): - TimeInspector.set_time_mark() - - analysis = dict() - # analysis["pred_long"] = risk_analysis(long_short_reports["long"]) - # analysis["pred_short"] = risk_analysis(long_short_reports["short"]) - # analysis["pred_long_short"] = risk_analysis(long_short_reports["long_short"]) - 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 - TimeInspector.log_cost_time( - "Finished generating analysis," " average turnover is: {0:.4f}.".format(report_normal["turnover"].mean()) - ) - return analysis_df - - def _save_backtest_result(self, pred, analysis, positions, report_normal, performance): - # 1. Result dir. - result_dir = os.path.join(self.config_manager.ex_config.tmp_run_dir, "result") - if not os.path.exists(result_dir): - os.makedirs(result_dir) - - self.ex.add_info( - "task_config", - json.loads(json.dumps(self.config_manager.config, default=str)), - ) - - # 2. Pred. - TimeInspector.set_time_mark() - pred_pkl_path = os.path.join(result_dir, "pred.pkl") - pred.to_pickle(pred_pkl_path) - self.ex.add_artifact(pred_pkl_path) - TimeInspector.log_cost_time("Finished saving pred.pkl to: {}".format(pred_pkl_path)) - - # 3. Ana. - TimeInspector.set_time_mark() - analysis_pkl_path = os.path.join(result_dir, "analysis.pkl") - analysis.to_pickle(analysis_pkl_path) - self.ex.add_artifact(analysis_pkl_path) - TimeInspector.log_cost_time("Finished saving analysis.pkl to: {}".format(analysis_pkl_path)) - - # 4. Pos. - TimeInspector.set_time_mark() - positions_pkl_path = os.path.join(result_dir, "positions.pkl") - with open(positions_pkl_path, "wb") as fp: - pickle.dump(positions, fp) - self.ex.add_artifact(positions_pkl_path) - TimeInspector.log_cost_time("Finished saving positions.pkl to: {}".format(positions_pkl_path)) - - # 5. Report normal. - TimeInspector.set_time_mark() - report_normal_pkl_path = os.path.join(result_dir, "report_normal.pkl") - report_normal.to_pickle(report_normal_pkl_path) - self.ex.add_artifact(report_normal_pkl_path) - TimeInspector.log_cost_time("Finished saving report_normal.pkl to: {}".format(report_normal_pkl_path)) - - # 6. Report long short. - # Deprecated - # for k, name in zip( - # ["long", "short", "long_short"], - # ["report_long.pkl", "report_short.pkl", "report_long_short.pkl"], - # ): - # TimeInspector.set_time_mark() - # pkl_path = os.path.join(result_dir, name) - # long_short_reports[k].to_pickle(pkl_path) - # self.ex.add_artifact(pkl_path) - # TimeInspector.log_cost_time("Finished saving {} to: {}".format(name, pkl_path)) - - # 7. Origin test label. - TimeInspector.set_time_mark() - label_pkl_path = os.path.join(result_dir, "label.pkl") - self.data_handler.get_origin_test_label_with_date( - self.trainer_config.parameters["test_start_date"], - self.trainer_config.parameters["test_end_date"], - ).to_pickle(label_pkl_path) - self.ex.add_artifact(label_pkl_path) - TimeInspector.log_cost_time("Finished saving label.pkl to: {}".format(label_pkl_path)) - - # 8. Experiment info, save the model(s) performance here. - TimeInspector.set_time_mark() - cur_ex_id = self.ex.experiment.current_run._id - exp_info = { - "id": cur_ex_id, - "name": self.ex_config.name, - "performance": performance, - "observer_type": self.ex_config.observer_type, - } - - if self.ex_config.observer_type == ExperimentConfig.OBSERVER_MONGO: - exp_info.update( - { - "mongo_url": self.ex_config.mongo_url, - "db_name": self.ex_config.db_name, - } - ) - else: - exp_info.update({"dir": self.ex_config.global_dir}) - - with open(self.ex_config.exp_info_path, "w") as fp: - json.dump(exp_info, fp, indent=4, sort_keys=True) - self.ex.add_artifact(self.ex_config.exp_info_path) - TimeInspector.log_cost_time("Finished saving ex_info to: {}".format(self.ex_config.exp_info_path)) - - @staticmethod - def compare_config_with_config_manger(config_manager): - """Compare loader model args and current config with ConfigManage - - :param config_manager: ConfigManager - :return: - """ - fetcher = create_fetcher_with_config(config_manager, load_form_loader=True) - loader_mode_config = fetcher.get_experiment( - exp_name=config_manager.ex_config.loader_name, - exp_id=config_manager.ex_config.loader_id, - fields=["task_config"], - )["task_config"] - with open(config_manager.config_path) as fp: - current_config = yaml.load(fp.read()) - current_config = json.loads(json.dumps(current_config, default=str)) - - logger = get_module_logger("Estimator") - - loader_mode_config = copy.deepcopy(loader_mode_config) - current_config = copy.deepcopy(current_config) - - # Require test_mode_config.test_start_date <= current_config.test_start_date - loader_trainer_args = loader_mode_config.get("trainer", {}).get("args", {}) - cur_trainer_args = current_config.get("trainer", {}).get("args", {}) - loader_start_date = loader_trainer_args.pop("test_start_date") - cur_test_start_date = cur_trainer_args.pop("test_start_date") - assert ( - loader_start_date <= cur_test_start_date - ), "Require: loader_mode_config.test_start_date <= current_config.test_start_date" - - # TODO: For the user's own extended `Trainer`, the support is not very good - if "RollingTrainer" == current_config.get("trainer", {}).get("class", None): - loader_period = loader_trainer_args.pop("rolling_period") - cur_period = cur_trainer_args.pop("rolling_period") - assert ( - loader_period == cur_period - ), "Require: loader_mode_config.rolling_period == current_config.rolling_period" - - compare_section = ["trainer", "model", "data"] - for section in compare_section: - changes = compare_dict_value(loader_mode_config.get(section, {}), current_config.get(section, {})) - if changes: - logger.warning("Warning: Loader mode config and current config, `{}` are different:\n".format(section)) diff --git a/qlib/contrib/estimator/fetcher.py b/qlib/contrib/estimator/fetcher.py deleted file mode 100644 index 16ef1dc60..000000000 --- a/qlib/contrib/estimator/fetcher.py +++ /dev/null @@ -1,290 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. - -# coding=utf-8 - -import copy -import json -import yaml -import pickle -import gridfs -import pymongo -from pathlib import Path -from abc import abstractmethod - -from .config import EstimatorConfigManager, ExperimentConfig - - -class Fetcher(object): - """Sacred Experiments Fetcher""" - - @abstractmethod - def _get_experiment(self, exp_name, exp_id): - """Get experiment basic info with experiment and experiment id - - :param exp_name: experiment name - :param exp_id: experiment id - :return: dict - Must contain keys: _id, experiment, info, stop_time. - Here is an example below for FileFetcher. - exp = { - '_id': exp_id, # experiment id - 'path': path, # experiment result path - 'experiment': {'name': exp_name}, # experiment - 'info': info, # experiment config info - 'stop_time': run.get('stop_time', None) # The time the experiment ended - } - - """ - pass - - @abstractmethod - def _list_experiments(self, exp_name=None): - """Get experiment basic info list with experiment name - - :param exp_name: experiment name - :return: list - - """ - pass - - @abstractmethod - def _iter_artifacts(self, experiment): - """Get information about the data in the experiment results - - :param experiment: `self._get_experiment` method result - :return: iterable - Each element contains two elements. - first element : data name - second element : data uri - """ - pass - - @abstractmethod - def _load_data(self, uri): - """Load data with uri - - :param uri: data uri - :return: bytes - """ - pass - - @staticmethod - def model_dict_to_buffer_list(model_dict): - """ - - :param model_dict: - :return: - """ - model_list = [] - is_static_model = False - if len(model_dict) == 1 and list(model_dict.keys())[0] == "model.bin": - is_static_model = True - model_list.append(list(model_dict.values())[0]) - else: - sep = "model.bin_" - model_ids = list(map(lambda x: int(x.split(sep)[1]), model_dict.keys())) - min_id, max_id = min(model_ids), max(model_ids) - for i in range(min_id, max_id + 1): - model_key = sep + str(i) - model = model_dict.get(model_key, None) - if model is None: - print( - "WARNING: In Fetcher, {} is missing when the get model is in the get_experiment function.".format( - model_key - ) - ) - break - else: - model_list.append(model) - - if is_static_model: - return model_list[0] - - return model_list - - def get_experiments(self, exp_name=None): - """Get experiments with name. - - :param exp_name: str - If `exp_name` is set to None, then all experiments will return. - :return: dict - Experiments info dict(Including experiment id and task_config to run the - experiment). Here is an example below. - { - 'a_experiment': [ - { - 'id': '1', - 'task_config': {...} - }, - ... - ] - ... - } - """ - res = dict() - for ex in self._list_experiments(exp_name): - name = ex["experiment"]["name"] - tmp = { - "id": ex["_id"], - "task_config": ex["info"].get("task_config", {}), - "ex_run_stop_time": ex.get("stop_time", None), - } - res.setdefault(name, []).append(tmp) - return res - - def get_experiment(self, exp_name, exp_id, fields=None): - """ - - :param exp_name: - :param exp_id: - :param fields: list - Experiment result fields, if fields is None, will get all fields. - Currently supported fields: - ['model', 'analysis', 'positions', 'report_normal', 'pred', 'task_config', 'label'] - :return: dict - """ - fields = copy.copy(fields) - ex = self._get_experiment(exp_name, exp_id) - results = dict() - model_dict = dict() - for name, uri in self._iter_artifacts(ex): - # When saving, use `sacred.experiment.add_artifact(filename)` , so `name` is os.path.basename(filename) - prefix = name.split(".")[0] - if fields and prefix not in fields: - continue - data = self._load_data(uri) - if prefix == "model": - model_dict[name] = data - else: - results[prefix] = pickle.loads(data) - # Sort model - if model_dict: - results["model"] = self.model_dict_to_buffer_list(model_dict) - - # Info - results["task_config"] = ex["info"].get("task_config", {}) - return results - - def estimator_config_to_dict(self, exp_name, exp_id): - """Save configuration to file - - :param exp_name: - :param exp_id: - :return: config dict - """ - - return self.get_experiment(exp_name, exp_id, fields=["task_config"])["task_config"] - - -class FileFetcher(Fetcher): - """File Fetcher""" - - def __init__(self, experiments_dir): - self.experiments_dir = Path(experiments_dir) - - def _get_experiment(self, exp_name, exp_id): - path = self.experiments_dir / exp_name / "sacred" / str(exp_id) - info_path = path / "info.json" - run_path = path / "run.json" - - if info_path.exists(): - with info_path.open("r") as f: - info = json.load(f) - else: - info = {} - - if run_path.exists(): - with run_path.open("r") as f: - run = json.load(f) - else: - run = {} - - exp = { - "_id": exp_id, - "path": path, - "experiment": {"name": exp_name}, - "info": info, - "stop_time": run.get("stop_time", None), - } - return exp - - def _list_experiments(self, exp_name=None): - runs = [] - for path in self.experiments_dir.glob("{}/sacred/[!_]*".format(exp_name or "*")): - exp_name, exp_id = path.parents[1].name, path.name - runs.append(self._get_experiment(exp_name, exp_id)) - return runs - - def _iter_artifacts(self, experiment): - if experiment is None: - return [] - - for fname in experiment["path"].iterdir(): - if fname.suffix == ".pkl" or ".bin" in fname.suffix: - name, uri = fname.name, str(fname) - yield name, uri - - def _load_data(self, uri): - with open(uri, "rb") as f: - data = f.read() - return data - - -class MongoFetcher(Fetcher): - """MongoDB Fetcher""" - - def __init__(self, mongo_url, db_name): - self.mongo_url = mongo_url - self.db_name = db_name - self.client = None - self.db = None - self.runs = None - self.fs = None - self._setup_mongo_client() - - def _setup_mongo_client(self): - self.client = pymongo.MongoClient(self.mongo_url) - self.db = self.client[self.db_name] - self.runs = self.db.runs - self.fs = gridfs.GridFS(self.db) - - def _get_experiment(self, exp_name, exp_id): - return self.runs.find_one({"_id": exp_id}) - - def _list_experiments(self, exp_name=None): - if exp_name is None: - return self.runs.find() - return self.runs.find({"experiment.name": exp_name}) - - def _iter_artifacts(self, experiment): - if experiment is None: - return [] - for artifact in experiment.get("artifacts", []): - name, uri = artifact["name"], artifact["file_id"] - yield name, uri - - def _load_data(self, uri): - data = self.fs.get(uri).read() - return data - - -def create_fetcher_with_config(config_manager: EstimatorConfigManager, load_form_loader: bool = False): - """Create fetcher with loader config - - :param config_manager: - :param load_form_loader - :return: - """ - flag = "" - if load_form_loader: - flag = "loader_" - if config_manager.ex_config.observer_type == ExperimentConfig.OBSERVER_FILE_STORAGE: - return FileFetcher(eval("config_manager.ex_config.{}_dir".format("loader" if load_form_loader else "global"))) - elif config_manager.ex_config.observer_type == ExperimentConfig.OBSERVER_MONGO: - return MongoFetcher( - mongo_url=eval("config_manager.ex_config.{}mongo_url".format(flag)), - db_name=eval("config_manager.ex_config.{}db_name".format(flag)), - ) - else: - return NotImplementedError("Unkown Backend") diff --git a/qlib/contrib/estimator/launcher.py b/qlib/contrib/estimator/launcher.py deleted file mode 100644 index 80717a32c..000000000 --- a/qlib/contrib/estimator/launcher.py +++ /dev/null @@ -1,115 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. - - -import argparse -import importlib - -from ... import init -from .config import EstimatorConfigManager -from ...log import get_module_logger -from sacred import Experiment -from sacred.observers import FileStorageObserver -from sacred.observers import MongoObserver - -args_parser = argparse.ArgumentParser(prog="estimator") -args_parser.add_argument( - "-c", - "--config_path", - required=True, - type=str, - help="json config path indicates where to load config.", -) - -args = args_parser.parse_args() - - -class SacredExperiment(object): - def __init__( - self, - experiment_name, - experiment_dir, - observer_type="file_storage", - mongo_url=None, - db_name=None, - ): - """__init__ - - :param experiment_name: The name of the experiments. - :param experiment_dir: The directory to store all the results of the experiments(This is for file_storage). - :param observer_type: The observer to record the results: the `file_storage` or `mongo` - :param mongo_url: The mongo url(for mongo observer) - :param db_name: The mongo url(for mongo observer) - """ - self.experiment_name = experiment_name - self.experiment = Experiment(self.experiment_name) - self.experiment_dir = experiment_dir - self.experiment.logger = get_module_logger("Sacred") - - self.observer_type = observer_type - self.mongo_db_url = mongo_url - self.mongo_db_name = db_name - - self._setup_experiment() - - def _setup_experiment(self): - if self.observer_type == "file_storage": - file_storage_observer = FileStorageObserver.create(basedir=self.experiment_dir) - self.experiment.observers.append(file_storage_observer) - elif self.observer_type == "mongo": - mongo_observer = MongoObserver.create(url=self.mongo_db_url, db_name=self.mongo_db_name) - self.experiment.observers.append(mongo_observer) - else: - raise NotImplementedError("Unsupported observer type: {}".format(self.observer_type)) - - def add_artifact(self, filename): - self.experiment.add_artifact(filename) - - def add_info(self, key, value): - self.experiment.info[key] = value - - def main_wrapper(self, func): - return self.experiment.main(func) - - def config_wrapper(self, func): - return self.experiment.config(func) - - -CONFIG_MANAGER = EstimatorConfigManager(args.config_path) - -ex = SacredExperiment( - CONFIG_MANAGER.ex_config.name, - CONFIG_MANAGER.ex_config.sacred_dir, - observer_type=CONFIG_MANAGER.ex_config.observer_type, - mongo_url=CONFIG_MANAGER.ex_config.mongo_url, - db_name=CONFIG_MANAGER.ex_config.db_name, -) - -# qlib init -init( - provider_uri=CONFIG_MANAGER.qlib_data_config.provider_uri, - mount_path=CONFIG_MANAGER.qlib_data_config.mount_path, - auto_mount=CONFIG_MANAGER.qlib_data_config.auto_mount, - region=CONFIG_MANAGER.qlib_data_config.region, - **CONFIG_MANAGER.qlib_data_config.args -) - - -@ex.main_wrapper -def _main(): - # 1. Get estimator class. - estimator_class = getattr( - importlib.import_module(".estimator", package="qlib.contrib.estimator"), - "Estimator", - ) - # 2. Init estimator. - estimator = estimator_class(CONFIG_MANAGER, ex) - estimator.run() - - -def run(): - ex.experiment.run() - - -if __name__ == "__main__": - run() diff --git a/qlib/contrib/estimator/trainer.py b/qlib/contrib/estimator/trainer.py deleted file mode 100644 index 84f387d67..000000000 --- a/qlib/contrib/estimator/trainer.py +++ /dev/null @@ -1,317 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. - -# coding=utf-8 - -from abc import abstractmethod - -import pandas as pd -import numpy as np -from scipy.stats import pearsonr - -from ...log import get_module_logger, TimeInspector -from ...data.dataset.handler import DataHandlerLP -from .launcher import CONFIG_MANAGER -from .fetcher import create_fetcher_with_config -from ...utils import drop_nan_by_y_index, transform_end_date - - -class BaseTrainer(object): - def __init__(self, model_class, model_save_path, model_args, data_handler: DataHandlerLP, sacred_ex, **kwargs): - # 1. Model. - self.model_class = model_class - self.model_save_path = model_save_path - self.model_args = model_args - - # 2. Data handler. - self.data_handler = data_handler - - # 3. Sacred ex. - self.ex = sacred_ex - - # 4. Logger. - self.logger = get_module_logger("Trainer") - - # 5. Data time - self.train_start_date = kwargs.get("train_start_date", None) - self.train_end_date = kwargs.get("train_end_date", None) - self.validate_start_date = kwargs.get("validate_start_date", None) - self.validate_end_date = kwargs.get("validate_end_date", None) - self.test_start_date = kwargs.get("test_start_date", None) - self.test_end_date = transform_end_date(kwargs.get("test_end_date", None)) - - @abstractmethod - def train(self): - """ - Implement this method indicating how to train a model. - """ - pass - - @abstractmethod - def load(self): - """ - Implement this method indicating how to restore a model and the data. - """ - pass - - @abstractmethod - def get_test_pred(self): - """ - Implement this method indicating how to get prediction result(s) from a model. - """ - pass - - def get_test_performance(self): - """ - Implement this method indicating how to get the performance of the model. - """ - raise NotImplementedError(f"Please implement `get_test_performance`") - - def get_test_score(self): - """ - Override this method to transfer the predict result(s) into the score of the stock. - Note: If this is a multi-label training, you need to transfer predict labels into one score. - Or you can just use the result of `get_test_pred()` (you can also process the result) if this is one label training. - We use the first column of the result of `get_test_pred()` as default method (regard it as one label training). - """ - pred = self.get_test_pred() - pred_score = pd.DataFrame(index=pred.index) - pred_score["score"] = pred.iloc(axis=1)[0] - return pred_score - - -class StaticTrainer(BaseTrainer): - def __init__(self, model_class, model_save_path, model_args, data_handler, sacred_ex, **kwargs): - super(StaticTrainer, self).__init__(model_class, model_save_path, model_args, data_handler, sacred_ex, **kwargs) - self.model = None - - split_data = self.data_handler.get_split_data( - self.train_start_date, - self.train_end_date, - self.validate_start_date, - self.validate_end_date, - self.test_start_date, - self.test_end_date, - ) - ( - self.x_train, - self.y_train, - self.x_validate, - self.y_validate, - self.x_test, - self.y_test, - ) = split_data - - def train(self): - TimeInspector.set_time_mark() - model = self.model_class(**self.model_args) - - if CONFIG_MANAGER.ex_config.finetune: - fetcher = create_fetcher_with_config(CONFIG_MANAGER, load_form_loader=True) - loader_model = fetcher.get_experiment( - exp_name=CONFIG_MANAGER.ex_config.loader_name, - exp_id=CONFIG_MANAGER.ex_config.loader_id, - fields=["model"], - )["model"] - - if isinstance(loader_model, list): - model_index = ( - -1 - if CONFIG_MANAGER.ex_config.loader_model_index is None - else CONFIG_MANAGER.ex_config.loader_model_index - ) - loader_model = loader_model[model_index] - - model.load(loader_model) - model.finetune(self.x_train, self.y_train, self.x_validate, self.y_validate) - else: - model.fit(self.x_train, self.y_train, self.x_validate, self.y_validate) - model.save(self.model_save_path) - self.ex.add_artifact(self.model_save_path) - self.model = model - TimeInspector.log_cost_time("Finished training model.") - - def load(self): - model = self.model_class(**self.model_args) - - # Load model - fetcher = create_fetcher_with_config(CONFIG_MANAGER, load_form_loader=True) - loader_model = fetcher.get_experiment( - exp_name=CONFIG_MANAGER.ex_config.loader_name, - exp_id=CONFIG_MANAGER.ex_config.loader_id, - fields=["model"], - )["model"] - - if isinstance(loader_model, list): - model_index = ( - -1 - if CONFIG_MANAGER.ex_config.loader_model_index is None - else CONFIG_MANAGER.ex_config.loader_model_index - ) - loader_model = loader_model[model_index] - - model.load(loader_model) - - # Save model, after load, if you don't save the model, the result of this experiment will be no model - model.save(self.model_save_path) - self.ex.add_artifact(self.model_save_path) - self.model = model - - def get_test_pred(self): - pred = self.model.predict(self.x_test) - pred = pd.DataFrame(pred, index=self.x_test.index, columns=self.y_test.columns) - return pred - - def get_test_performance(self): - try: - model_score = self.model.score(self.x_test, self.y_test) - except NotImplementedError: - model_score = None - # Remove rows from x, y and w, which contain Nan in any columns in y_test. - x_test, y_test, __ = drop_nan_by_y_index(self.x_test, self.y_test) - pred_test = self.model.predict(x_test) - model_pearsonr = pearsonr(np.ravel(pred_test), np.ravel(y_test.values))[0] - - performance = {"model_score": model_score, "model_pearsonr": model_pearsonr} - return performance - - -class RollingTrainer(BaseTrainer): - def __init__(self, model_class, model_save_path, model_args, data_handler, sacred_ex, **kwargs): - super(RollingTrainer, self).__init__( - model_class, model_save_path, model_args, data_handler, sacred_ex, **kwargs - ) - self.rolling_period = kwargs.get("rolling_period", 60) - self.models = [] - self.rolling_data = [] - self.all_x_test = [] - self.all_y_test = [] - for data in self.data_handler.get_rolling_data( - self.train_start_date, - self.train_end_date, - self.validate_start_date, - self.validate_end_date, - self.test_start_date, - self.test_end_date, - self.rolling_period, - ): - self.rolling_data.append(data) - __, __, __, __, x_test, y_test = data - self.all_x_test.append(x_test) - self.all_y_test.append(y_test) - - def train(self): - # 1. Get total data parts. - # total_data_parts = self.data_handler.total_data_parts - # self.logger.warning('Total numbers of model are: {}, start training models...'.format(total_data_parts)) - if CONFIG_MANAGER.ex_config.finetune: - fetcher = create_fetcher_with_config(CONFIG_MANAGER, load_form_loader=True) - loader_model = fetcher.get_experiment( - exp_name=CONFIG_MANAGER.ex_config.loader_name, - exp_id=CONFIG_MANAGER.ex_config.loader_id, - fields=["model"], - )["model"] - loader_model_index = CONFIG_MANAGER.ex_config.loader_model_index - previous_model_path = "" - # 2. Rolling train. - for ( - index, - (x_train, y_train, x_validate, y_validate, x_test, y_test), - ) in enumerate(self.rolling_data): - TimeInspector.set_time_mark() - model = self.model_class(**self.model_args) - - if CONFIG_MANAGER.ex_config.finetune: - # Finetune model - if loader_model_index is None and isinstance(loader_model, list): - try: - model.load(loader_model[index]) - except IndexError: - # Load model by previous_model_path - with open(previous_model_path, "rb") as fp: - model.load(fp) - model.finetune(x_train, y_train, x_validate, y_validate) - else: - - if index == 0: - loader_model = ( - loader_model[loader_model_index] if isinstance(loader_model, list) else loader_model - ) - model.load(loader_model) - else: - with open(previous_model_path, "rb") as fp: - model.load(fp) - - model.finetune(x_train, y_train, x_validate, y_validate) - - else: - model.fit(x_train, y_train, x_validate, y_validate) - - model_save_path = "{}_{}".format(self.model_save_path, index) - model.save(model_save_path) - previous_model_path = model_save_path - self.ex.add_artifact(model_save_path) - self.models.append(model) - TimeInspector.log_cost_time("Finished training model: {}.".format(index + 1)) - - def load(self): - """ - Load the data and the model - """ - fetcher = create_fetcher_with_config(CONFIG_MANAGER, load_form_loader=True) - loader_model = fetcher.get_experiment( - exp_name=CONFIG_MANAGER.ex_config.loader_name, - exp_id=CONFIG_MANAGER.ex_config.loader_id, - fields=["model"], - )["model"] - for index in range(len(self.all_x_test)): - model = self.model_class(**self.model_args) - - model.load(loader_model[index]) - - # Save model - model_save_path = "{}_{}".format(self.model_save_path, index) - model.save(model_save_path) - self.ex.add_artifact(model_save_path) - - self.models.append(model) - - def get_test_pred(self): - """ - Predict the score on test data with the models. - Please ensure the models and data are loaded before call this score. - - :return: the predicted scores for the pred - """ - pred_df_list = [] - y_test_columns = self.all_y_test[0].columns - # Start iteration. - for model, x_test in zip(self.models, self.all_x_test): - pred = model.predict(x_test) - pred_df = pd.DataFrame(pred, index=x_test.index, columns=y_test_columns) - pred_df_list.append(pred_df) - return pd.concat(pred_df_list) - - def get_test_performance(self): - """ - Get the performances of the models - - :return: the performances of models - """ - pred_test_list = [] - y_test_list = [] - scorer = self.models[0]._scorer - for model, x_test, y_test in zip(self.models, self.all_x_test, self.all_y_test): - # Remove rows from x, y and w, which contain Nan in any columns in y_test. - x_test, y_test, __ = drop_nan_by_y_index(x_test, y_test) - pred_test_list.append(model.predict(x_test)) - y_test_list.append(np.squeeze(y_test.values)) - - pred_test_array = np.concatenate(pred_test_list, axis=0) - y_test_array = np.concatenate(y_test_list, axis=0) - - model_score = scorer(y_test_array, pred_test_array) - model_pearsonr = pearsonr(np.ravel(y_test_array), np.ravel(pred_test_array))[0] - - performance = {"model_score": model_score, "model_pearsonr": model_pearsonr} - return performance diff --git a/qlib/data/dataset/__init__.py b/qlib/data/dataset/__init__.py index 74e14f47a..e7d296d73 100644 --- a/qlib/data/dataset/__init__.py +++ b/qlib/data/dataset/__init__.py @@ -95,6 +95,7 @@ class DatasetH(Dataset): - insntance of `DataHandler` - config of `DataHandler`. Please refer to `DataHandler` + segments : list Describe the options to segment the data. Here are some examples: diff --git a/qlib/data/dataset/handler.py b/qlib/data/dataset/handler.py index 89fb3375a..905fcd623 100644 --- a/qlib/data/dataset/handler.py +++ b/qlib/data/dataset/handler.py @@ -265,30 +265,40 @@ class DataHandlerLP(DataHandler): Parameters ---------- infer_processors : list - list of of processors to generate data for inference - example of : - 1) classname & kwargs: - { - "class": "MinMaxNorm", - "kwargs": { - "fit_start_time": "20080101", - "fit_end_time": "20121231" + - list of of processors to generate data for inference + + - example of : + + .. code-block:: + + 1) classname & kwargs: + { + "class": "MinMaxNorm", + "kwargs": { + "fit_start_time": "20080101", + "fit_end_time": "20121231" + } } - } - 2) Only classname: - "DropnaFeature" - 3) object instance of Processor + 2) Only classname: + "DropnaFeature" + 3) object instance of Processor learn_processors : list similar to infer_processors, but for generating data for learning models process_type: str PTYPE_I = 'independent' + - self._infer will processed by infer_processors + - self._learn will be processed by learn_processors + PTYPE_A = 'append' + - self._infer will processed by infer_processors + - self._learn will be processed by infer_processors + learn_processors + - (e.g. self._infer processed by learn_processors ) """ diff --git a/qlib/data/dataset/loader.py b/qlib/data/dataset/loader.py index d1de4821c..a51ea119a 100644 --- a/qlib/data/dataset/loader.py +++ b/qlib/data/dataset/loader.py @@ -23,6 +23,18 @@ class DataLoader(abc.ABC): """ load the data as pd.DataFrame. + Example of the data (The multi-index of the columns is optional.): + + .. code-block:: python + + feature label + $close $volume Ref($close, 1) Mean($close, 3) $high-$low LABEL0 + datetime instrument + 2010-01-04 SH600000 81.807068 17145150.0 83.737389 83.016739 2.741058 0.0032 + SH600004 13.313329 11800983.0 13.313329 13.317701 0.183632 0.0042 + SH600005 37.796539 12231662.0 38.258602 37.919757 0.970325 0.0289 + + Parameters ---------- instruments : str or dict @@ -36,17 +48,6 @@ class DataLoader(abc.ABC): ------- pd.DataFrame: data load from the under layer source - - Example of the data (The multi-index of the columns is optional.): - - .. code-block:: - - feature label - $close $volume Ref($close, 1) Mean($close, 3) $high-$low LABEL0 - datetime instrument - 2010-01-04 SH600000 81.807068 17145150.0 83.737389 83.016739 2.741058 0.0032 - SH600004 13.313329 11800983.0 13.313329 13.317701 0.183632 0.0042 - SH600005 37.796539 12231662.0 38.258602 37.919757 0.970325 0.0289 """ pass @@ -65,7 +66,7 @@ class DLWParser(DataLoader): config : Tuple[list, tuple, dict] Config will be used to describe the fields and column names - .. code-block:: YAML + .. code-block:: := { "group_name1": diff --git a/qlib/workflow/__init__.py b/qlib/workflow/__init__.py index 8944ecbe6..c0745f6d4 100644 --- a/qlib/workflow/__init__.py +++ b/qlib/workflow/__init__.py @@ -10,22 +10,6 @@ from ..utils import Wrapper class QlibRecorder: """ A global system that helps to manage the experiments. - - The components of the system: - 1) ExperimentManager: a class managing experiments. - 2) Experiment: a class of experiment, and each instance of it is responsible for a single experiment. - 3) Recorder: a class of recorder, and each instance of it is responsible for a single run. - - The general structure of the system: - ExperimentManager - - Experiment 1 - - Recorder 1 - - Recorder 2 - - ... - - Experiment 2 - - ... - - ... - """ def __init__(self, exp_manager): @@ -34,16 +18,14 @@ class QlibRecorder: @contextmanager def start(self, experiment_name=None, recorder_name=None): """ - Method to start an experiment. This method can only be called within a Python's `with` statement. + Method to start an experiment. This method can only be called within a Python's `with` statement. Here is the example code: - Use case: - --------- - ``` - with R.start('test', 'recorder_1'): - model.fit(dataset) - R.log... - ... # further operations - ``` + .. code-block:: Python + + with R.start('test', 'recorder_1'): + model.fit(dataset) + R.log... + ... # further operations Parameters ---------- @@ -63,15 +45,14 @@ class QlibRecorder: def start_exp(self, experiment_name=None, recorder_name=None, uri=None): """ Lower level method for starting an experiment. When use this method, one should end the experiment manually - and the status of the recorder may not be handled properly. + and the status of the recorder may not be handled properly. Here is the example code: + + .. code-block:: Python + + R.start_exp(experiment_name='test', recorder_name='recorder_1') + ... # further operations + R.end_exp('FINISHED') or R.end_exp(Recorder.STATUS_S) - Use case: - --------- - ``` - R.start_exp(experiment_name='test', recorder_name='recorder_1') - ... # further operations - R.end_exp('FINISHED') or R.end_exp(Recorder.STATUS_S) - ``` Parameters ---------- @@ -92,15 +73,13 @@ class QlibRecorder: def end_exp(self, recorder_status=Recorder.STATUS_FI): """ Method for ending an experiment manually. It will end the current active experiment, as well as its - active recorder with the specified `status` type. + active recorder with the specified `status` type. Here is the example code of the method: - Use case: - --------- - ``` - R.start_exp(experiment_name='test') - ... # further operations - R.end_exp('FINISHED') or R.end_exp(Recorder.STATUS_S) - ``` + .. code-block:: Python + + R.start_exp(experiment_name='test') + ... # further operations + R.end_exp('FINISHED') or R.end_exp(Recorder.STATUS_S) Parameters ---------- @@ -111,14 +90,12 @@ class QlibRecorder: def search_records(self, experiment_ids, **kwargs): """ - Get a pandas DataFrame of records that fit the search criteria. + Get a pandas DataFrame of records that fit the search criteria. Here is the example code of the method: - Use case: - --------- - ``` - R.log_metrics(m=2.50, step=0) - records = R.search_runs([experiment_id], order_by=["metrics.m DESC"]) - ``` + .. code-block:: Python + + R.log_metrics(m=2.50, step=0) + records = R.search_runs([experiment_id], order_by=["metrics.m DESC"]) Parameters ---------- @@ -146,11 +123,9 @@ class QlibRecorder: """ Method for listing all the existing experiments (except for those being deleted.) - Use case: - --------- - ``` - exps = R.list_experiments() - ``` + .. code-block:: Python + + exps = R.list_experiments() Returns ------- @@ -166,11 +141,11 @@ class QlibRecorder: list all the recorders of the default experiment. If the default experiment doesn't exist, the method will first create the default experiment, and then create a new recorder under it. - Use case: - --------- - ``` - recorders = R.list_recorders(experiment_name='test') - ``` + Here is the example code: + + .. code-block:: Python + + recorders = R.list_recorders(experiment_name='test') Parameters ---------- @@ -191,46 +166,55 @@ class QlibRecorder: True, if no valid experiment is found, this method will create one for you. Otherwise, it will only retrieve a specific experiment or raise an Error. - If `create` is True: - If R's running: - 1) no id or name specified, return the active experiment. - 2) if id or name is specified, return the specified experiment. If no such exp found, - create a new experiment with given id or name, and the experiment is set to be running. - If R's not running: - 1) no id or name specified, create a default experiment, and the experiment is set to be running. - 2) if id or name is specified, return the specified experiment. If no such exp found, - create a new experiment with given name or the default experiment, and the experiment is set to be running. - Else If `create` is False: - If R's running: - 1) no id or name specified, return the active experiment. - 2) if id or name is specified, return the specified experiment. If no such exp found, - raise Error. - If R's not running: - 1) no id or name specified. If the default experiment exists, return it, otherwise, raise Error. - 2) if id or name is specified, return the specified experiment. If no such exp found, - raise Error. + - If '`create`' is True: - Use case: - --------- - ``` - # Case 1 - with R.start('test'): - exp = R.get_exp() - recorders = exp.list_recorders() + - If ``R``'s running: - # Case 2 - with R.start('test'): - exp = R.get_exp('test1') + - no id or name specified, return the active experiment. - # Case 3 - exp = R.get_exp() -> a default experiment. + - if id or name is specified, return the specified experiment. If no such exp found, create a new experiment with given id or name, and the experiment is set to be running. - # Case 4 - exp = R.get_exp(experiment_name='test') + - If ``R``'s not running: - # Case 5 - exp = R.get_exp(create=False) -> the default experiment if exists. - ``` + - no id or name specified, create a default experiment, and the experiment is set to be running. + + - if id or name is specified, return the specified experiment. If no such exp found, create a new experiment with given name or the default experiment, and the experiment is set to be running. + + - Else If '`create`' is False: + + - If ``R``'s running: + + - no id or name specified, return the active experiment. + + - if id or name is specified, return the specified experiment. If no such exp found, raise Error. + + - If ``R``'s not running: + + - no id or name specified. If the default experiment exists, return it, otherwise, raise Error. + + - if id or name is specified, return the specified experiment. If no such exp found, raise Error. + + Here are some use cases: + + .. code-block:: Python + + # Case 1 + with R.start('test'): + exp = R.get_exp() + recorders = exp.list_recorders() + + # Case 2 + with R.start('test'): + exp = R.get_exp('test1') + + # Case 3 + exp = R.get_exp() -> a default experiment. + + # Case 4 + exp = R.get_exp(experiment_name='test') + + # Case 5 + exp = R.get_exp(create=False) -> the default experiment if exists. Parameters ---------- @@ -253,11 +237,11 @@ class QlibRecorder: Method for deleting the experiment with given id or name. At least one of id or name must be given, otherwise, error will occur. - Use case: - --------- - ``` - R.delete_exp(experiment_name='test') - ``` + Here is the example code: + + .. code-block:: Python + + R.delete_exp(experiment_name='test') Parameters ---------- @@ -272,11 +256,11 @@ class QlibRecorder: """ Method for retrieving the uri of current experiment manager. - Use case: - --------- - ``` - uri = R.get_uri() - ``` + Here is the example code: + + .. code-block:: Python + + uri = R.get_uri() Returns ------- @@ -288,35 +272,41 @@ class QlibRecorder: """ Method for retrieving a recorder. - If R's running: 1) no id or name specified, return the active recorder. 2) if id or name is - specified, return the specified recorder. - If R's not running: 1) no id or name specified, raise Error. 2) if id or name is specified, - and the corresponding experiment_name must be given, return the specified recorder. Otherwise, - raise Error. + - If ``R``'s running: + + - no id or name specified, return the active recorder. + + - if id or name is specified, return the specified recorder. + + - If ``R``'s not running: + + - no id or name specified, raise Error. + + - if id or name is specified, and the corresponding experiment_name must be given, return the specified recorder. Otherwise, raise Error. The recorder can be used for further process such as `save_object`, `load_object`, `log_params`, `log_metrics`, etc. - Use case: - --------- - ``` - # Case 1 - with R.start('test'): - recorder = R.get_recorder() + Here are some use cases: - # Case 2 - with R.start('test'): - recorder = R.get_recorder(recorder_id='2e7a4efd66574fa49039e00ffaefa99d') + .. code-block:: Python - # Case 3 - recorder = R.get_recorder() -> Error + # Case 1 + with R.start('test'): + recorder = R.get_recorder() - # Case 4 - recorder = R.get_recorder(recorder_id='2e7a4efd66574fa49039e00ffaefa99d') -> Error + # Case 2 + with R.start('test'): + recorder = R.get_recorder(recorder_id='2e7a4efd66574fa49039e00ffaefa99d') - # Case 5 - recorder = R.get_recorder(recorder_id='2e7a4efd66574fa49039e00ffaefa99d', experiment_name='test') - ``` + # Case 3 + recorder = R.get_recorder() -> Error + + # Case 4 + recorder = R.get_recorder(recorder_id='2e7a4efd66574fa49039e00ffaefa99d') -> Error + + # Case 5 + recorder = R.get_recorder(recorder_id='2e7a4efd66574fa49039e00ffaefa99d', experiment_name='test') Parameters ---------- @@ -340,11 +330,11 @@ class QlibRecorder: Method for deleting the recorders with given id or name. At least one of id or name must be given, otherwise, error will occur. - Use case: - --------- - ``` - R.delete_recorder(recorder_id='2e7a4efd66574fa49039e00ffaefa99d') - ``` + Here is the example code: + + .. code-block:: Python + + R.delete_recorder(recorder_id='2e7a4efd66574fa49039e00ffaefa99d') Parameters ---------- @@ -361,26 +351,25 @@ class QlibRecorder: from a local file/directory, or directly saving objects. User can use valid python's keywords arguments to specify the object to be saved as well as its name (name: value). - If R's running: it will save the objects through the running recorder. - If R's not running: the system will create a default experiment, and a new recorder and - save objects under it. + - If R's running: it will save the objects through the running recorder. + - If R's not running: the system will create a default experiment, and a new recorder and save objects under it. - If one wants to save objects with a specific recorder. It is recommended to first - get the specific recorder through `get_recorder` API and use the recorder the save objects. - The supported arguments are the same as this method. + .. note:: - Use case: - --------- - ``` - # Case 1 - with R.start('test'): - pred = model.predict(dataset) - R.save_objects(**{"pred.pkl": pred}, artifact_path='prediction') + If one wants to save objects with a specific recorder. It is recommended to first get the specific recorder through `get_recorder` API and use the recorder the save objects. The supported arguments are the same as this method. - # Case 2 - with R.start('test'): - R.save_objects(local_path='results/pred.pkl') - ``` + Here are some use cases: + + .. code-block:: Python + + # Case 1 + with R.start('test'): + pred = model.predict(dataset) + R.save_objects(**{"pred.pkl": pred}, artifact_path='prediction') + + # Case 2 + with R.start('test'): + R.save_objects(local_path='results/pred.pkl') Parameters ---------- @@ -393,25 +382,22 @@ class QlibRecorder: def log_params(self, **kwargs): """ - Method for logging parameters during an experiment. + Method for logging parameters during an experiment. In addition to using ``R``, one can also log to a specific recorder after getting it with `get_recorder` API. - If R's running: it will log parameters through the running recorder. - If R's not running: the system will create a default experiment as well as a new recorder, and - log parameters under it. + - If R's running: it will log parameters through the running recorder. + - If R's not running: the system will create a default experiment as well as a new recorder, and log parameters under it. - One can also log to a specific recorder after getting it with `get_recorder` API. + Here are some use cases: - Use case: - --------- - ``` - # Case 1 - with R.start('test'): + .. code-block:: Python + + # Case 1 + with R.start('test'): + R.log_params(learning_rate=0.01) + + # Case 2 R.log_params(learning_rate=0.01) - # Case 2 - R.log_params(learning_rate=0.01) - ``` - Parameters ---------- keyword argument: @@ -421,25 +407,22 @@ class QlibRecorder: def log_metrics(self, step=None, **kwargs): """ - Method for logging metrics during an experiment. + Method for logging metrics during an experiment. In addition to using ``R``, one can also log to a specific recorder after getting it with `get_recorder` API. - If R's running: it will log metrics through the running recorder. - If R's not running: the system will create a default experiment as well as a new recorder, and - log metrics under it. + - If R's running: it will log metrics through the running recorder. + - If R's not running: the system will create a default experiment as well as a new recorder, and log metrics under it. - One can also log to a specific recorder after getting it with `get_recorder` API. + Here are some use cases: - Use case: - --------- - ``` - # Case 1 - with R.start('test'): + .. code-block:: Python + + # Case 1 + with R.start('test'): + R.log_metrics(train_loss=0.33, step=1) + + # Case 2 R.log_metrics(train_loss=0.33, step=1) - # Case 2 - R.log_metrics(train_loss=0.33, step=1) - ``` - Parameters ---------- keyword argument: @@ -449,25 +432,22 @@ class QlibRecorder: def set_tags(self, **kwargs): """ - Method for setting tags for a recorder. + Method for setting tags for a recorder. In addition to using ``R``, one can also set the tag to a specific recorder after getting it with `get_recorder` API. - If R's running: it will set tags through the running recorder. - If R's not running: the system will create a default experiment as well as a new recorder, and - set the tags under it. + - If R's running: it will set tags through the running recorder. + - If R's not running: the system will create a default experiment as well as a new recorder, and set the tags under it. - One can also set the tag to a specific recorder after getting it with `get_recorder` API. + Here are some use cases: - Use case: - --------- - ``` - # Case 1 - with R.start('test'): + .. code-block:: Python + + # Case 1 + with R.start('test'): + R.set_tags(release_version="2.2.0") + + # Case 2 R.set_tags(release_version="2.2.0") - # Case 2 - R.set_tags(release_version="2.2.0") - ``` - Parameters ---------- keyword argument: From 99efaadd384037d638bf0eb00645cf63b4b891a1 Mon Sep 17 00:00:00 2001 From: meng-ustc Date: Thu, 26 Nov 2020 20:19:23 +0800 Subject: [PATCH 176/241] Remove batchsize and add daily-batch mode --- .../benchmarks/GATs/workflow_config_gats.yaml | 1 - examples/workflow_by_code_gats.py | 4 +- qlib/contrib/model/pytorch_gats.py | 66 +++++++++---------- 3 files changed, 32 insertions(+), 39 deletions(-) diff --git a/examples/benchmarks/GATs/workflow_config_gats.yaml b/examples/benchmarks/GATs/workflow_config_gats.yaml index 37bced99d..33aa0fe8d 100644 --- a/examples/benchmarks/GATs/workflow_config_gats.yaml +++ b/examples/benchmarks/GATs/workflow_config_gats.yaml @@ -36,7 +36,6 @@ task: n_epochs: 200 lr: 1e-3 early_stop: 20 - batch_size: 800 metric: loss loss: mse base_model: LSTM diff --git a/examples/workflow_by_code_gats.py b/examples/workflow_by_code_gats.py index b5bad31ec..4bb8b30dd 100644 --- a/examples/workflow_by_code_gats.py +++ b/examples/workflow_by_code_gats.py @@ -66,13 +66,12 @@ if __name__ == "__main__": "n_epochs": 200, "lr": 1e-3, "early_stop": 20, - "batch_size": 800, "metric": "loss", "loss": "mse", "base_model": "LSTM", "with_pretrain": True, "seed": 0, - "GPU": 0, + "GPU": "0", }, }, "dataset": { @@ -95,7 +94,6 @@ if __name__ == "__main__": # "record": ['SignalRecord', 'SigAnaRecord', 'PortAnaRecord'], } - # model = train_model(task) model = init_instance_by_config(task["model"]) dataset = init_instance_by_config(task["dataset"]) model.fit(dataset) diff --git a/qlib/contrib/model/pytorch_gats.py b/qlib/contrib/model/pytorch_gats.py index 7cdfb571a..ef1ce5cc1 100755 --- a/qlib/contrib/model/pytorch_gats.py +++ b/qlib/contrib/model/pytorch_gats.py @@ -9,10 +9,8 @@ import os import numpy as np import pandas as pd import copy -from sklearn.metrics import roc_auc_score, mean_squared_error -import logging -from ...utils import unpack_archive_with_buffer, save_multiple_parts_file, create_save_path, drop_nan_by_y_index -from ...log import get_module_logger, TimeInspector +from ...utils import create_save_path +from ...log import get_module_logger import torch import torch.nn as nn @@ -49,7 +47,6 @@ class GAT(Model): n_epochs=200, lr=0.001, metric="IC", - batch_size=2000, early_stop=20, loss="mse", base_model="GRU", @@ -71,7 +68,6 @@ class GAT(Model): self.n_epochs = n_epochs self.lr = lr self.metric = metric - self.batch_size = batch_size self.early_stop = early_stop self.optimizer = optimizer.lower() self.loss = loss @@ -90,7 +86,6 @@ class GAT(Model): "\nn_epochs : {}" "\nlr : {}" "\nmetric : {}" - "\nbatch_size : {}" "\nearly_stop : {}" "\noptimizer : {}" "\nloss_type : {}" @@ -106,7 +101,6 @@ class GAT(Model): n_epochs, lr, metric, - batch_size, early_stop, optimizer.lower(), loss, @@ -165,23 +159,31 @@ class GAT(Model): def cal_ic(self, pred, label): return torch.mean(pred * label) + def get_daily_inter(self, df, shuffle=False): + # organize the train data into daily inter as daily batches + daily_count = df.groupby(level=0).size().values + daily_index = np.roll(np.cumsum(daily_count), 1) + daily_index[0] = 0 + if shuffle: + # shuffle the daily inter data + daily_shuffle = list(zip(daily_index, daily_count)) + np.random.shuffle(daily_shuffle) + daily_index, daily_count = zip(*daily_shuffle) + return daily_index, daily_count + def train_epoch(self, x_train, y_train): x_train_values = x_train.values y_train_values = np.squeeze(y_train.values) * 100 - self.GAT_model.train() - indices = np.arange(len(x_train_values)) - np.random.shuffle(indices) + # organize the train data into daily inter as daily batches + daily_index, daily_count = self.get_daily_inter(x_train, shuffle=True) - for i in range(len(indices))[:: self.batch_size]: - - if len(indices) - i < self.batch_size: - break - - feature = torch.from_numpy(x_train_values[indices[i : i + self.batch_size]]).float() - label = torch.from_numpy(y_train_values[indices[i : i + self.batch_size]]).float() + for idx, count in zip(daily_index, daily_count): + batch = slice(idx, idx + count) + feature = torch.from_numpy(x_train_values[batch]).float() + label = torch.from_numpy(y_train_values[batch]).float() if self.use_gpu: feature = feature.cuda() @@ -206,15 +208,13 @@ class GAT(Model): scores = [] losses = [] - indices = np.arange(len(x_values)) + # organize the test data into daily inter as daily batches + daily_index, daily_count = self.get_daily_inter(data_x, shuffle=False) - for i in range(len(indices))[:: self.batch_size]: - - if len(indices) - i < self.batch_size: - break - - feature = torch.from_numpy(x_values[indices[i : i + self.batch_size]]).float() - label = torch.from_numpy(y_values[indices[i : i + self.batch_size]]).float() + for idx, count in zip(daily_index, daily_count): + batch = slice(idx, idx + count) + feature = torch.from_numpy(x_values[batch]).float() + label = torch.from_numpy(y_values[batch]).float() if self.use_gpu: feature = feature.cuda() @@ -247,7 +247,6 @@ class GAT(Model): if save_path == None: save_path = create_save_path(save_path) stop_steps = 0 - train_loss = 0 best_score = -np.inf best_epoch = 0 evals_result["train"] = [] @@ -314,17 +313,14 @@ class GAT(Model): index = x_test.index self.GAT_model.eval() x_values = x_test.values - sample_num = x_values.shape[0] preds = [] - for begin in range(sample_num)[:: self.batch_size]: + # organize the data into daily inter as daily batches + daily_index, daily_count = self.get_daily_inter(x_test, shuffle=False) - if sample_num - begin < self.batch_size: - end = sample_num - else: - end = begin + self.batch_size - - x_batch = torch.from_numpy(x_values[begin:end]).float() + for idx, count in zip(daily_index, daily_count): + batch = slice(idx, idx + count) + x_batch = torch.from_numpy(x_values[batch]).float() if self.use_gpu: x_batch = x_batch.cuda() From ab98f443459ad429249c2ad62b123e8c170e21b1 Mon Sep 17 00:00:00 2001 From: meng-ustc Date: Thu, 26 Nov 2020 20:22:51 +0800 Subject: [PATCH 177/241] Remove batchsize and add daily-batch mode --- .../benchmarks/HATS/worflow_config_hats.yaml | 1 - examples/workflow_by_code_hats.py | 3 +- qlib/contrib/model/pytorch_hats.py | 57 ++++++++++--------- 3 files changed, 30 insertions(+), 31 deletions(-) diff --git a/examples/benchmarks/HATS/worflow_config_hats.yaml b/examples/benchmarks/HATS/worflow_config_hats.yaml index d8fb55198..0abed6c62 100644 --- a/examples/benchmarks/HATS/worflow_config_hats.yaml +++ b/examples/benchmarks/HATS/worflow_config_hats.yaml @@ -36,7 +36,6 @@ task: n_epochs: 200 lr: 1e-3 early_stop: 20 - batch_size: 800 metric: IC loss: mse base_model: GRU diff --git a/examples/workflow_by_code_hats.py b/examples/workflow_by_code_hats.py index 67b917f17..8e92804d3 100644 --- a/examples/workflow_by_code_hats.py +++ b/examples/workflow_by_code_hats.py @@ -62,12 +62,11 @@ if __name__ == "__main__": "n_epochs": 200, "lr": 1e-3, "early_stop": 20, - "batch_size": 800, "metric": "IC", "loss": "mse", "base_model": "LSTM", "seed": 0, - "GPU": "0", + "GPU": "2", }, }, "dataset": { diff --git a/qlib/contrib/model/pytorch_hats.py b/qlib/contrib/model/pytorch_hats.py index cdfae0284..db3696fd1 100644 --- a/qlib/contrib/model/pytorch_hats.py +++ b/qlib/contrib/model/pytorch_hats.py @@ -54,7 +54,6 @@ class HATS(Model): n_epochs=200, lr=0.01, metric="IC", - batch_size=800, early_stop=20, loss="mse", base_model="GRU", @@ -76,7 +75,6 @@ class HATS(Model): self.n_epochs = n_epochs self.lr = lr self.metric = metric - self.batch_size = batch_size self.early_stop = early_stop self.optimizer = optimizer.lower() self.loss = loss @@ -95,7 +93,6 @@ class HATS(Model): "\nn_epochs : {}" "\nlr : {}" "\nmetric : {}" - "\nbatch_size : {}" "\nearly_stop : {}" "\noptimizer : {}" "\nloss_type : {}" @@ -111,7 +108,6 @@ class HATS(Model): n_epochs, lr, metric, - batch_size, early_stop, optimizer.lower(), loss, @@ -169,6 +165,18 @@ class HATS(Model): def cal_ic(self, pred, label): return torch.mean(pred * label) + def get_daily_inter(self, df, shuffle=False): + # organize the train data into daily inter as daily batches + daily_count = df.groupby(level=0).size().values + daily_index = np.roll(np.cumsum(daily_count), 1) + daily_index[0] = 0 + if shuffle: + # shuffle the daily inter data + daily_shuffle = list(zip(daily_index, daily_count)) + np.random.shuffle(daily_shuffle) + daily_index, daily_count = zip(*daily_shuffle) + return daily_index, daily_count + def train_epoch(self, x_train, y_train): x_train_values = x_train.values @@ -176,16 +184,13 @@ class HATS(Model): self.HATS_model.train() - indices = np.arange(len(x_train_values)) - np.random.shuffle(indices) + # organize the train data into daily inter as daily batches + daily_index, daily_count = self.get_daily_inter(x_train, shuffle=True) - for i in range(len(indices))[:: self.batch_size]: - - if len(indices) - i < self.batch_size: - break - - feature = torch.from_numpy(x_train_values[indices[i : i + self.batch_size]]).float() - label = torch.from_numpy(y_train_values[indices[i : i + self.batch_size]]).float() + for idx, count in zip(daily_index, daily_count): + batch = slice(idx, idx + count) + feature = torch.from_numpy(x_train_values[batch]).float() + label = torch.from_numpy(y_train_values[batch]).float() if self.use_gpu: feature = feature.cuda() @@ -210,15 +215,13 @@ class HATS(Model): scores = [] losses = [] - indices = np.arange(len(x_values)) + # organize the test data into daily inter as daily batches + daily_index, daily_count = self.get_daily_inter(data_x, shuffle=False) - for i in range(len(indices))[:: self.batch_size]: - - if len(indices) - i < self.batch_size: - break - - feature = torch.from_numpy(x_values[indices[i : i + self.batch_size]]).float() - label = torch.from_numpy(y_values[indices[i : i + self.batch_size]]).float() + for idx, count in zip(daily_index, daily_count): + batch = slice(idx, idx + count) + feature = torch.from_numpy(x_values[batch]).float() + label = torch.from_numpy(y_values[batch]).float() if self.use_gpu: feature = feature.cuda() @@ -317,14 +320,12 @@ class HATS(Model): sample_num = x_values.shape[0] preds = [] - for begin in range(sample_num)[:: self.batch_size]: + # organize the data into daily inter as daily batches + daily_index, daily_count = self.get_daily_inter(x_test, shuffle=False) - if sample_num - begin < self.batch_size: - end = sample_num - else: - end = begin + self.batch_size - - x_batch = torch.from_numpy(x_values[begin:end]).float() + for idx, count in zip(daily_index, daily_count): + batch = slice(idx, idx + count) + x_batch = torch.from_numpy(x_values[batch]).float() if self.use_gpu: x_batch = x_batch.cuda() From 0fbf401c98d1c95c7ed6c2118be08afbbb68359c Mon Sep 17 00:00:00 2001 From: Young Date: Thu, 26 Nov 2020 12:40:50 +0000 Subject: [PATCH 178/241] update trainer and README.md --- README.md | 12 +++++----- docs/_static/img/framework.png | Bin 209724 -> 277491 bytes qlib/model/trainer.py | 40 +++++++++++++++++++++++++++++++++ qlib/workflow/cli.py | 28 ++--------------------- 4 files changed, 47 insertions(+), 33 deletions(-) create mode 100644 qlib/model/trainer.py diff --git a/README.md b/README.md index cd0c8542f..9a4d2d868 100644 --- a/README.md +++ b/README.md @@ -45,13 +45,11 @@ For more details, please refer to our paper ["Qlib: An AI-oriented Quantitative At the module level, Qlib is a platform that consists of the above components. The components are designed as loose-coupled modules and each component could be used stand-alone. -| Name | Description | -| ------ | ----- | -| `Data layer` | `DataServer` focuses on providing high-performance infrastructure for users to manage and retrieve raw data. `DataEnhancement` will preprocess the data and provide the best dataset to be fed into the models. | -| `Interday Model` | `Interday model` focuses on producing prediction scores (aka. _alpha_). Models are trained by `Model Creator` and managed by `Model Manager`. Users could choose one or multiple models for prediction. Multiple models could be combined with `Ensemble` module. | -| `Interday Strategy` | `Portfolio Generator` will take prediction scores as input and output the orders based on the current position to achieve the target portfolio. | -| `Intraday Trading` | `Order Executor` is responsible for executing orders output by `Interday Strategy` and returning the executed results. | -| `Analysis` | Users could get a detailed analysis report of forecasting signals and portfolios in this part. | +| Name | Description | +| ------ | ----- | +| `Infrastructure` layer | `Infrastructure` layer provides underlying support for Quant research. `DataServer` provides high-performance infrastructure for users to manage and retrieve raw data. `Trainer` provides flexible interface to control the training process of models which enable algorithms controlling the training process. | +| `Workflow` layer | `Workflow` layer covers the whole workflow of quantitative investment. `Information Extractor` extracts data for models. `Forecast Model` focuses on producing all kinds of forecast signals (e.g. _alpha_, risk) for other modules. With these signals `Portfolio Generator` will generate the target portfolio and produce orders to be executed by `Order Executor`. | +| `Interface` layer | `Interface` layer tries to present a user-friendly interface for the underlying system. `Analyser` module will provide users detailed analysis reports of forecasting signals, portfolios and execution results | * The modules with hand-drawn style are under development and will be released in the future. * The modules with dashed borders are highly user-customizable and extendible. diff --git a/docs/_static/img/framework.png b/docs/_static/img/framework.png index 673f10e033136dfb87cc0f28af8aa75d5d2a2dd5..d8242f7c192081aa1ebe3f3c266fbb76173ba7df 100644 GIT binary patch literal 277491 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

7$%RKcJJzq%NQARF)8H((y#j} z;?>(JtF;W;xzT(_$hK5U=|?_~FM4%9+RMV+>Y-R>-4WO2Ui2wHni8l+HJXn0o_5bU z-5oG%0Z1gJ+9oO4!d9O?oupo_J~2Q@T`t{$pAzy^R{+575uqB4h$^Q5;Zj%8stPk9I*kApgMcv80Z6h=qnjR^UYjy{QG}3q)Og}CA-$JSC?tgN zF?n4VJ#+WO^F2w?im}cO9FDc<)rfNq|8I=Td+*=NNK*NFb#B_eUQ7Gy>G5iu%R-h? zdmwO6;v!g`9-noC0Lh@h_F;kcw6AozPLEffrj&G`q_j513sqdhtZtNLPXdsG5;p-# zAh@B3S8gT1Ok!EmSL$Ycy5gBsT_V6REY;exl-J6^-8zc{IRpk$Ej>VZ%rwH|`vTQs zC;t!&+O1NV4c^lT>O%z9g7(`t1RFJL(yW25%czj6jssA`l9JMZHeFj>$^4ip@bmT? zb8zp`2ZWGZv8$k>T3AuQ8A%x4d)@@w08sfkL zxeGvz2vA>^=p_-Vc?gsXHwf05>;^<$I`DIr%0kHkn^1!#PRMtR4kjGO*&#-!Osgb)t`WF#5|rpKuo zQvw%In@lNtga9)XSTX<@ie3duv8!Nedc5i(09=yDMC;PyRHrfjSu7JyRw|X%%D<3% zz>q|s(28h@uh`u>IU`>En5DECj|XjRorQN5@#>3!W?TePSjx^}_*y?ZC%ccF@gW1) z3CC+ahH6S%5uSb!r9=dTwxJaKktw2(tFW8CodAudlqTpX_T0S^r+mv2BFrhK_GnT_ zDb>q}SE(tG?+F7L?{jRZMstrcupK+nEYO|yl_qW6e8s6=0t0m@OD_|K)MbqXEl827 z2O-P`hL*Roli4Mu%T=3LLi`D()tGWkE4zAjA1m%ZK@9fgjQtU9AG;iuXf5)ahDGgh= zf6oO=e3Sa(_WfIp7c8Ii`pefZ4J|D!ar4gg7LT7j!oO>ZO8WPz zw0PAmh7z7wCTIo#c&v|8V>3JmVf+||Ss->52BRL&p_a?N%G^<1Y?M)T)9N7}Zl1k% zZ{AyOW>#ZVg~P2X3sr=n=Z{}?6IzSbOJo9c^sYH_@aX$D?p(9a_?Ur} zA=G4}As9MsBgND4@{Nm8IC5xRJ*+1;kA@Jdy`?t+M=jK#1)Hi%)Y-$|x{8rQ?Ez7=Hx^7|n z<0m7&n?%_NZ8Sg#U!A4OA2V$1-gz?@_u`dnTNG%@4(>Y?@?}@(c42XGvA2n_$!AZG z1{tdtt@+ZblTT|rrrMI)t*r&Z%Ry_leeE?gFfcPSb4(30^WL^Lc4h0AZHjPnb#oER zgzb@CfHd*+1of-;FO%^ZZ$Dwvz+3<6_vas3Um;hwNsG_CnegI+rmTc#6F9!*8UkQS zDHT<+B}Syn)#m`n7YeM5Wwl}U?zX`k&`3~*1IzTIK)dK1qftJf1x*|S%sH`4&`)^r_Z8uQ`_^!y(ZcH%dO-si~-9`Ak_PlZau7y)==Q4@g zPq6@sxQ|_c;bFn$Dod%JR3_?y+ra1O@!jX?hmRc;bPVX!WAcP)az5oFy&CiTMq^P5 zb<>nm0b!WyXoU9tJmXad3F=${vQR2eVUR^p7acEl5j4hb=J#dgSeSywz>?lNhkh=P zU0WWD@(t-vly2mcR3bY2&*(>J?K{m!Z^(BPoD|W5*AWg13LxAbb5YpGq)%q-VjR0x^ zLn0_k!-24Vs59m~5-2;DQ05XP6yx<;5`b|4j0X#>gs^n6#9eeGEg`EJ!?NgYa0V#o ziDd$Aj&Y>PRaXfby0MHqf^_bf;#Kb`fT7e?$mxVI>zQ-yXX!PDAwEFJYq6_f(NA-z z4hLY{vXr1a!b{83wlE+(8?6>@p&tSQSS*qP&> z@&hdY+6?`M&;KuiWt^7(6sZ0ZSVmxub?VmU zjJFUJ;)uK$0mn~rO{jkwjT<(Bw*GCv!?7W^4uaVDv>47j3p>-8$(*iE=IG3|ZJqyN zeeCL@^EA3lBd{$f%IA!>5CH3_*JYYAkf*!_JwBf^^uliSr`#;iu;pA%GOky3P@s^Q zF6VNTaTJymaIV>j$%%02&_P(aU>(Su-2my_2=?)qF$zjIP^0{<=o=K56>&hhnqfs& zGn$tb6c=z^Y0Ol<_w+VAd;J)O_8SR&KA!{2*x|-Vt6v*`C$PM0LZk^8Y19nE1`x{M zJZ@7zU1x6qp=1XTFy{fUfvv38+l=_^k#AyN?i)C&UqL~^H>^mR*0id*Aw2Yi#;LAL zYX6}iJ8;TeA`-Whc!=K2o-+65?tMF3o7XUZpQ+45kZVH-=|d?sA2E1T?tvrwkeZ;y z7jd6vEw0DK$9mgKYd?&Netj7LFo&%3z&=C1tX#a>)ZWe3nh=5&pHq{^O-q-FB<`q@%o zKS*Vw2gb(6v;A7MvA%Wh23t~6@)#8XMutX?mrh={#hl{KecCIEiwn6ww2t~#XcW2s&9GTx4a7gz|kwb8Z|RYcpqPL_O$uR!F`ABmADC? zS=SO=v@o~O1hnm7dg9B;9$_>)jzW092dq44PblSNW{M?a@}b{|tysUay+tJSGF zsc-JaNGWAjPYmkMb=LnIBY5V~a0;YX%U(?nn^ZTlAp|f^%@fVLINdNMbqE6trR*gg z)w}{M(S0ny3VWH5({FtE@WDX8%vj0gd%KCgQ>N15Rh<|h=e4Y6zrC9+r~gciQ%)lU zP`m9w7+zPgi}j84SYT61BvQ=qW>2j83k?LHU8u#())?J(ZkDUsS?b)S&zj?FfpDHRUuoy6+ zpCT&e)kTF|=>mi_$o`a_)Y#V(P0Nrea-w6qPUoVc-!wHdt5IfVYSHdQ*a8Rz~O{4way!z^`t7x2v1t_DT1@faY001BWNklFit%N2u1db6|khAt|*ZauinZ6 zoy_o{v$l*H5{4XM8R*Po$zFh;qNGygs$qoEErh^JiA=4tQJJ6i)&WWcT`TD%4 zzCD^En6D>s6MlE){%<_TKlb@!27YY@em7VyDJg*qSI$D6+I6{jsDC$#ii<#PreTL^$y?S*pH8uU0pz*)mS;pi1IZ*vQ*KO#~q44qJN4RyXqFI69I6FK0 z`}Onm^n?>9PH;5#pU-jCx%)L5_P<;AcRVNG7yYxqGO|K8?O6k2J27~;dve*w=nRb= z;gjc1z|*HsVbZ9n(7L5R*E#<9NdfN?-a}GqB8(m~fdj`lP|s^*Pz^AK33bHiFpYF( zq*PZO*uBMQxsuW%&QJ?A!U&q}Pzzm2)5b0?MnsjBmw~Ij2iK)Wdbk1KfCI6Z!(4Tg zmX*T#9jiFMgkFJtI3q5WWjO$EQOz2z-@XDv`;UZz;ylR6R&Y^R2+VcSRoP$Eko83g z8cjJzHP;z^VOKpjF9#g$T)6YZ<2rHi2xKZhLa(lU;cMY%a1y(MkwFEp{nCjL?}IPu1mF~*^gRKx&rT-XiH2Mnt>((?1=FHWUpR^2cS!fH zz30X!#7+v{ycfH_odbHdK}Xt*-DQU_5+WNKodh6~D{NtNyXD6BN?&7v$-LlPkha>jQ z-?VEJ>H%(@4!@*oT)*Mif}+9^u8keBL+I17ZO3uJ+x9;?c|J02)`F?{AmSggIf-x5 zy2U2M)>*J@c7s`y=3E;+b;KM;hdNKa8aJa0XD%@_G&UM)FSEHZed4TJ=PsXf+7lF# z>*DOXO(SnrDmjV4(#6{Ha|ZfyHBg%zezo{K4^8<1L6DlNSQsjE8>l>uivEE3#Z>HEh`z= zuh)?F14s7c%*)c_)u|Mq=<^@_h)^+d*-om zJO;LTcWd(2)V$gmq@q^i^ZkmnJ zAr-}%WB{s_$VA^WvNPnWCM>`ch9TGNTm)S1w<1p61qgeX0>PZ?`Iw!JopwT*y9~pO zro&3ejm^#U~5#&+o`J-tvxkn>IgKMLg)HU1V{u>`UWZZ z;tHcWGp{DiAS=P70a^Nu|)~S z_=3Xxk$r}AmnXbWh?YP9;A~v2n)Labmsfia?%HU?;L+^Tc`L1eva0$m-A3o;=3)Wt zrBGngno?Gb?};&S(dE{)gnORddNHs}TV5luU#F0`_*l$*zK!>5^QJz1;@`zJ5D0`% z-n~yiO+H5zQpi;gDTStbgxAzYRtt|6(2=@KTlOIQ#Nqg3krBdo9}@l{uE5pFHG1#1 z0}5MPNhSgMwy?NpPILc8_lk>(`*#TFRC4F;tsP=JN!R4G4B(8M`>kcyFWMU@;Cm?9)?YmD5z}%#{O2^-D-qq~LdA`1TFE13u+sx7xUEU3y7r z2?uu5<*Ico1p&c`G9WGg!Te0FoI(g$gznvhq6r*btF2~umK2s?oYg*wOt3>CSM@@w zJ^?5ogsil46=0D8<<#H_ZA53Mt~s+;=R< z0FbeiQvI9Zk7^p2=*wGlYbphxHGo1lok8KB^=$sw&W{=RGiTs;gJqrj_4rYfpji`d z(EWSL_!-cse&avu*#354SqE+qbRsoZ*L}pF5m4y>SD1^yi)Bs+wgTSjKJWXxg~^#EBEXAImQt zD{8y{oo4sH4VHEDH+%MMt{Cv2T(_z;?4Ng*|J8NV?f2h-x83!cM%9@*w*~#h^V-vx3K?UD75lv4fb`U96cMg&b38OoVzv-q;8{`7*eZIi>%W`<9-l` z>v1tvw%^Umumg=TR=7VjI>Uj>y?Ff`va+(ELBsmcz@;guax%H&{4L|}1eSNsKV-?2 z=_P8lY*!wS+0c1ptNE0|O-jibG<`u@BD!2_iT3%2q{RDeyZT~9;Od9BX_w!|ynQ}w z{2;wSeTR(?Ij}Pxsg8TL?tjz4zfN4Jzys3d%AN^{?=}t@J5ZCAt=fG3 z%=Nv_bv-V!pna0~;X}`U!+Vl1Uvpug{?I?F0Sx;QCD( zd+jbNEE>0A<(7vXd$vKXHFj9$GC+q@AeNLuD~U|VX`0jJ>ea8KUkw{JZlFb8e%>fW zoH`#E2&YkjmMiZHJ9lZ(W@wwUgdFf8wgJMh)|Q5?8@VLden4!^evk&RBE; z2^71m;xqV>3j0IEfqZyM`HJc}drH+J<>0+7i0{}c) zJbUSyQA5Web$63QW{sUoq@wn`dH$Yt-8xR6C9cB#*dYXfq3Q7|?AEoQKtB-57>i|s zL4lpRM}GdC(_zD^psduiw6KArdXpMvHD*;eF(Y%P%}?&yDexkiY~eh;dJrAFZ0+J9 z1qB7!_a5GHON&z>XuZuaBsV!Nd5V8mzpqor&nO%*Xw>Ib8&^bSeNwHy|LAV9R;$Hh zSf+?m&CUOoH@ffeUYYUl;!+~^9Y5~V+&7G&%xD(0p};cVX|@Wv>IJ1#EOiqJcFsKN zPbi!r40v}L(R!_xW!q9M<7@B9Y1;p?abfS>bL!;m&h=ZnuBvHkHJoS}Kj3S-0A+@u zBRcw2Nn{mqJ|EtwYU&w4GDFBS@lWgpysw`V0uI^5A4NJtIz^1a0r&<{05nPBD&)En zTznRRCqSv15OP4`D%`;7*+Fv`u!9Fo2h6iZu&anyz6KyJR7)F5-9?ze4d)`SxSl$F zJSI6cN&WQ26D*+QrAMpXD4!yw86CF^bTs3Ttp_3&ubw}kk!PdbS8iRN2mpo#1_s+o zOH0R~OZMue>+-$5eSO>;)jo2$nJZ{Acs4_xHBgnUIyigL)VKE^-+zVR5nb%l;#J28gaykK z4}gFiwbB?Mcy!zZ4fJAK~#XD^>$ z7%;MzVBCla`zDN@vZHhFfEZ7=dQK}AuH90~GTU%74FE>lAzqajWG>SIx$+TAOUu3K5u zJUn>JfTXuEuko5rOp`0eGeD-v-@TpFp=Sr|&PFA@O)u8t^V?B|Im7^+t)(Tt_A+~n z39E?N0&<8DlEf1Fg;2OBkqMWl%T?C^$ZN4ourM=D<)oAelEOW-r; z{C$Acz-ER=t}?(ZVyU(jFhoz{D&%tM6){;3z-V6)LiG5Ax`|vxipp(XqLh9B22Nc) zaDTR?$ik#aJC7*BKqTJFf7wI(V@E$`;1A5e?*_{{_iI-t84MpZ5()|nV8QaaFlP98 z@O1mhFz4sT!Y~Y&nwSFq|2K@k94!Bm!B^ET@GsGDtAf#-oE)y}j38I1bH&dQ7*{n2 zD=sePI?XLww16HxdVK%+x9V%F0$$u_RdD-K9j?5fwW>UFCMkP*E8lEIf)EdIr7{-4se|17XvSX2Nz!?r?z zZwHV$yF-dHku%vcF))MB10m4PuRZuQ_l3mtI4)ZmySA8vY*VuW+(uqJ3hxpVAh2s7 z2(4`3%eT*vSC9vf?>_}knJ3qg#?J5N-Rt1ho0r@=7UmW(bjWb9v@i!lgKAtiH&dAb zD>p5LPdV9OW@g4^wtKoXfVb~oLfD}L(4|vXXzSg9TVMVz4vw4%gPvXcz}hVpbor`7 zCX>PV5tG5husW;>S`L0K+CVK^D=uaVHOdDg!rrZv{WVt0)U;4j*bKFTzGWg z<(n7wJqLBg(?#GF=;q?y=+xn}4fKqRHq>^rLGafzXzjK=J-YVZdi2EM)CDW%q1$U0 zg5iIJSueLq+vx#DIc&)%+Vv$C#OQmUjj|TU4Yzht(+etQvT?9xYglD&2`tR7g z{jyT^vFPr@J6K`KQ^cu;0D*M`NKHz#9VD(c9G!&Af(2Oo@X5o1fus7=>Dr;&gq@p$ zF`qPxFl49LRj}pxy*JH*_wBlO>fDL!gS*4i8#iq7pfb<6p;>jaN7EpNF#=j zM`BklqGjdTh)abH>NT{L$b^_d3sFzvyKUbUbTTRR15ukuFPax0a#H0}=1!EDR5M=s?Dex%+;JwT&r_B*1qwlxaXJHyjg6OHj=Wym zUg{7bk%>5vhUe0#UgOyz+xHhYZ0UjLz8L`YqaMW-m|IwMU?{mr0UAhT!Zz1V-RZL> zcw=};NpXHwc2@4Y4+(YbY{Y4^r_C>I)2eNpiIMSViCaZSK25G%kdl@%b>xI0`l*VP zdFk=0W-Ne&0lJM+*gs_4pk*uANR&k{=88EA;Tz*CUDYkGJ)LU?Ce zbZhw1)WWnmPhYQvU0o}TL;A}`TAXTPN^JU)_fe@PEdv`d)oPfu;t`Ni3R?-lWdJJD z#i4P|#d38z5Xi6E{Z5yw&jQf)S_WL~E2+cp-hDDP(rV(7T+CK0t&5RfsdrNinOcUA z0a!gsc(*V!xl)sQ7mHei0Nzv95}B}Xdc67^P}&I?rnA_^nxl+io^KAD|8ex>VdPcR zOVmv-&X#8z6@lghYHLGqm^ZfCv_VyCVGl|S*npt(T2aDz`{oJSffcI#A*RDCWR7E%{$)YId=d8kL%-t3A6t8+}No6eTF3U9f&y2rI^-QJ*Uhi;2o zb@E0a9)tI5|6YUom6nz4zkL0YrnIyKW0R1+uTaR9YkV!2-4ZRU-G4dq#!EL>w{1Yk?Nu9>IfNbzu^iZE@WwUEHel4$6^W~$ z%d1z)YBi0R*Q~bu+`Q#;d%b$|!XP;{3C|&G+^9+KFJ3ukuy^}`6gOx0XD?qp{kU%1 z+Ksk0wy(FW-ARg=uf8&w441v{=HmW2WZQnVjmR!o>LxsluH_@7=7*)pBaZc=&SH=gysQ?b@|p1k1XPe${#Z`MQ6^jP1_?%X#@< zA>_zT=+LGM5PdBW!hmTtGx+wUkOPhjW-NgsO+MG5MOrhObLmX6KIWuDTtW0@R}D`dJcH~{Sscj3v24Rau(Pv+cK+=^pKl1qBE!L~dJXR1NQaJpFX#3( ze)I&OJQkijegf62)c`-gRuB^(4X4hWg5`5pgPlzUKtFQgAmrrcZ~$G)mc!T2`LH%< z6%6V(7{m^CT!$FJybgeO>Cy#+A^`|$*usuITfw@P5GIVA40oPfhm&U_Vf?5G(Ac9n z+<9;lcJAB8?W1>(KG33x9|zzE4;Ts-mNmfES_%=T4*>%V)bp$lwQAM^V?)#Lo#$Ws zZu~oeb(@P494>s@IU=ktB6vB|TKzR45Ruh9SP!d-m8fsx~sma?% za$9qAi$>fDK< zsF6?dpeb~Bo`^&lw?76PWviYknFI&5)?4i6Rv7s{5~_UU~N((ydIZ)nY;5L0X)0FA&lRfF6^|M6+<;b~bh^>}(|qwyfLs zq^V!SmX-IPd9x0BU_m z`S4`)#9>U*Vm`@8UQ+&QudnEO~e%i6!GdUK#VN?YF`{tG^7b)4*Y}xUMLU;gh>S0kxYAjDXj=maG=)a?wpb3XQ+Y z(+k9PI@T%0-(gG;0PV@s<9D)is>Mb5;7FG%-?Bg)fJZ!~&H@Cg2>v1gh%+T@oYYms zxqmC7mIuqDuLz~r0N~7*ysyjJ1^T6Ud-*h;H*?919)mg-vRb;&x~BCj`8&Czk)feL zDt0&%9Tzn#DJo5*r*D8UX{U;ci)>sPImBC-TUb85^|F(pfe~uVIX!qY8o zB2FB(U$|l}njyA35q5gfl(`eUhYT1IJ7L7+6oz3=$H?DC_8-|xvwY#|k{<2*G`6oV zOzYaI`0DPbnun4$>(P&!g9J13eHVtTV z!jS1mJ+Hh^uMq+wmeLnenP^nHTpe-a=))ehY%R(>y&a_z4+)ny{$u=@fxq(%{BE#} zCxiMwc|sgKfBg);eanYS*Dip!R|^mdg#YqfO--sp_bxrbxY}>h^Zn(Wr(IDzXJ@b8GxUk zAJ|xlA>{BbC@n69HvX-_%Dfgh+Ef^dVFx;B_j>5lw+}S+^oEOrzCM@5u7&>?946T~BhE!EDym)~zy#OxG zt{}Cu=giD-zI6KMfA{78cLK}7(+}BDM(+isu%eo&;T*r-Ugz7U&iHQUFQ4Cy zI>o5R3EwPh)L6(;Ksw-Zj2}T3{hLQ|`c~#vojUai*cK~~#usP=5OHa8Wi}y9lEhVj zoes&m@_%jXWCG=Kg`54P>vQn+~bGS^YV#&|AWvpC{l#Qx^!TAe1xh%GV< z2;9<{_5=k5870IfW@lz*Xs+J8jD-f&ax$~0%)fIWJk-~xS<8jzFP}j(C&%ih)h{ob zztXW+x86%AB{MWyO)FELL25B*9)3tk{x)&O829%d61K)aPkd!;Vj2mQUIrkxzMY$m zNJ~rGICJuxH;vEJ001BWNkl4+WpzvU6j`v9Ob$>w7f3E~zxLih2|`^v0d*`(xvx%lmd4Sij4F08fdX zWdEwA>%&@jwG=Xhs4$+V5`at~gzN&!PM1{X%i`}JpNo1D`}ke6<{cY!_waK%_uqQS zx*GQIQF_8h50Rr_sE4;Rnqpl5Lff$fTI)K+oO3aO)+~>D+B=Ef;jyO2XDz0bT?Rmo z{zF7tyb=pNZ75-|1B=vXq-n1M2D(uSN~ui9xre98Rl5lQx*DDaAX^Aq){9U|9&L_JxP`-W|H#Vcf`xb5^cj#u=*3o4(B9)!Qd=<~1y!t#1eCfHwYv z*Kc1tt)q8dw(0*8tesSt=xld*-oGQr6%X{UoBdda>L z)K$*Q212QKa$1VlkrRh!g&o*PE~?RTE@aX0Hmsu`oa6Qb>FmO(~dF2 zCV0=8IzLDP?^`TS^=QB zy(47%iQxS^2M_2mXiVslJ)sY8Jny}2_vRyM=?Znow*4^;Texvl9i(JiT39`wJ#Ajj zyd|@cIv-&#u}{2z<;h4&X}rWugwhk6>ebE8`L$?MZ~BDkW9+4lGq^}DS0R3ff?O`w zHwDW_6sQYJ**k;~r}9$LLf?=Kpp+eEs&C+{Ez`0^puu=BC85Mhn_b+>-p{^-yZ_=< zuYm;{@h5?8dabOTl(N-0{}X7~zaAdMPl93R!`ux{IG=oruujEqcR`>riuZf*hn`u2q;9?dx; zEd=H2Pb%>7Z4T9q&A?ge&Y6FuDnCG}rWD@3iUEDT!S`+Ef6IsE8y13Ry?W52LvJpt z`R4Uoz&GGSfPcV0=%mrj8y)*6j+_eB3`{ssjQA$C6JYns($vKC9LuuayMscCeVes)lX_Nk#8xd{m$GQ(Jo~Plx^2F2`P{4n!Ql%x zZ(FzULrNmAPVKry@`N}94_-Y3d*`lRyw=#*z_f{-yES&dRs+Dy#toW+NGN=A{_;7@ zQAL`!skc{)r~|u>c<}V~20FUiAWah~*n*ycea!4^?bxlmHlrREUp8wlnzckeYWO(s znF}UQKYjl6wmNm{biQ@&!CHL51xjz}I#OKjGw=?4%K6lM-Imo?PMwQPa&)LOIqOrF zrL9PuoA5pk3j*bb_8zV2=iT~t_^I&f3zp8B=*?~Wag@;EAqRGmBgYOo zW_(P?n5y?F@#>LbhxUf94_Z}TR$g|P5K_a?z_9!7puOtWt=fi5T!nc2sHu+c*{zSU zosFI7+4Cpa&t5&PCAO6oh6Nust6kf1gVa@ou{o`8p1pg&PF-gNlCe^m5OZX?5%c6- z!pwzJr8n8=LbjBal|OCU%3pfr`bBdak!|?R3-|h+JbQf3o&&+FzJC2;@a1bR zYS0x@u@nXUgTCZ_<@q*ix%R@f3k!$zA0GAM^)su`9S5$7ZKc(9w9K?vWe>t&<~D># z-Gr!{mrs?M|Ycy-}2o3nak{+adaBt!Mvsc_4BNR zW)CSaeF%?jEq1P;zM^w6Vd-#|fk3INkn0{R;y!u-KI!9VkMu5ynzFfXCwv zuyd)%QcjCkO(2w@F7%sL%WHvo!dR4{0Dk3K?R1u_a|i^Omk&X!APi z<;~>OfvWh`*s~C5g=PuEmRV7E>DO>U35Q%Ty>dJ_{P^``j?fK-&?b3)z+K$uFWttG0l#PkM&W=mCILd zSk`F&;ZVc#M=xtVJsKRtJYaNQFU?JTus1ANtIk$=4j(w$+dt6vTU|$|m{CK=Xa@D^ z?;vs#<)q71!zqP1lxmLVCEOy1sY2R?Z?{%147n_5Ie`w=EexVT5C&MvX9*3XX&xe?NG1zq*c6 z$M#8SNv{kH3{DQ|H@xw}nTzvE^uM{_ds>a^HFkc@|2m{~%Qk1jb|0xm3A{!I%=hOd zPIZwofO3r!1n6kO=ndCu%2gQnj=O$pdovH?2sBQkaiB?r0#(h~4z?mYF1qRFqpNWEFvrZ| z+_uMz91oOeIb*O|=0a{it2QqM?-tF$*2b0t%7*$zkdPJw8f`fU%x&S=$)jLpVf7s> z$Gv|GLAy4=_>q&r$W#xsr2yN4gP>brH|XrwYGEzf!*A;vc6`*k%%KOtp7-8A zyJS}1qv1@TFkIp$#4gF`yO$sH?WGRYfzo$Uw+cFI#mk=Rnx?GWY0R{d%G>vEVh8u1 zhM<<<+uCKDZnkdO=KQtWS2j(XGbyvUw773ZLY6zrvZ0j1jmq zq@)DTzZ!y#=1qNQOK;!H>w;Dz9lkUvT3ze*y(=%*1+CT`3W+e1*on_eWTNqkc=c9F zsTTnvX0>eVv$xKFE6n}!{P3|b@*(+yh{xmQYqi?zjp{eLcx?a4Rr-cT`V?RhPfK2w z)A9$SCl8Bw`ts>0<7&o5JcbFneevNC6Jz5IK%jTDJeuj+rvv7|;>&@guA@_FyEYw4 zI<@O~wnNW$!xN)Yw=7<{pi|E0&#GNPdzk7%!vK*;RFoc@Rhu$wGyr`sk%_*?ZYboc z!$6>y{CU#ew786^oksZK%X%#UdQBn|t^KXf-k-Hx@XTY=^$huI+?v;UAhfq`$uPX` zcFwhs9*Y2CJ^@$+3|T66vF5t5De+1PpE7bF@Ko$7;7q4*q+_vy5`V&@i^VRYN~-!t zf8e1>6MO?L)7#G7ItKOhl)_|fskT2)pLYgBId}|t&!QHUkOZ--z+Jb^jCj>fmcn=~ z%eHrLx8dmP_pd+gyZ_+cgNKiW=_-2I70eggNm`|6DD?S!{xgk6gN2DVXsE>kyRo>q z_}ie-eREz#y+jbrnQAHG)fc~h`P?p`dz-l04vtAj_eZu>WvNPf4Cs=tR4Y%U#VQ>z zv-xdI)Q4^ZIw8{m&*^YloNCwOXAj#C9M#8x&*!)J@K#|*2(yO(WHxN!z9lC&XHq3t z{@S#WSM(FiB>RGD(IeB8rp& z?7fQ!3YJxLbrp3jtGM=pT~ODucGiL&#RfwW5fOx$Wau-=%p@5Q1f)#Kd7uA!-X!C! z-~Qk7|FXOM|Hpe>d#xlVr@W`kxu54Q*(YQ#T!Z%%^w~O#U?Xw zoH*kjQqcSBAGdC?w(t1qbvb=!o^tjjUw^wer><^qyCa4SyX(_0=1hC-xi`Q2-8GXB z9X@Q-gt=eLc`X`>4QJ4NA`&g$6^)cE?pfGl-J0(s7fgTR-kHz8{A|1He}ChSOD0}+ zVyo6|o@3xefG@c8s*7sAnE!e9xu>0b`Gfa7_TG2je_ML%T{oTEwr%@czhAjbs%VVhJ&f!-VrL ztnvn2Qs_uO*AmZya1v?zeI|7xk+}KmID<+Qp;$D<(Qte=GaMj@&V8h}Z;p{PL_)F0 z7<4s2V-?_I@XZR=@f!AiGG`rr**bpFLnrd`WR zm*VN?X5gyJu7#r&;O?4-E*-iV04yD5ag-MQvP49^{Lb_E@S_iK{Z%&^Q!LQ~Uo!DB zNbFLyDvGbZ`U;aKPDcCo9bnsy$j)ks?j2pIs)?domu|T2{+lso^k^J4WE8rz?}kmW zwI=U4r;`hv+I2M@?u=2Gn1?O=b{^);nS=4?k4K-LeiLE!!b{KN>fc?5fx*-}w&W(Hi6TSE>1?2FIPsZ%E$8XSUth5vhl!+-Rb;6MV)PfdN@t3V5w z>3jjpCrlcp2{_6EjqVY2nb@A1H8sO*+bYqTU-cEaCTEUGz#0(_Vc>CJz$NBU(j}YD zj0sHqymUFL*3|aXT1z+L83C_IX$iwsYin=kq`r-rr+9`Ib<^yxO zp~v6z@Ewz$eev1*jqy);Txl)ZS^l_reraV{e7>*;7<4oPZ}|eQ>C!pWT3=42@&P^C z8*ndce1IDmbSiJIJ3hZSe>=}-N3?r{*r5?lInX!q(_~#E@Zn*X~`@N<*t2m00>n zxb|q)dX6FrCpITO9a20XAAkVXM=Hx|9tYIKWh=gU;PPuOIqS|_@9lre38$5Qvvko{ z=T1KRh}&CST^o6k7viFP}8|n*B|@#?o;!-NmKS-c=`BY zkx=Y>5Z&dl^@yZp-M(_w^6saNJ4sxlO<^bBTd?z#6Haps=s#%cq{}aOrm`&d#=^z( z&%5TPt5*E9{iic4LYwOq>?H)A#R5Xv8s!aiUtAeVj3)5skIZ=JrR6KWn>_B!^Upi^ zgwuXcgwHkOh&_AumiZ4Wkk}`^_WR1BmaSUG%R;NqKL6sgx9!-uV}kT%BB8{y0AKpY z+ot~MZ|}Y#F4q&Q!ih(J+O}=%MOR+%;fhr&bQvqXaeU_{k#^@*Mh4+`9+2d2j~)PiEjZgJsE5zWwf7 z(7$M)1i+gP&3+jF*3R;N*x&y#s}S&9P*Bj61O4j*|92UD{g0XOeLT?prCBy$`NKDG z%hf5CMCX5(90%OM<4F@^!%nQ$;Yv(Q)cdjtC7~R^t3>{jWw~rV$6d%rcAJx@0xZ}oq zu&aItz9^Y%y19S;mWsvIkquR>{f6|Vf<)o?fz)>p2>?6+s* zn#+yn5gvbbI?g=nEcD39GshsbYtbYBv|}5l-G2)@cIt>JldncN9KxIL&%%WhE<~HQ zt?~2r-T3&EkBlkVk3T0sAiV$ad#Kw}hYQc2gsnesMu)bY@#ed;(9_csy$brGsIdP& zo~_hk2NGC*`leYv-Ka~z>iiB}+I&3x^uyXHQseRpbEJC(p(!)6A^Mpje7BbOX0FF0- zBvvKb)H^sTbsKNcn2DK@b_lK{qIDj>Qz*`6RF)=gC8h6H+M1A8lrrgB`@@g9t+E@% zfM$+2;7Vo5hGO>+=rk}?i}Q z#5tNsUB+71?53ZJ1A%~angs+OtF@L+{B~G$1cBVJZP7=r*@bNbWx1Nr=7CAJ7lA07 zwLQvLlxwuCmEriE#JClV$=;w#I&9M~z2Nej|8&c=SHURt27Z~pXgKjVfVVND2LtPT z0oV2EZrpRsltrW`di?J9BW3ZwT7=d%a2Nr1c>*q>xXbap%gmw~{Z2Yp(NO$T4PGIR zx2eNmVw)C;^G3oJ7V^=EH+yavx@88H$Z3(EVV(6NCqd z=~z$DW$f`Ip}3eBwe$vD0?^E;3e`Snvwf*UIcDVgyWO_<-S6X}Z`OA^?xIm8zJN=B z!@qrqPIb-linD7rCOp2rxqlsf{?NQlR%M{3yWc;BZ=z3*SC^p;zNwMKrBA&pS8$AKn712=q2O?Fd z6&;m!nMP%4^@+-|K4Ru4y#cpGgkj5?E$#N&_9AAz3#2MCI_XGh>@`?44z@ndo9;fR z9g*h~uz^i>uDhxGX9^rJ8NGke-1xdhYM!O6@}#X-2~|ECj$g@aqMqto!8)pYpnEE^ zD4e)hG42F$dkx1(ZPz6oR`x=G+Y|BVyh2whjzGKv%HlU#iXLFrmwN;5CWq_FaD0^l z6{X#mQw6;Bn+G;F*sl=tc7Qf{gDz8Sh)T>DalbaL+78&YXV-1ShGM*yiN-f%XBW3< ztd|Vz`OtaFaMAn2u%AT^M*YKj3~Q5{gT-(NqP;guFmE$%ht-{byo^ zB|ZXL^X0yvd$x)90!oxp_p{dHLF#d^eFjKX66>~1CSobGk56i5Cn?GY(<>53XVzPq zxEYG?Cc;%7ze}9U#rgh8ZR-n&)U?cUm7%y8X}pDh;``F?%ShIJ?wC3e$88y0^bM6!5KJEXW%!3WzjRPD*FNVPQMdF4jqc2 zhaG{etgQcXykhPXC@eyDwlGTm={N|M|LGL{YZv%O8PWetFD-j?5G?;|&&&UA?fVNX zm#rz4b`u2XC5*tOkgpg)=OX;YMw z(6#Tp>Hem5W&n8eoftgmFkEonh1jwqfo`3gSTMh&$&75`go)<4(@M29PS#gnb{ztJ zgIHG;Mzp%p$Qib7+iED|7hiNSTC{F~&h5Hj{n`y!6fJLa9{5!=t@m|1Ci=z>*UCSU=Kqnm+l{tx(CF&TWagb`Ybz7 zC-F6b6o`vcUDntiSkoj2)I^)~yR7xSzM|Y0Dppo>RM{=XEGNp$1sc{d`M&^zRSLR` z$L}^5LPaP(NijbtQ8r{bhI{&U6YZ!_M+>27DFN*OYlK35Lc_6)2+1V)Faes?>$mo> z8yc1X+?AllW!!BerL_}a>6b{+y`H{K(E(oDGP~tj3+68A?aXoQRy&e~c}M2$HFFn= zKf%noq|{akBk~2@7erRa4uyrq1lp#Vrul--=PN^ryNUS-0y;3!JEGe(^B0Og4WePp zyhY-rD$8Qeks|p+{*@TQAxXt>=&Ok75Jh-K1MeuHI4|Hb?sw5}d?px&GSESx^^s)l z>5J}ClK=o907*naRIfF@NNT#mBFs=wr{)E^f0|mhb|IhxoA=Z>n=*$(@jHm5olver~w%_Y$-N?FhdkUZMNq#>l+kNUx^Fz2HnSsahL*0M)qWHz$I?#NEL9D=-`Ex-o##rh7<2Gn|P^u zZ@_7a3ss@oKWpee0_qxXAjg2xOm4H**sg#z-e4*_H+3DwE&$^h>5#%oUd1|V9+;|$ zu+!sro>38wou-&xA%cU6hkFa%(uqc8Y5YXV(v|;a!Z|?w3dJo2xOZW~mA-)6fX~#m zR&y%IdRH?);tRNbF)0&0x6v~5rQ#6ZbgzfvCCs9>ADt=UkW4991aceiOdEvo_?|pV- z<)&vVA{*s9j)oKGX@+#Be+2Q1is_WRkn_lhKkubg-3mn~Ns*2itD6@q z8ysB4tXJ06(YY;KQHj==p%ii?k6rHnrt6_B{v8t^0)Iv3&5InsFg`dM2Fit;-4J?xNW(0Z35$CVkj%>KMb53cLr4D7EN z_|0HBL&IM1byH#RAMV?M938d4CiDNd_57N?oQbSz9wqhbV?GF$|1(#?{sPM@%f83w z3qHkpXP%FS#yT9*qY#^aiW>tgp(;y9St!+FVkPwGUOfs;mU3cS4FdTC|IXa1GO-am zcKw7-?Ybb#k%jHMelm1w=~nmg1dJY8Mto}xe%xMbmdR`*RPlpx!VcsQ_&c`B-ugWT z=;n3zK{VE~j&`lvn>^^w?Ykl0b%YOZPJqaU`H@ zH3MC|0atGGyQs1(HV?qyq^%ASgCqHFbAbkrn@iiQn`3 zT}DqB4aMgIikWq9Vs(tCC`WX;(t-VqnTNq{$di}vicsu0#q+y4f3a8k%~vEPfaheZ$ay z6W}XdSj(~=9XV{&_{ScYp*nTwJTTASeSP|+`cK4=^RfV-p<)VFUB0tRQt$d&I@QGV z7XihoY-yUp%u%iR$NZuc9oU>_;2Z|BG_2tY_GJu=GMzKlA6TH`q|HZ>!a1z$YoM*q zh-tjghBLos;1mM-YXN|^-k~fiA;z6dxD|w(y?&D=E8XX}h;WLfvikKX?6%JQJ{-Rh zj2ns6LXSV^452JDcxrTQtgv*+`n^-8pSN!}+rQ|s{N}@RN`9=_?z5D2&G8qHs3uk= zl7FnU8Y&uNa|~s*4*>c)IB>qVZ;Iv`Syx+tM!k}N9c(xBZ|XEl2a_?839kxR+_-#O z2S@Ar&ym!#*5a6&rDSs+Sy{7w@cWcGJ;%~)9KGcd2*QWra-TdUl8 z1l}#Q`n+uC$SONtq-`u#phkekdjif6<##pVErXJrTu7iDn(b*7Tih@8>|R^Kq@dK0mob~q1bB*%BP{*azp*7{DJw~ zqv6D@%=*tsjj0Yx5m--X)VSNBFbm2m_V(@mZL~bT2%rzN)=@?3QHSLimD{)5+O)&A zxcdJf>%7$SD{+af z^+OCS%MZGZDUd1hDeI=6_w2ms)i+*x>+z=_*-}?mC%|`kx>zGs?L7`sHzt|yVXcn@ ziMblprn~=FFmMfN{+ea!^o)& z#oi*QVIY1y<1nn))cu&y%S#(WoFx#|@2}_Cbd1js@^ZT=pzx_F{cTQvB zkV78w_+9^JIkf+xd->pM56-}UHv_*JEH^YXAXHvzB3%Bvaj>)eKjT6?2$ugd=XrmD zOF1AwQ`jUzz3!D)h=#@nY^+&hKxgxgs({ZQ zchsVNn~o-@S%7Dc?mld+UW48qzxhnY)}31r+fs$3orFKXp8?3Fn^vH)u>t=4{!M^d zmThRPH()iVv)h31?5u2b>CoMPVgbyena(U1u3QDBEcELsW~skOLu5Y$*bB(cET8%; zh89^ZjH#I-)#UIkhFzW8bu~`i>#NHRn3iMjncLTZ`{_^L3%A>i)@@p2#E{YG+`j8R zUjPSEXIW_2*;!fT{=<6CT3NED_moG*9hCygamm;0tWn>~=l6;B4=J57CV;rLw)O9Q zMecVi!!?5x#F>{Z2OPt4{oPhX!|^*AWV*WqEc5uC#*ix#idASvLw?XLI>gkAawt*4 z-~p{*^~iMNQlH136L2+AvLoTx7hoEqTi4~}=jYdL z3fEj>vGov`>xgi(C*XQ1wf(psps%GAK*SGw{O)_A;rKVq@Iv$b-XixCmErh_q9N5p zqZRm1EQVm|JmW|>{tB4A4C-TPbxZ~nMZ<~bnQ@UtI7;-)qSI!r#ZA181sdBU;1tkr zMnzftLq&8L5f*y<&T}K7xM(g9W6kILirnv+{VYp-1cgBg`t3Y_iV7=wz${W>QncKY zipP@C=J;by+%T|cQ1^?@yXe^7hxIfR5Ya*t(Nqn7SrLs>)MiwV!uDG$LviV53&r#U zp<`EsYED&9Lbv{s8MZIz8fWG{9G}I^M`(uIL39)Y0cO59Y3peYOT9x1lZaJo4Sv8f z`euGv6CU*Wo#K2f%!4T)z6MM!EK;}DWjj8~ZfGbW=r=Uz0>ya66HJ*|RhGu5lfrdm z(dnMPschI3rI+snt2M$>Fgjl7a@I(!m|waeUb*>%DG!hD6bUDmWS9;6eDm$61)rY! z#!Z^&!VcZr%^ZH>p|=9mT9clb?{}LxtI9R;8%X0$0!{b$4Fe!0DM%()LcwLhaZV3^ zw>jzgx>6DD)Xbk{%;zf0VxJOd6*13aMei{=hLcW?CBh%I*01}Da<6W>C(7a*q^{=; zx+GG{3`55SCK0Kg+-Q&T_0L@q3D=GV>(2oFkvEY0i*{b}%o!dDG{`G-jwK@7Tyv`u z!EA?-8thEkG)O7?X{D^zthJvl8ma0V+Q7Do!j?PZX9#p8G0 zT^WjrtYZK`@A;Z#Bd71nA4PuP4LBt_Ds`WP;uk6K3?lWJbg!F^D;&R{nU4XJn>CNh z@8@1qUB3B3o5g64g`T{W8BbbDU7^7*5o3Y?_{wm6mIAgC;T^3pkBCQk{O*OD%BuTY z%CUf1|DZq{JptFnRiUawG#sS_`oa@%8j5_RG;tD?eiW>!2>8Jpa7m7Q%69?! zO#(%)o_fWx@6UbzW={`K-?L9YZ|FBmzkbV+?Ko@6w_hWfObXb1v}$#tP+MBu(Obb% zQ;_8Q6wzgx^@&i_&gXZHi-r^DYUU@ko&4NVR$p(>-Na~!gyQqS7~lywyG6?rFA&2f zQXgy9&nvBpo1Sx_xJ2!p%b-ua=>kHuEOspuJ*+ifn_uL9oR@P}v{mggq~>Ca$Dewp zq~8sV4@`#o4O4DKePbQgM9ScG78r25p?Zz!^42x%!6CVY#@MT@Vg)+3 z>5PKhrU*>b{uGC}PYaz{T(URC)}z4H3z6DQCI?x}!DRfrXNLj50;PoNEza9(BBds) zSUS%cKrPD(m|j;EGNxbhy#Qh9E|*72!P-W!=O6=e1<1-eWcfm$fbCymoTLL@5#NB; zE!&`P8dytrT7bP$3Vl5ig_WJ!uV~nGrKOA9wPSaqkIu@6NvKDM}olpfEv^}rPmpEoiZPKY-4Fd}uO$Dd=gb3%Pq7QH|q z={A2XFTgdWHF;WDi?yK&o38AKmF4lPpjjw;$>s)3m7&B^fD62Sr!yS|BO1@QnfWB` zs2k+#?GwFf>ZK}Fdp$!x2&UgxY|ya$fb(0SFDL68 z7Ab;aMNfMRQL~j zq0_Zhq1bC0>O^L~Rp^}NctUY`Sv?xk%}(o1nb}cQUR%k`JG}vyziFPriCL`qBv`o0 zTj(-&^N~O+<4&ey2o!$vHYpvR;{KEx=z& zS1A&Xe=PKTCLBtjW!^xFIv*{IPhmwf7<{A8pUS+BgyR#zc#^@1#_ZjP_3G7YXQV8) zm=twm#;i~OTGCUqKJ~=Oqov#VA2axxIra5h6j_(_KGfHupr5yqfK}dr>r^=g(HSeo zLMZi~uNlybh7xkG3{lJ{O9#%JZz;}zXM;r(?lvY(q92!sqiyT^ltV2N27+e04S}9v z=0(1sTQsEU>p$@|LHjLBAJU_+htar1$`dDxYcVk_Ry59A==`{9b)raHybOdguiqu6 zPO0CU+-5R`Crc&AX4bKGd8}*8RwOZ4E+{?{nl4d_dTY%i!Fr@OnESOD$AOTih-PI9 z0?e$U;lu(W>R@~h{H`k_WwB>LO764oJbq`B6TP_xLWvE`T6=?OT8}9x#g0`(b3pu+ z*PrUDruS8%qy9kPo}@5J9G%lKV{fv;dw_bBY`st`JwdT`w<6q^=Xbsk3CAA*qmVWK zk(3e>x~ql0o<7C}0_vc#)f04`Dc%VUSsZ~Bj{O|hJ?PoU{h&rH-{1s zjt+`&i)MWh6MaY;U7%>7*Y7-5OxwWvQvxLE-~_T9V>1$lNLk`908c^^1CYhyCoqs% z-wm?nL)#~2^z32?$Xee(P-{KGRIzT;n%D&nt)w9FR9?Voih9CO0JCC3n})1i!+Q1V zWx8pVWwDh+@P6`QNfv|WOnG>`>7XAR2WQ|wpMm`fmJ_uJELc1b->p~%%i+KcSN{>K z%T^iluP&XNik+1KK2r2=ovYbRtRo6zac}D`?oPs7mE$h|VPIYx#w?}GS=7DKP@ zon2<%WZM#p#jFkWIuj2iMS$5Kyalf(A3gJW;n03f@1FZ~5gbTh*|=eoT1aG{&ssfr z(&S>vG#zMbx;VecDb&O?{TK35`6)mR9=~&(=$Uy(?LwP$cZ=u&kKZ-3G88|afH%bx z4NM&}xv!B>{B4ORv9hvzr!$*noimvD@}$F?L0@PAEKN*p5{3Ecu*z%j+ zp!?=XS^NP_m_)!!zJTk-v}xB@Aijl|1~8*{#!)&_7Qc}RlL_bzQpZR>v8*#zUVBD^ zt!G6hJMJPagj-K)Es$pQ?cCHSrRk-+iW-L;-XeGe7MMx=rUXQWn3T zfb*f~YnkJwV-F`*DdrrHrk#EE)a`yv0p*%8xJ3&ZB{a)OY0c44N?xkhf%$nyR`w@J zyKx~w{f?m}%eF+@{HD;U(}3VyfMVW&({wqbq0PfVS~RvFGh??e=pHY5!(iQth#X=9 z<_$QF8J5s{$)V;#>t{W|T$8074cDHg+5VdX?Iu`89Jy3q5<&n|EZl%)X&lfA7}3-mdq(0hgHradd~qDuQ0? z^}CxM43SX$Z(`B~)_?Q{-Qp6B4XdKT?5wP109ChA)}h_{cKbO}7LypPfu0twQ=|2X z#ZdgZw&@3@FwaEv6y^*hqNJI_P;4=n4!1dZXhBh)p?+7DCyF%VYbhiEe2*Aq#n;EX zH8enq7rR?qS$^40>i6+G7nZXWNkRI%FbFI^sO@nMr51wdZZKa;p!ZE?a%ubuvT$ye zttT{+`k08GmqJP*3hFF_B4X^6|*{vNIyWpJsFcc<1)|OfSa;lUhnjr$@8+>A4}M-V z<&*re0I4QHJ)bZ-Pv{MEzh8;AAZb5R&t$Y%<~4FVpb>5Br$=z5a2uj!=y+j znbp?b#J8+DuYH?V1+$*|R16p&yYi9q@2L#M-XW-JZyIRt=UepP>JHApewl&&0+!_k zde+;oWBL>KWBZQnX2(RM{O4Qm!JQ9GGcT9--SHs0c4>MR{~1~9|X() znG>_W!198n^RZ&}GMs*j$f^LJv#06C?y1{lI=9jR&T?d%j;;XJfxVjyi?L;U0+GZf z1C+%vTR^8!rg!d68Fxu1ScZUF0nuQX&t;vNE_1hzPIz*fOmNNenT~eGX_&F3Sn^T85aP$#Kp7{8RXQ1(DOu_5Ul>-tf{@9G~bYH#AyZnQ+if0nIT1YYmF%x>ZW{N5!&ojus0IuOHaUQ zx^(H~#XM-7-Jk~-3`&`F8BH*$If^kOFAdfz%i^<$5CkFY^*4){5~C{M7!VeD{jQ6I zqAKs206z8x+!KwK*WNvk!G9%28zMSU?M#j_$5|e~keKdcaL0_MS+;N7Zy7%jL4eXZ zsm~Ixf_W?m&v*l_KWFwo5{jSAU@=YG_~TyJkZ!GNuK@EM%sj_el+q0fm*1uWqKrOT^t{Y`BS0Ve(m+LYpG*$PeHC+|KQO7Ae1V+Tqh$#JDl-{e zk-e9Xk{8{|P`m_8F(N)rS!$H%sfA8YY)P?TBbXPrg5ytnAevLW1Yy~)w4 zjR57pGIO8wi@rH_b3Q;Xg75_xeK&1(4lC%M5c>UafI>u^o9W1H32$-j;p7H@zRVO< zs>bOn;gkiTRk37bQk+t$IB z+~qc$R71S|ayAKgVTDPe!=C zM$TcLqvEm-K0b@Nk2~Ow%4Y)V>*{&6B0_dH`nh_!wwPttY#FTWQ&^{^qP_ZUH ziEMs^KwGo+a=*q_;<(RJWIKX2uk!`nQzN0o7y^HwSPipbKS-pOd(v|yW^rH?fuNHu zTO8f9XXhP}aN;|mqkA^zo(&$1?%jLp=&^7`MkNfY1$3AeDV~Wd*=CAzi}O&c={1M_P`92 zpE~WHTXEuXrG+e3rnlzDsL2y<)uN3tzOs~M0Hkz~ ziwXuAuq#81x5SjIZL7BE>+NUg)oog~H2_q=r*xw;V`I%)G$iYh?Py^>m+x0^_#SOq zwKMeV<|C5WXn?SEdIjvtK8WF%P^aD9JaBe4#dwrQRvVpjmyX>H)w}tS{Spu@-DBBC zs(V~!Xx803I-9!KqJ6GwtTVqio$f6WbDUD_>(#^G1eAqhE|hLLw)yktqgQ?(v})ZF z1^ET0NU*QtKmy*RpJbm5E zMW?5Mp<;&P^}8mnU%$RZc72<#8N8O1IzrcDwB+Vil*KPp;Fn;jn=^FiidFFol>;vk z_+C%IZC>&-BNB?om^k6{yG3wrMl@V|8(9BY5ntHDpU$#fRWk^d67AquzH}aQx&wI< zj84uuQp{@1??Z`OK>Rz=M0)(rV@(^NGQeV(LI&Eb36%iJLIi`VYEl2j~ygprP zqT%>$tZ^$5_7Lc*ynxfV_m-DecWI^72CxeInw-Dm_Zx~O@Qx?o5*nr%Y3Jj`1Z=gF z7)lO44B%HZ@NIs;Z8VB$>apb9Zp+Tbkr|L+DA~YdZ6`GX^Ce75x*R&+8zZ;1I*WogZkq|{;}iZ!x6vS+{@jfP_4DBPNX<4IYkcnWhquUHxP zscd}3Km}_~<`=n-i-h80PPP`zgNf93(T!Gy;-83vuDrA}FvR0`N|%j|Y1uYfD0*}v zWPy1iIn+_C^;FBE_qB~9)5{8LL4eX`u(*~xO~q|fS#5s>dp?mOaXt11T<@gfX_6bn zwcF;sgYyIV3F*9*EnKrE5&4l$89$&g473!zwXYc%uL{Q=(@a+< zwS97PR*6JJ3422+WnzX>*L$T)aZ8a4)D!3^X)tc0gfmCZpH@ zb>ITK5yJ`wq@3d;VYB@LBKji3QjqJ6fCPhff%QOd&~0+t)AK!pz(Q5jnLsH)Y2Q31uKZEsHhF<1NKbOUg zU2}3VGbJ_Sy8N`Gbu^S%FCG=%fXm2XHitGl_p<#R5!EyBghX6zDy{0L$oi0oTueCK z=XZS~%g(s&^`G+k=6*L|Sf8;9>WUs|H}?H?ZysFF!5P@!Gw_?ia$Q{=?tI{Obn4g{ z*IsqKd9hr+@;d{TAAfKrI(F!Y*WP>uD^@MX-G90dZQ8Wmmt#1f!LqnSUww7TS0g_^ zf4_sNgJ5}IE)hU7pQTt)TU(16GiG4eFq0gy-^Tw)u>8$DtStQwQzl)5pLT9dX^-1? zZ32M;RHdsbpfJRC_h-CD; z6=ktMIF$N>)<}}W8u5!BFFp~3MFw5$QeWdYN#xWy(4b1MB*Ba z8MaQ25lU<2s+vJ6%UYmqZIeUY;4Mr!Qp>0e#Vf=F#~VylNK&0@x?Mq)+hW)=_P^^P^DcOiZi5d`j!>j$`XOC1+^O zE+)}l;yMj=MnN#gbonEp*!zIm1@IXRoM{nAgZ^gQ`bnkeOolZo6R#Bw$Hf52;SIQY znsdBOJeJA6pfbs-t$ zL}wE7$$0_Sr_pe11ha|i5mUMTuxP@TJv^doEA=6PB}!_d6cEhyA4(j~z!D~YRag?4 zb&4LI8OJChiB2<`=5(>;JSa@bXxgKp*l-4w5NMB9`a)l!+vKUIm%m;SZq%SJEym*- zdI2%!F{!PR&s-UfolZh&Crfz z*@{r?YK!Pmkh;NBm}5-dQrC9;QwGLr@X7fB_uQs^GP-E)s6Z=)?p+m%579u1KCSeL z#8$Be_g6re0lzokIx%(sh-`yCS3r9P_s`6UaevozD-)}%tgJEa-d)Snk!X{$*eZ4h zx-Ux2MQpqx>j8%DqL@$53%KXT*8N!6*w|P?;Kvzso6w?_ILQNR6_KS`=lcR~<0LM> z2k0IJ%wv;LoMO5}LN#{)Y8uGC(ChDE+~DP!0$~t~ahTs_D9zDOTnY{1@32I(o?!3Y zTikPS&mHOeeHsH3g-ifgR}kzH&H5fi>OPP%>IZc z0i%GK=lX)~>(X)%(U5C`zS9$MKa)A1&)zU=Z{Ndvw)FJLy(7OUw~2l8Tkp$*zdtww z|Ed}I&0skb9d+qNmt**mra=C8V7XJrPAFM44==v@oB_}B?|m2tG+54ns58$z6F1y& zgUM?B@8Xp9;duXUk%M6QpU+rkj;2nXirKSgBR4k}(uv=n<9{MpUhvI4e7|}b#-BaW zbXL1{%z-B-<)dVDzmYOyc;$7PcUHxljo7yo`7~S@bU;PyJL<4P`_J19pq7}c zpMKtkO|i9RorT__rv2aYQ^FjR44D@J@vpu8656+Kk6`b97&5>_4e#?fkiha|*SwL_ zB8$ofj4E8Xa(=k`l!wnZbXVyx5g=6wp=DP|oJu4VpGM&E8cd9=;R1jnq4=K}bSe=F zpzZPAVD3k?rL{SAO0OltDvv+q@LIVlk&i46fid5gexc1wm~>T$F@h*LC@IdF{>umG)W%=@~uB~#*h|x`xH99}g{fAT>3q1#Lwu2nQGj!5)2j~j`yNKv0 z)tKCFXGxcilgdgS>gnr=MM8;Zh^UZ(yTm0}YmCV+a+Rb&ZgL3`WijwjhLR}z5GjjG z9&%5+b<3Xl`JJ{`mdDN{n_kg`)%ndFy3KVMjxS{!eLDz%n<>s=Ou>rv>;*pK4#sT^yhI$d;KV^eQ zB}q-xV%kRh9DxMjj1fwuIYxP@XZV;9ka^5%&0k6gSsHP~Ta6+8O`)sOR z<18*NPKusU?TLR$MBTtNTe^m+c}qO6wVt4qb*{&sW89^qp@g_DpS0k;?^f^Lwkzj~ zhtL1TS#Y24y#2S}%&Bk4i~bqRboa>@k2;1W3z+CuuiyE2dVfy^VHQL0NiqgWS3i9o z?`5Xz6g0^MIqYvoIb=sIoddQ$j{wlQ2ea2;65b$@%NfMB|%Okri=+q}YGa&Ifj zV%I7n(dX~f4Rs?-lu}vZPa670C~+xw8%_JBRW*H-bP<`hg6JV%!1-d+Js65zK%|}m z+vSa*a~(utGPO#)2Yf--=;+$`VQdTg0$I!$;|scknv1lf`fCidP)ePoG;Ow_ONg+G z34d;0WD?^pF~I`xnlIp*lG%1NT>CULPhf3d?JLS{zE5IOR-}nyzJTk5l=Hrt%S<)_ zw*oqwL04*x*-GK~7D*Z{)OVpi10*q6QgE4++`D^}0L^Hq<`h7^!5~cz9^>ib`Yy6& z%Y(3!QwjP~Paw7JG$3CB&=$>nQ-09(awM|76Yt(zVlmIxpwl#C8W|wU9l^AMfwOh% zx{>0i5Gkt}22zXV9?*orUcYlV5jCb`yIuuwXNDr5HXoDX23g7;DF$nqEM?91^A=fm zH&SwRvkv|gMRcn+eCOmS2OMumH(J~Jcny0-K``%m6Rkz6PD8gQ;}ei_?oQwDUorDg zU(nq~V(>K6cK~+=kmC!wnhJfA_5=KtIIo_OWPI>VU&6v#_j{r+06Wd zFX%q2qBK5AS@b@GBg8z%8*rO@+WZYDjh*rN>x*U=`19tCop@wZZu9=TD-W*e;0)~l z8Tid$`RAW^;>O#j;;17=VEj3$xVGkZ|71=gytm^xs{&q>SV;b2Mej6ik~o4Xaj}=7;G-XFAZ$>GYXnIQH0M%{pa0 z8L)isz4zjlTWTZ|c(XofRGD9!?c#SL4tMl018cTJy$lTSVg zd3kvV<`0BrIZ(4DYG~5ZwHEN5(G$yhwrsCORc!@wy11KY*#gXkf-Nyq8L(d&-(cda zq)RQ`>OFP4u_jsyMV85Y&bVm{AQwGz%k0)@-=+gxU2;*naRnAFUW{&Cx}&I1e-st| zeV+3_yLS#Gu>9=QH@ez{H5zoOV%$9C;c-L6yar%#t^JfWAMOphkBx*A*Q6GoBW)rf9U=*LM?DLwF8Z@py=2^=N#Stpz}j5k0Y)kFuISLRqJ>X1NzX%5v!E8k4jD&~pHF4M?5X zQ~)TCzYWGoNUDJ7bItZcvG)kniwMg-=`Lx->i9*L15bnaE>EFboV^A3%OBILiRkH% zUYIAD~fL zH1H0<8tx5rH}fSD1j|7Zul0Z@;QZ72rR&>gw`*GhqE|qA5i}N&#%XNPKrr8=;CLo4 z(A_X9mM`b5Zmnu70lmW;a5q&4D@$XigXkq9NZiv=((SD*OI$+4j}q&5Ji**aX4@-& z>S$%xFJ_=C2=`?`y_w%|?O3Dt)jDq3`UuU*fidpX2od92}9NF`AvSGe>A`;OX&2{?{(WJ;)Mu5%=$53 z(A|`~8wrWg83~i3u8oy7vJN_*HGiU+cM|ZHvaF@rw%-Eq2pAJ=#iI-S?npEgzn+;- z1CbOho)lWU#7jk1#}0)>B8PC0!qIt!u99e3%{V6Yx?wb=4f^C+#bkZMtg@K+>0eyh zYZn-n2w@aReJ{IC;NwZrGuqYh;x zRD22m$uI8#rzZ}r5DOt_5C}rEOgA9@3;|tdl%vlQ>MbQ_KBu=z=CA~p3TAQKk7aT@DY%l0X)qCwt(SZ zgIzhXvYv+?ddPs{u3ftt&@3RVZ{NQE1&}x2=86?73>`TG@ct2y&VWh*fd6P;&OP^B zNSAznbY}k@U^xRkGu`BW)QOjQkmDLUbZ8SzzB#~_xo8e{MF z92I9A_kA4)QE?PkTxL|=lwtLNp7i(-^h&k{h9CK zJTs3P&b_(!oO{Cko%gpORCp*>M_1yu`~HMb_aZC z|IncI%&2u7W}U%`MN(HW_;GPoc3h1`LmZ2y1Z+3)1@k42#b}R((hBhDAi6FPbU!P( z!?~8VSZkdp70^IYdz0~7R#w)?+S&LsfIHY)batK>%EO7dL^O;U!=)k?3nzY0q|Vo5 zUr4OpB+#9{Y?Vy_v1aN|;HeE?-pfi7e^C}qa48xshBO)T8u|jvmQt>M1;KVkyP3Jx z#N7a0tbmtA|D84Sk;?E`0pzjOv4dgd%_VSat?_uE*z=Gamxylm*K$n>z>z?R#@9)qHD75E3OF^5@SuE=_n8DsrLS(CO%YAk{8BbG=g z`ovWB2yPG5&|0 zpS*thU6y5yDIR!8CvRc?5`U2=6Z;fNh5()~w$Tj!z2hhx`}VuTQB${^HNP4tZf|zD zqfSWHWmIZX&|64$r2qtN}&c!1Pyt$SscBne#?dMa)>j(2bOZBiW`KKxhoc5QZKi zhEDNFVljhcBjsMvqsnoqHL1edG%sY5h&cXh0eL3JS*XwIAO=2lT+gMiQ0?tnd9JbE zAM(~~ZptGG*^v7im}dlv+Sf-)m4}m8Dxw>ibWFi@p=Mj+hGtp74hAjNnja!Xi!^wu zBFPojBm6zQUx-XWGfyS5t^(}o%s5)sd-eM2{F=nlEgk~RGQ+*_CEr9+)4Lqix$NNIf{jR!d!8g7Z-d0K@blF)Ir2}-xqSg2Oz#8u;3+{rj`zSgylh<9os=Sa~c_mu&l9|uXAEbyc9ChJvsYqWf zMqvNp5TsZzB zeE#_gELt|-ROv4|{}S{%Bm=%H6BW4Q!9Qc;_r~}Xr=NNT#-1YYE`&E{zHX!(H(Wgl z^B2xHW@3E@^fBPpak75t_2l^}ZK#XcIKj!2H$0EtkNP$?>SN@RO=-kSq$ycFqn!@fJ*?1&b!k~8Bq3U1q&Af+` z!?dE0!S<8BQ2TmAtcud=CEB)o<+)UmyPLaA`bxx&5N*JC)?ehgDQn8LPyjI#o^@!Z zrf)bYu_uxdEk-l8D8g9)og*)#6_M(5nE3u2%Qd<-UH3ErulPcqhcn-kc$$b# zl^1TunnnwrF#g?4$XKj^vNWXVR7B}614^ zLep+WOfzInW_0UR42@EM@$SlT_PMSE(e~}w)HxIi)jD$v!W;rE^@rR8>+dz3xRZ$_ zLzopXf|P5EZKr2}uoldu52`s?{n%)+CSD>2QKaYuF{u-GdH_oa_&o!6FnE%)QBfXA zenDjNkk2*00ZS_y+bvuvBKq7PbeqU1GtSY}BMh7(ZtxC(7WM`RMFiTyV)V!QMFuTk zJ3S7TYl0$rOk%9eoJ5n)gUJPOe+}GK81k6EoMMq=1qfUGL66y0%|u?|9m#U$Omy8t z<j~9u+xF?5`uRPr$O=TxC@S9(m$HL%D9)Mjmw>Zv6et=v27( zF0laiOR)TN70f-WoO16!2aJF2vpuQ1J$v^2r^>Q`+=kU`Ilq{TITY+Y&(V-#z2~3( zWL~m#V*$$z&-ZH|)c*~z{Bza(JuCh{2aNwGu)HUY{f;~C_@~OUjEihiIMwVu=dY{FtMUK{Qf|<)43so3FbS&6_nh0QAeRzQ8};GQKpJaPc*uJjP3} zyoiZc+=NiKBFtU*F(&`*FSz)^OVFufXMFh4Oe7LX{PDWmz+_|UOH)w0s}9#+ISDVl z{v4JpUyO6kITyXUA8t&>r1C7__x4Hmpn6LsUYY(1^4r9lyF~y1AOJ~3K~%WSHHc1n z^3#98F-MQY$U(;&2F$B(y^PaNKMg&)ABxT2ZN=kLAHfCVFNCX+!m2My@WDs#p>3OX zxaIoWQMx{i4`#iO(@sAXJ-hZcly9jjOXjm^q+j~Sw2aes*X}6p*3;0-_qOd%V0qeY z(;DsEmbax{`?llj>S|sbctYJ99!&|T>#dn*JI=G2>qUx0h^oP9n z$xEsxyqoKCji^i8@f1E&@0Fsb3%cdsSbwe2>Z1YuA=`Rdptya;VK|x+ zm)(#JJ+5P@eSPdnc{F)EnBSDBB!AG|J6>9SHJI;WW}%p7vXmurTB<@Yn+)|Dn(}CB z6|pYhowcDr??5u!R~Aoq0t)X zeL{*(VXde6i@YXEX8r2KV9O$@7%v0ZExEIzTNnMN0DWex34}a_&~1#nq{?jRoaIEEPAmJ!PTe~gkT_-H^pBgi zY|?kgY5ghye9L^%bLyW^)d|VUGCgyj1S$FO1bNK-QehmTjo(p5)#4}LTOMqft z-a#YXottggeVwVV7~IKHt`iHoXJi(MuM^$s()e6V<+j#QanEeTR=gz9TN9QjrCdt! zQM#@!u9a&hDJmhRE__gHSntj)Q@ ziA^B2lAv!DpszuHB=Aj&ag?vf^SSh?h@{49!UNDOzdir(7kTcFN0R4ju>7VIM$C#J!kW4hmf<*$}u?&8_F z{_>lVlaqt5zWEHznzTgc4w>kuX!$Das@-MsiiOhr-s~Cpc;1I73Kg5oVQF1E9b$m$ z_uHx=PT604`MCkh0+f9n3$Q-+E#7?dEnIzR=ABKz>iBcUqfOg3=C|XM*`J_I>vlL} z^jTQ>&2r5CXf_HvbjHY$$71!GuP}Z3D>(7klQDYK8K$az&m*@R8uVd(d*SmH%klk& z&6se>@3DFN_r}#)Yui+m>uS@uLt zo3z-gC9^+)s1!iT3^U5)IX`d zUPeSWGxMg}+`J)Oy0rcw7D-(V@Shp{Q6S_sCJv$_mBtEiY$pma9{l%Vo zN@i9=e4Xf`{YBoX(pL;&NYRS`C$__Nn9xtN#$%(2u+quxH7|S7RoR{r%m&qK7mzWe|zYf3?#$wkCNdY!+wiCjq)jDsKfL&vG$OqZ|l;7frZB z2mU(*_^>{LH7*Ghd*8|$4BfAR6&7K$=!D~u)H@oymH;0SHu!>`3F3mTVQ&Vj5CIDu zGa_bo#Y?Lfg4rXE$YkXVWxMV~W?W+tttBz<%0fw|+(us!(8f;d-W_|k|62M=Rel%j&o#qM7LB%9msm_Q!C1kB;|LhafQ(l$ zfeIAxR6)oy*{sQEVmdKxBtmyGK;yr#_JlIwCcc7|TU>F6S(ZBrmWe zMhaLd8Poot`%?4$(bPxGd<5*qe(^KNIMiG77_^QUX@611`P%$0FpHUBI|8D<>}FF& zr=FTaN|FZjw>3ZfczJHKrb`(}DbRLb$bCxINqrt$Jf70HPm#LOiMY#LL-K2YC>I#N zi}`ghEhgf#q4h{7N=!8C(0Z2SHG@=@T)#0+G?ilhE-#mQ?~b#pyqU=Q6G2TR&?lPh z+06Vb5nap7mp0TP$0Ernh?@I@nX!(48TZb7bxGBsLqbdY9$ze`XGSA^pdA>2{doj_ zHCW#I!&cn$@SQmK%<(7=_4wI)Es3ngi?2?@pKiDfEt>Dui{yR>mVd5-?A)vae#IGE z=(xWGmVc=-x+e`=zDJG`a4pWj4Yjd9H{j0s1|2D9^X9c<8y~w!B zpUeIKT*drfZAi#@_MGEA=ShI@gb5QcY}hbJCD+l3|J)ojWFeH7m;aLi*`CMjPhffB zvbnf?{8fl;SZ5-hq=GChTDA&xcCFF;Hf`J-v*&+^r=BzKDLAy(p}1kfpYZMawRq|E zX}I9L3!zElhizN1aQmmO~d|@fX!O-k%3}w zX}mObHZwmdc+uD0-9vP<@n~|2*0@+H8t)6bjTw;B;?YzISU2~Dyng4i@{+2Hgf%T}>f|VL;Z0LPCTi~wx{on#;D9{1%@|xO24S&qQYu;@CQcJng+JuZ zWF=;*UvCi5NCQKO_)50*CMfGwB7O}_9UE$7W8vgRNoMmGd5l%OI2jYGg_45}R`>a{ z`Ks}7o=fI71QU+n^_vW})WJ1M6~sk@r#)QN}zS zg!MTGb3aG-D$g)8PFBEH*TFn2y$P3TlVPsB7U8UdY?iLM_i*wB29IfI2$gwOQ$m&Q z!?2I@2it#Ae_!F`3Z)F=;sUCzTWsgLW-7)Ki1DC>k+w};SnCN&(N+eFNtlTpG7=5u z{vh>@Fdh6MkFX{LK;HqzjevD?Ae3(;0r6<+W6j);Yj^p?G2HZ9lPaV%&jfM4o#rD1 zyjPSahceR~)_7dBy|z}vT}t1_M0H>^24R;!=r*qN#{F60b`8DWf^s_>3+0ij=M=1Q z1lPcVP@B&()o<@SKy3uLqe6P9L@&ikQ#V1gY_7z_;M!m0F@|iJc}>k==5`=X`$OKr z@kr_|&AOc;(gi{Hh*&t;S>}zHuV+OQ8dlV^&&pgP)$6nHg0}ragXFt619~8Vr};u& zBmXd)e72q^o(7VMEOCHkk;JhUsSj8Tx0#Q0s?YJ#>SMI#H^BT#T`g|Q%cGA;;WcKQ zqnV#}d?&Kk{wXA4N~JsRUJuE{%xMeUHrqo_x^Va;Xns_j`uD}UJaCQ!Bk)^|z^?|& zPIT1Ozq<~-5AE|$*C{WQue~`Pvu3~lUmYFwuRx^(vgGDPm-^5{#;4^chFN=RVH=u7 zIbd6k+0)?5(HigB$ElX?saf7r3)|2*%sHl^K3252|4lUPe+4QX*KaXalMRPG@9XEl zazoQCr_%g$H0;KW8=E=ZvyXhXr)ipe_H&wM$2d&>d{0gF{sNYlfAuLAFQ1QLLq-@1 zv;f1LoLof9S7TSrPIT&!$t!kr%6*yNHp7$ZHy7W3j;qKY92m1EzXY|MN{7R*H@Q^`h+Po=R=C#IC z&pm-r$B%+L-;KPSMkxEX+yLbB&bkmUy!IT14;zloT{@vlL3cDis3pWrJG#CEuf6uF zsp#K%^L<8l{NNM!A`%Ikd6X)5zrIHpnz-DLYygOE`ma+T#rU!3BduypjFiM&$)Cll z*JI=6YIF+}Ftax$KUi9fge7|ucPCE8fbS(I{q$b$^!160QnO&oZ>^qLTc-T0l zc{YL8XF+AXrY{yrh6!{CQ|3iCR#J5|K(nBTHS9ixp$;pJ#*t_=*ifJAjfBjLStRv| z4VEaC)}Px!aG_?!rw_ESdTRQgiwNXHk1#3|IP&(*g^041@DD@P=e> z6WhNx`fb2y>g8B*gLjes0*dGPf?mUD$eh!F?dKF|z1?JIe~Br|I*N}V;2K}ZGtG=) zBzc@-dY1@`3PNt9Q+HZCnv!w;oo(Ceteo7lIh|evMWtYdNGi&s-yKGrUdo`Yu+Tq{ z^$n1X3I$vZqNGhc#--Ijn{A07+oqK0c0ICTW}2`>09*s`ZH^y6X02Df#>zTQjL!^i+wgxXJP}tc}Gt^=5ox~)0@WaAs3w6PYy;s2`XgtY_{-%RS&+i2z$npku7u z+yQn?+7Gfnk=%E2>UN$%%{2}-8u}D&=;fnzhZT)gzFjHmE%yZ&%DD(j6 z(E2RyNOCnX4*p=;JWuh!;1pli{4TPwAPgiM2p1N4Zf*FS9{BeIBe1`Wz<&gmC8zY! z$q!-jR1+n*SM7pxFUHLiZ$WNuCUf-{+J0AA{v`vZh852@-Y|~(|07tIKXX8r{JUtC z#b8T5YZyt@kmkE5Aa}sqp68PPcEFx9#(UDo8^&1uH&vE@&a|uHbJVaheCC;F{!6ed z8wU;P@Xok(?AY-qkyLw9-Wvk)hNfwIp3BjnI~x=~M;~t(54FF5<faP&#pO2@eO~!E}M`7p@N1;jHK?bls^u)b5Zq#uY zaOhw|tA6*Rw=wSA@o3RZ=-}N8^}(5jv6)+&D$J=Suj&`Z2JCtoOQ}Lqm%x3+WrTYU)^FukFk`x{rK~T zI}+K@v?50I9KUvDER>s*`wwPxVoDDx3>AD93#Y1~xWXT-k34ywfi;S-gBe47 zJKaZ7Z>lR>Rnbqmgc7Mf4eS9Z%A?771Uih^Hg2@#rPW6e8<*NeqS0+Q*Rg2oC7}?r z#-zex&y%r8O5ChH8dgyzhgxXsN@(kC7&tu;@)*rRES$I$M2{*(&lYsgRBtOH$q8Di z2?D}I59l~gXLDl@hGZEto(L3qo{;`oYSNcNUUd}ZdJ+INK(iU=2e}C8f^_kc;j1^a9(U?kKso;afw95+n zL;3YGhs+w3_&3}D@Q&PE45+Q8*O~P$B62seTtm`Ux>0Qne+i}XH88j^=$7hW#*tj~ ztl(yFxsK84bW;VI0p=A@wAdfa08%smQOS4a8gaSuUV%HY2?jFc*m zB&R6CIA*%pQt9bhTQe2WY@2x`YrKIRdXsD{0U?@+Bg*LfWuq&HjP&3>O*CE;50>aC zvj!BuE7agXHgh~4PTm29KeEOJ{vywd_16?m%>eOHM0Bw*V9Lrix3qTD%-7IM$>_md zlQhx=1iG7vUnZtwRCD*xf&v31#ehx#qnMYSWb5>hjzt|-RYa11){sgw-&>f?7>-2} zPcf)KBp4d{g2GVycg#i|VKFPbLN<5xk!LuC$mkeN{1d6Z%-owLDvxzXf_j)4N@DW_ zfW{)pIYd~(49Q-Uig3MidM1tw50a*fw9=gvE0Kh>1n6xsJ(TZ|O`neKy3GY~gc%oF znnu4WLN%&lRw?j_qblVt@q$w%zQtXEB!@YV-R_u9Tmn(=&;pk{{3N#70EFF2VU@EZv=bH|aF=_>S2;;i{7F3(~6QbAVc;Dc!?yWQ|2r?=ho*m{KhVkGwd?BFQs}@T6wm4pMhW;&an2U-VRDdRy9k)&3wot$CqVgG2F6<|cW{U9t(RrofN2SszmbZf=(Mvg%yWqJ ze5uMS=9_Fy>x4Q9)^~#F2BnllY6%f+5x^4T(ko{t5BK)=3XR%SGZpXzgEngLG~?VW zRc_WPMQV&@9$i@EH6in{NK$CwN0L={h_9P3VLmfDZ9bftclbkIqo+2{R3w>FL@{5` zZF0Abz7{kIO8?y-^w!gkGkvReDNOoxF7Kcy$0;YOA|LUtGh_+iM}H5qgiIde68_@rJ6jav16gu zctRl!91V9mujF4y4p-P(Q~ zkEA};ta~x>7y)!<>0~d z7JS``buC=G)(|QFf`aBSHo4@qR!-+eTQiE}SY%Kr)AOJ~3K~yrJ)_%?(YX8@)q+&4?eF2*f zV^uRE5X_kRm4%Hd;V8~{2*_qb6vSV!rf)?0AlmQpaPn0m+6rb#3pm*qau0T#&xI<@ z%(GbQ#|ZcuMAr}~!NgzrgPt4e=PH_flbM7PK9E^Is@OG8EB!7(^15lB7$okVzho(Z z#{Q5eAdcahb)ga`Z9$HA-l=;*$x{i!j*}1O0(JcQ5_iz9#?=fgnxxDSR+c0_Z zWb98MZO^&=zYdnA0xZ>M(WdWDZC`=qufks#8up<@hoe>7gJJ75TDNEiZ<~Vp+kN@1 z=kVF*pJC#J8xd&V(R`k^>+nrHikjLwJpaP8xODuL#_9UCcV9wuO$0Yza~m4vH8Oo) zcEC;PHst2~krL#|%XT;nhDnwQPW($wO#=1z)mmoJ#9s7ez(D1my3gRM|NW0B-? z0s_8}$50^Rk>rC+=nkTHVe^g9>S%wD{PpI~!l_Cq-r@^pw8%~?FG<|26y2;fZrvSo zBmi8uQm)*d1&6e$a{3DRu$dnr&F}e(z30Xv2{GNe45UW-gZUrFqSZ$ec$U^WtQ5WJ z3ufGLW0BMdfIng2%Rne&a3<7OaC$zOC>Zs=^HQHNDZ`)TJ(LLIoI6qamz5-MQwq0g z=nD%&?Wai;j&9*v4B}m2RpwORr251_gfbc0cWCHUfLcJ{djPtJK}XeX+qNR7X;Y!R zmom7QRcDVV=v83uvussmf90~If*b|unG#)<1qrPH>M7@OsK43RoUv@Dhd6GylFth8 zxePQV%hli4t!)NCN0MO>no5+6`R{PzQYLx`;5mEhYh#h*CSt1a6}fv#-@i?I<6J#VAnX$e2+ik=1!= z8t!&nv!kiEL8xYp6Nzx0L^plAD%CDGhu3M1&jLjrBOQrF5(5dekU`TMI?u-=$qN{G z98A;wMVZ*U%=3#T0D805cXQJwZ>k-;mNL^+Ts~7 zWu2Za6>5M_(Y3oqI?N+6dJ_XSBL1^4=rzWN@o?f%D0L0l`m};#@7uD$GjrPfJBN+# zzpP2i#tS8K?6+E<2hMw71oqVk{A#eguP$Vd^Be%n4F<%aY&WFs{zl{et55rX6)Zb6 z>;C=w|0FN@R}a`eI>EjI%O$0&43+wzW(T1-RBWoozWl=atE~Dij*VNZ(YB?#9zaVu z>YIv)0h&@>7X5NwP9Dlr>#%W4HM(@@juuUK%Lz-?eQAutiaH(&MarCe`_3P*CKg81 z#?8>pUu>w)vD7-_q~En5gvNRD{*r04=Dv@aAHRo7F1QRmx*le9#X=RACdd2Toy@;~ z9gkpV&5!8Wxlg?#|F@NE46qmDF3}=8O)ACaI(BS>r*(U@X_;?8u}DsOcFtr$OOwx} zA}!Z0RPQC9FT|qH=HsXlN5Svugtjf~S*O1^v-=xZo-%Rzx89De|GsO7edB=Rdkysk zy$hY2C=Vw}iD3Cd-mdbQn9fKBDvJZI=H6_n7K(X3h*FB{4inQF=h%2S*+&yCA?E2) zC3ZfKMUzrJ?x#rI>I>!@tz&s4aTgdTfXJIhdO+t;!ME{9>J+ew!FqC`DHb$S^L> znbwbpM)-nmBkoVE+vKa;Su>A`wvlS_!nYYolJwz&hnQKk01-NZ(t~SDmFuNqo{5j+1x&a}r5x z1k?t9$SoPVC|^}|*vGFe`J%PE<;HO>c*8BFx7N4jBK3?!fobS7ovLo;S*w~s zP(u~-?NX^01_u#du$fmBhP?Fw@5Xf-C`c3ca`gOW%%aKTS@YW#sU$P*g2lD0`F3LE z0_mIlA@9raXz~tbOdz61d_i|+PD_&k%jN}4#031+bud4f-b{1Bw2lcwkwb%o3T}QA z!pS~NSV+v@r`bAOBmriw*Hj+~TQ3B0GA~yh=I)#|wKKm9_>{m=U#_QjtR^KHy(@&I zly%e|5sRcIY3TDUD6+IYqJ43Di3*cv{Q-cwiqwICH$)RHdC}=KtzlrjKa?5Mvek)h zmPKDMu)-hm3~|O;%)7LPY$7cPgtEEg(d2z#I?U+&fxr8UJP%}l=j89Jp#&Q5nD3RB zCQc=-CWG~QQZ&~W^vGtGY>HGccw3;zYo66C(`GW6jWM$PA&=;vrTY4`rPKgs?gH>p zj!Sq&Y3dTKc@mgp!y^(X@?Ix~UJO1-6Q5of^v)lm*I? zQ!EBzIj$Up0=-b1t}zi+5Em6+$N*rcmB-iP_6Kjl(Ibw**i+7ft!;cA7j`YsxKR@m z=_F0ANv=n#zfu{NCg5Ev&;p=Cg@>B&lO`JgJ9ceHTH9#Z^k8^ecfhw*UmN|i0A)*A zrr-L+HwMHvJm>PSmg3`uAK~~>Cm>YV(}3{3ZTlfu4tMEU7>li~3S4~W=^^Jnj5dvN zg8CudR?MTM@~cQSVdfqnzMpM~gRNAV<>vc0jE(tvMWRik#^28ygLTqB~BcbIbL3DLFW?d4@D~%izHqYJ+^Mu-JWMtRSImG~y5KrO zjG&~#o?F*UjEvfv7yY1nqby}Q^ zu>s&41(2C@mZn~Y&0}oXy$wyhGYTr9W)mwu&`9ee4CpzIBn@ zugM}e^k3uB3>xGQxkt!G5RvL5^BapM*UN8~Kjg_!a}2OZPGk*nZ@$CP9%sfj^+zzS zg>f(T{GmLO8ZIh}3zo>Q0Bw?XR_#_&Bmz4eOXS#4C>`?@(g;rLhLYWg`EO-4byS=DOmo7ENVW0JQfHr{*Gv>xvwEBK# zU93P|i10x{Fhdp2I!7;s)=NpL3STykD;`d*V&ZnbpxZ0ID^k&h)PlzZ-^vCfvsk>P7O-iZrWCKe4Ac*)z za`Ad*UdE)1=}uUDF4?paz*42~h^;YEX&&yxlf|Q{J2mrV3iwiT z!{yk@HPx4Et*3zbUSB9<+*eUry)0c@*J9R-3%g?nx4PoVb2isM|0RiI4!C0fnqQ;? z$NrWhuus79#_zwwpYQ&o(J=o{ZOD;B|Et}wf6GbuzjW>cVEO;gZ0;+tEIG!@RxE*S zr*Xx3Ka~-!`gVl@j#5>v{|l~qz0nZYq-)T&MLXoT@|r(dxpuj!21}LMX#%u=+_fD& zI~{JS(9)!GTUx#fb?G_;3ws)18(uHf+nwm#zS~bK($e=Y&pe1&JdV32-H+zYnj28O zvvvm>^mE%^;+th!)>4%Nxd&Q)zL%RTy z-&dOc4J+c(N5HaNQ_E(p%(1dTICH@}SY5IT*Ijla@^UjY^Sy2RAy{4k*iV71@mD-F z)>NzFCCT5hMU%CzJ*Tk9_gXBHxEGAWh^UG|+sU%-V>`Wsm^!esNBM*8KW=dUR31rI zNlZ{6=rQzJY31R>okTQ_L0>skzw&V6G$MM62q|WMMjVhu;|TC1apd-e+yfgNe;t8t z(27?V2E7t}bPs`Vuz^d&T~}hDnDCM!{6X9N1QSkj47K8sP3LK&qUW{W~{r9nx)Jw{7O-J#Cz0eH!|#j-mFZni~rz-%*5vG~ia09$gSD zSerGz`b3PJz}(0ebdQji-Pr1?qhKkCS(z{KF6P<|9hZd4D7#97WwRn4t-gg>AH`sy zOf;5_2IEi;L!}jVz$VG3E{h~?vOr@QD3nbBr*dYlE0TDULBqiumdt7aX~;y&ybhp+ zKa|m=S420A(rmv^z*D}E=L-2uX!~icmWa-j39kiq)6ee_;H;EuTtWA?)5}&ThbR~3 zK;!#d3;i4|sG;cBHC+YGP2p0595?28G$}9jV`e@#&(^?WS3W%U`pU02j?n444?(!P zp&RqwuESr`PoxVBwW4LarVEZd{&3kOSmw{tf6F3?%N6Kqi3oF9d^!=X*rDtpjkWcG z0=g(=cXgb@4Mm#u^CX)`L9fv6O*0gHqQ@DSD{G}PT;0PpOZrGaEvThOU)i`f98w{S-52etvr_(FiJ~Hanl6D!uz+{eh8Kq zfz)zl-TTUi#}0`{5`#3bP_gx*uPFb#c)0pHCVdWw#lY+A?XX5P1216MgH%q6F96X(W{yf_wj!Dst(jgR##Uw9 zM|9}fp;FFW5ve|h!BZI=OYhuqL{5{Y=V+}TvnQv(1AGdwADGBAnY)RnKG1ox!+0P~@3nt|^>&hLq~e zj9$v3AqCy?jYhRRocM=QN=(Og)z;xCp=!sY8wP8(KOyG0FXRzp9Bp*sz`aDOn1N&c zA@`hEY3fpFz7BxAz;~1AETi294q@gRrD%gM=($pi$H4fjfN&f9$HFZ5G1n!I$-qru z{?Zq8pI90FuA_!6CQGwK)123u2D#ntA1g{z#kS@Zis%Dh(0!^iMj3-P-IYK~{h>@| zY&=qRAwxYy%=i0>yth<-UENdLTB=_MX`(@n>++g4YnnB-TP!1@d?+9kVXr6`eF=?E{6#;F{ra~S$?7WciwLi6#bXjRmBz)E~@f^fS+^ z(Y1dh`rkm2_pmHgUo`$L7(7ri$20vBPXTl(SfA|ARw?E4vNefgEt{rl=CD*c#o1h# z1+sZUWS-YCnJje{gT3{>pu5;%TEwEs7r_wUffC6qE{`N1BuM@-@&GJq=wuy>Bo`7W z$O@DE-QCaZzW(GV0Lu^N6o1HT+?TWKP&ROsKji5o^ncd8g_U|jf#+-HQ+CkidCj@i zDy`sWo6ijtd&Qag|FQQTU`~~3|M2gAPLk<#m?SVshCazqE%a_jv8@HWYg-Xr73{i} zwXI!O#I_bxbg_42krA*XC@={iFf*BwWCj@8FfBRH{lD&$WE|iB_r2?WHs1G}b6sBQ z;pCM26rSI6|BBTqjwk3#fWIWrK(nrSVxn_qo+HBouP#H`M)oP{Ddv8t+7J~pwFw%D zvEq)OKDLnHZN`P7f zgcHZDjO5Ah>X=ss`2X-&X| zL$!wx_#-gxV&IPo@ri~l%hsA`T??&V^$l^JQ5B3Htbr17(pR8A0hUzbs@NpqPQato1bC5Vsg}iOc?x8hAsI?NZI=V+Ql#TG5}iX=)-hOcVZ^{sssm zz^2t|A|>zjso`MsSpqqjwYcbC=N*y+`Jz8oL>DokPUShri$i!gxP2OsSPbwR@{C!} zqU2DMJj;0XqknvSm1!6?Svy*?r(AX9O$3&S)KLunvoGKlfZeflRA9fWz|RKDV(@kO zwX-pH)OehKmZV@CUsP z7oD-twFTJr>gq&fdo^M^s^M|=wPK*!F9KFY-yG;W+@f#CcGRGDXVe1jgFFE!Wmptx z>Hc=f>yCaYP%B`1YvgkZ1xv`Rrp{bIFa++O*E&r!M;&Se`d$;SyrC7)sBWe&Mt~ zB5tk%soh?`vv0UGdKL(`Lz}apjJKiqU}ANr&?~`u-ha(f4hL&jfb=kj9sOMcT-&53 zmMb*lJ`K)i(xW7gIlQj6FAV(&5wK04hIN#Cb%DEc`V!zk6& zs1DW*HzC)K#a{mpF0ZnEMKJmr0n-h`IJ3{-Uay6N(G4JEc>PH_ZAEG1b%2;ibk{$0 zX@tHiwIa~VLT8n2C0kUJ?yaB#1e{!uij%4+jXVI*5sHwPNa#ZZRE0}x#(~rl5aNmx z2Nnbhwg^p|VSXvbN1D~;zCiv<wC3h!?P$$iL99Yd=F!^31epL3Ju7LfBQbL- zcK^`%55*oJ=0gP(_yWm@EuoGp&^sFZs?YD5xiuK^7=VE9Zmjw2G$7jgZQTHSX5v+7 z>IcmyrfHedVP)bc0Q!5$Q(xZEY-@@d4&%YYPM>gbDs#8ZVf&}Pf4{_bkIh-w)TL*h z%I%!9Z^p$_Ey_v7hT5^DbxEA#XJ?STOasdW1R6?-zW(xrHfQu3(&s(dR^qS>HdYa6 z8;Gt zZZ+vtA728E?M&P`o{=%p-rDkj0*w$&yT|XGyk$#$UPfzckU@40H9XyY4T~WYsp$|v z?ROp;4n?nqhS18zx4>*~s1^=J|4E9*u@*Ln>(;h+Dj0o*!6PJA&KpRYXC=P@wX2zR zA78O6zcLhigBjcG(09a%J^|rYZ;|Wy%3$nmCZ5coE7Q*Lsqe>oN})+Wup(nZ+~hq< zX^}4sVwi`DL0R%0ta+J~IsuF`eoRr0JWNCvYUcBO0ryLiV{MSRL>%6Uu)`B{POTqN zpR>26NufZhO zc^e1^1JuRicb?X+*0)M*A48uBroK!#iNJ%%Fs6D3=UW~DDX_gvo91j~7&n2`t<2_V z1*4Fmvx#U)dq?@oP_zz=t)75ujL^1;CF>fQnv;fO$@>wz7OZEm9><98m~_Du=a)-$x1rhlaX03ZNKL_t(!V^5rS#iIU7CB8r8)Cp4^S;m{fcF{`R z>hn9F{w3bYj(v1g;Mb_Y&j!mockRTVubzdA&!3Gk2e(a%{@ioz0LwqOg1^)6JHYbq zQ}|yBEN`#hhS%PE1v%L{m^gY0dblN!7Mixu zz<2D~W_6G4hTYOgi%WL;_t}}b@cWV}KyqBkQuaFgBeK2H>J*FKS~}XBs)Ck1mE(MS{@E&l*l&AQ%l1DEUhsU~0h z8(5w{_vN>k@ih@2e&jh*a!8SMK~H4jv62I8fTo(v(xLn(0YcM0fPfF$cV3c%)yQiE zI@W+XFm0%nGSra)-*yHr_6A(9rQDl8QLr3ci^*Y3=Y(n^PJ97E7ceeJ4|dB+tA`n6 ztOOy)P&g!^kwuJm0b1q_I1d$SG%I@v5ebFw6s7QJL6Pgx)If2O296?NcA7exez+<^ zk%bC$2!rkvN@G=UcQ=j3@0qFESL|%dx3va`rP03$l~x1SiPkq9jI02ufYv3IuGD%MRZp|YM_`Jm|g;~lSHRjl>bJ` z6z3xlJBW;9Jw^FT7A;z2j4qz=rQ~1(cvuXMD%MqxA;Tykr5EtH3J(ogl)iX>%euFU!zLqZCACEq43(0Ry`4v5do*5=?v?NFC$#=P$Li z=$RA*VTU*1YMWUK2Wu}PnAbBCx0>d7p`=!Cs2yXPdIc+dswM%lDXl%Xp_^|t6_V^wpkb6Kl24^!ba;7G0n1RULIF!xk`eZ9L@Zw zvZ*UQgPa!F2nK_>In-@6G5f@9Dcva+s;{}*_+D$>1IjkeThw!PMQQ9)A{N*5Gv(c} z?p3h%PYQZIpl*>UxAe7_e_P%~*)x|A(Pj`Is!g6mN*znY=ZYhrhB4X7{tiV}L1Tyk zV^o{*T_|>gX1+jxVyCRkDSMk*?}3tu4|12#JrtTp;xOG@ZFBRQop#zKED_mwfT`vBkBX34hmd-0vIeyh$c&>_q?4 z8f!`1@_|!Qxy7$Bvs2u?WkMm{X|E`)IbU+pL2wZ%>m3XSYtJC?bIkf)Z?W5=VTnsM zYxGfy+)Q{=^x(tK(t`IL4y*wo+Z%9oPx~2w z^*cn$5CGoZs4QGsdlq0m4})%DO;up?RwC7vpilFpWFA|BRs9{XeJt7N1e^nAk#}?< zq9GD5#;kXH18&QEAo(4u*+`@uG67f_tee0vKLlYNGnM!PPD^_%8P1AG?vqn9O)d;L zze?uN^D>C%L%`Xl)v5?Z>lCBe6L48ntw*kWeuPaq)=WQlvP?8~hROGMha@A{I+l(K z?5`F0*-eRlcE5=#DinW$OR+_3T&eP(0y|1a^N9I=*(JhvC zS-QHit#o8X51o_M2?Gm$Fawjz6QC^MHsBj(>73>GV(2B^?7^NP$jr#HK)0NeEMgFr z)DAO3qCg%gfFC?Za=r1cA| zsDzmwB1LwF=}{)KpvVt`WZs_G{yJ09A+wnIWM8q{%ILDPf7zlC?jYg?X-cN$G|Oy? zqqQbI(dX~ICcHkn90v6xtsn3dyG2I`0e1%JPnoG3!@N`xs#w#A8TiN>aEazmCK*ly<7s86JX7oGl3QFJjEz#@4~WnJ;O?|0H>p2XW5HbJEp|;4 zhiI~?r$7RN)w!hk5Gc-KrsW#+hyqWw+jxq5kb8SL7+Xq!TOykN>qJLoFj}b@agH}1 zAaO7i!RY-8D6+a~pu40~UtV6GsTwmw%$%8Nsy2!OJ=@*rhl4TD>}e%Cpc)*4jh^*b-G-dOhsQ=2bfJd`F|#0DLtAultIB$mmYd*_ROEHILtQN;^M* z3NVk?Hl9{E$h|Rn?l_Ubc81NGP>}tKL0ZD(e<`AE1l%AKZX!`(khp~Z)0fJa4wu$k z&7_V8BSHpMdyA48#ljRJ!bu?YVcH?x@<`BnF_HR64Z-&MI`nep!`-6~vNLilfLXJ>3Z1e#p`oS0>Ie%BS^%-cJPExy z?M5A~ueLhAjm?ed)y0WUxm_$!E#_HGEse0*>=sZJbNuhCHdr0wWan5x8HV+50p9|Y z)1BVdc+w?X)`@Xgc4m&XFFCfvIY~!4t#cOYw}5K_?`4tAmeyL1AwYds!%hqAropoS zchOnP^@vtkmZU@XTH&{1X#WG+T&_#YzJ+Gpc23`{`wIVBz8FUwex%i*7ANqZy6kUY z`LQcsdRi;m?NG+33-38)yu_|3gFax;?W9mgnkC=(9nEwEhlMMs^#ILKMCHtSk2m19Xs4B-*aI3ojWlw}W}6`S!r@TOq0s6B0HSNI z@)o-nRW}$C~ML+2axGmT4ltJ3r0IiVR*x1@wPK%98G_YC` zE+CVSB4sPn&@%9Sof#*Cd5UNkD}%9rF!TgKece8DHg)~$0rbvwWca#U9p%_%fIeFT zdljf8O|z{GM#DtZmF+abGa!Gf{I+&|ZGN*dR{<)Ep)KuUI2iU5Io@YR0Zbkx##5ES z$kXDw4MI@dev|j9W`u(AK0|i}scXG{r`2Vu3D%8Bh)$YltH+_k zWn6#mUr%V$!v56v?w8tb{@ewC#neaGC5w7?6U5{05h&`b-P6qK^=`Lk?ux>aZ@s$C3 zyPzFSIQ3qMY1|H>;1lC!18S|Qv>4YZt$Bt_z(|)?GtVZ43(_6gRK(Fg08W^!r`TXP z7!c?5zcSP9iZG#%-zob+Ww3TGNM8oc^_gTFpN{UT2u5cs&=m~!fz?EBagqWoaaoFR zHkgiQXp7eD4Y<#!D2>h|LXWg|njmh=EKHR)@KzR#OfaB62J3G@^kIfU&$nvGHEJ+c zcmhsKPi$p$1Ail>R(t(UD_>cb>J8NcO>KyISg9xA9F^MkBL?;oLE^NgiO#rcL+y#o zJYO>$Hl|5!jF?T^d&5k8uE9-UdR~BC&Bkqm5@uozC7quw`hQh0cA5s?O@L&lck%{Y zmK(a%ga85IV#sH$OX`7*4~f)$V11wl$lK(+s4^?IR3_XQe3?+UD?`!80P4z6-+|4W z6)-vNCSJC_Hs7Z77L)borfJTx+Z@XQa|b}rw4W5LD2;xs6n(*38%&akE_u`XB+c}e zp)g}toF=E;)RPl{I8NJmxJ(|%y9%O%H1k$pz-{>vRBebZ(o6>lD+|nbdW+pp)dZvG znnVvWu+og1bJJS(yVPtlav0^Z#+_aKrvJZ{z;6?+&p1``n9P3M|Xn|83r1(YuCo30X7%2N_jyGCV>dcHx3jo{g zb_?iA!?JfT54P{ALys=K(6ehV>%W`g`_Q{*0Sek>M~fcXVb8>1?+?!1(tS?ogq1Tb zdS`3kwJmIEr6mVi;;dxyVDa*I5ex+_gRh?$i`Vvm>~COs{+xvmX>bNH`W|=Ml;h)B zR075uiL8vrjpmlM252QSk7jTdw{s}A2GFIkQ<<=UfG5%e*wkRr(pCa6()!hKF!~o} zK9q=Z0D4x8ZYqP(Pc+y^pjBxQWeseD(N{q@fEjPvl-A|s87_@H38fBU&0qP7T{GLim0}X6q5lr>_Por@gSz$Ww!1P^`zW)%7^IHy z`t#cyy;J(-uZZ!Tr`R>BqBJ^-zz>6LH+ciS?k-;+y_ghVt{{%X=;sT#jEgJiPwa;NK$H50pa(11Zt?_ri4ob&U~BMN z*=D8se5n@ zF|J|I6W)OHy2{e1(6?t3j3){Ly)2q<<;F@kO#3DVzV-%OZItNLGq+F-+#ua~==U?4 z@NjFExI}Y4h_3MZT?@)eBNrG%V(NBpI-ffgMYR;5ghYqgfN&gC!i2|3^PfqnIg-U~ z-TzSRJkoq8DI!J2__U#xy#Jc`5d!T4^DSw!KVc?_bb&PHvcf?G%JBG|Gb=-}8v#DF zpvY-y+N(mbS|+aa`rU1=kg3VK_n1&cj0!WM?>6k}Z7^yXT<;6Gk54^&ClTkNJzh_nMANT2B?c7~6f{+aVy|$*%z{BOX?3!9i1WRz-&GH^*^SlWI!}f% zT`1P=o@@DjiPT}482yP%!$e~UjJ>|%WKN+K&Ba(IZt$8h(&&_!D#sF?J~Y3fh`#m& zTox-rT(Zf=A86($6p_rVPLlf=4n<{BslNi=5w?Z(jg%~NK%S2k40X3JkZ;k-#Xu~F zyH$a>rgL`I)IE*OE5wHqz^Coa2rc;pVVkfr2>2tD?PYM{7$V!t%;xn9#-xHkuPxSh zr8M@SA}z6fC;I&EH&ZU`cLBVmtZw5}=@bj_2UwB_nuutjXq@HQQOs|HpfvMYX*Q8e zAQGtyz}V8BieI+2`mD&-+UM4NzKLhuciK-k`1noV?2dzVRN()vz%K@tBN5B2^#65n zyZhk!IX9tmr=%0^zgd2@&hokEO1xdi@@rM#n^miD$$5Xq=!3@oq(dBk`gBa5I^`#A z+p*DqUxAImO_={cn;Gf<=WXHy!17~rUY6*a-XPAOe(n^L zfsLZCs|-b-*G#@hI$`_x7#}+SCmFyCZg^D2khW6?tr^e zD3vDbs{o@+L6;^6hOx^)d<|=^+ebS`1pR{iI-d>Ek4??J6;agVcm3$&B9SlvZeySaLyr}mXnC;uPl}C4*rsmq z4bHbZ;<9X68|!7Waj6*4s7{GMVz)8TQ0ha5e%kAIUszsRb1IQ~L=io~%*VlOo#-9v z-5CzXt^@RGVCrSqj7!7_Oh68s_GL^E2V+0CG*3zo>MKLBPna=K5kDv$d5Oni#=ETX zHYpmhkBo7FK(B`K4be%;B2ieOZ_f!zQaZV=re+UIa-^j!_~R3!UU z=N0(#->eA5o+9QGWboVSa7`RAfZ7-Vm7!RP)+{DXL&dnOB3L(qV2bPSTSSoD*u$hA zvR@DmW#-ekIv~qpq zFc|7B&D@~CfAIv|k6Zhb7%`>(A*M@lhht>Fe%*Gbp0hJF=!LCo!_RD5Q+AlveACf? zoO+XHt}Yznp7bBRe80fP^R9lSh)n%jS7%q{_H^Gj>6C*HluTr1eZ*J6PbVJ~7{5Ug z+0y23Rt-1Ajt1*RTH~9-;v}s+S;NTb3hHq%FOfK;%F@UctkliK{6^YPOSH&_BXhF` zSNQ_&k?k*`LR{68;7dMRwL-5KinG=jYbZP*&sEAdU>Sp6_63}$%kO1UB0;=@fGj3H zp0k=irpC+?0twCeXj1fcx}#mOK5_yn636>&r7*3a$R#xMw43u{%~+)fOVc1c`F>)q zW}ZhxV+hzL6JbgHuW>b#F-d6M6~Sn!0`fcom-k246pX&72nAZB-q3WJHu!CrdOZ_* z8eQDeEa&Bd%0#PehTU#1 z(%`4XBca`OZ5BGUFW@e$D2;qbKo1a!ySdf5PM&-032{x=tlv^(JV?xsG2uJ~zEOh~ z5b

-z^{;RiWtBn)xnM(=~mExbBMt>piV#-9pe24clm;qgRHamoQ@vL)Q|NOlV$a zJ%Z7sTD74jaPj@8Wu@MU zhtrhxU*NUu*hNPLew7ORY_R;RoE7k^faO=;6qZTH@@rRMNMPts2Fp)9XN`|KmS3m> z7o78_pS36H^nUT}a%|tW!*aS7DzjlI3(W3rs7H2IE;{9OwmQt>d@qz{n_-|w*L;gQ zE!#)yqR=Xi9-Vt4FQ!wY`{U z$u^3}>NHo^g)KMkL_GfA#?J{Lmh);#nS1$t!z}7|b89m^`95Shve6oEwE%QOV*}Q2 zUX7leoe=MZeT@y+xqGLT`CPwihm{}Q+}vb!u_X>KJ3AXay7oeLW;SwiaxFkDKv|CG z?Bzo7prosZn7u08U`jyrnfGHrz&WxHjbV)x-z zWE0VQV0|N#y52C<1c`krFO6KTNL|RR1@JHM6}umhfo)|lT4ETs>V!`8A<&;B3)#BP zQ1p2)9m>$p`U377l7NPUW;&0+v&m57to^R78fDmRl7*WqAm4Hl4@T}}rZGxUH?8&a zax4K&hWtNcD46c?7CWB`2dk$M84^JhbvS5b+MPV9-Hg8{R7+x-Dfy-;gR_-j{)P#o z<1I9Pz)07&s!()+1|7?quS>^XB?r3Ev&G?@nZES~oQKt{uPHDUy~|9Eq^Lq%d#ggx zqgb@BL^2C@o*buQedKsj^ait*yzcu7i``aUY%)h!3+=u|lN#2h8x2MeX3%FK{pvf< zeDapkeDA;m`-TP%E9|GCS4ocaPkrxxsqN;^dHKwY?2J3|y60xqN4C#B{H#fKg1SZ} z5|g#V_NsL1;~5!V(P>*?At`H+Jm8%+8;=nEcyfJok}^<2gl|25*8##FAmHx|y{s_M z+sd;3<{P&6$g16QGdlCZ==`m zvL+T%;O#RocO$6tJ;lyf!j_MKE+GcGa?+G}MuMF1A0BmzLB`z0_g7RQwu>y^;H?hlo z;13=F6~X9z3YZ|km>HKSqNg7@fF#+39VVr4K=shJ!JlZHy{TSaJ_i`e`?LoYH2 zpGj7QWGT5-`G{=)hK!hJg4*1I8p1<{bd9^9GXUVae}BWEB{F6HQHhZP_{Z zfq5htEuLoQBw>W4zA1MDGypmuP!Jj9>7h03V{aF*0;nJF8nAD4k zNDS&0`-X@~^`=RI+w8WyQ?5QLzhYyokW3w7VO%n{l2T85 zit;VfEm>)jOROORnmx7PuP?$dpFd9eB%gN)hCT+K{PX_~b) zKqrV(svSrb1fl*rTob`=yxD8?}o zF(o<3lF7%^$u?QRypfrYF?HgAzC#ME+-zRNMrELOE5LnBU~J!j>uc-UzOU^| z3H^;`v@nbjq!VLELGH`f5~J2u>tIaivU#lz`+zj~mOrZsMjp~olax~bG_`qLd~d^H zj=WAY7wYCiWhZFImafl5(CFfLKTxm$1TLHS2#UQdgNKPwqR^ zy+Z1SRxQUC4bP(OQS2HXegO9 zQo76{vuJKIKM*O2NmyV9tu&dgRirN#Rt$gCXHyrvzy z?5MymQi1;%EVs6{BDi6_)phw#7U|9eiiZ5RU7>dS`^7uU|H-*_Y}-+RjtX>C;HOkT zy599WcUb=n>^JzQ{G@H0{S7QXHfQ14L^wy>aA(~&^Qel_$frc=V1~I`+*WZc0`5v#FEnQcZe)#(N@1fX;F=?5G=`FHA+LZ^M z7V-KO-hlhX)q2LX4QoOQUBi!rcs7s))0H7<|Sa0eBpU`YF)4>4CPD zIZapq7DFTV31z{W83xo_1UeqXS2FN^n*JIN#$I4x1_)N}tN>*hoHMkz$$sVwxUQ}Y zMQ1UKzL6sYTIUJ4=2lfzW$E3vQV^V+&=Y*adap@dU(F$`)k{RE1+dy1a0xZtGF<~m zGT#nn*0Y6boiypeSzvm{TkO0rT_a|;heI*ZLb};Br?ygJSB7%D1d?Y0=NMXzR?X(- zcovC^c_WCw_XS*ktXN-j9z&h46!I+B@<7tyCzT~Dx=|9qFJ@%2KDr_5i)#pTK_);B z7TWyNH@g0P0mDJ=@I6oKRTUbyq63>;=GbS^gw#yeE$6v@dg+ zQnZ1YPf3`GG5v-XthXktz!!nkHbs&ztpjCsn~p5(F=zlO`k292iFs9cW9%|A@qnSM zy=7D#!PYIBAi*uTyCisU4+IPD?k>TC1(zU!AR)K~cXzkox`VsByW1_}_m$JTs#{zN<5Dv(Q$vSi^}nmS zx}*PYDt`UI8qGT-UR1`kL9^NlM*6up-4BEF(UC(rkzy%k>S6%KS9W%aWnI9oeRc_D zPClRvU%})|G?Afj0j~u`D{Rm{x@(ZSjd)Zd)?eECf_7I0E=BDj(UY&;DXGKhS3*E{ zZI^pZ_1V2aGd;w8=4*0pH!{hqtr4=lLSIC@TEbZw2J@0*&^%>f0cKLx#={%zr5bW7 zR7=Q|Ara0VF*iuhuuw|N^mG#}{wxj!Q5?ceT!Af~C_?1-&p0xea)f`-ZIrg*3C7fi z{ikNv9?LE`ui+6SW05$QvRGGt-5ff$Lrq0&I$Xw9pqybzL3l@J)Y_A|3$2W88c7fD zR2sh{h!-IrLhf|jfY{Rw>!Dx1cl;(ji`~(n-7E_4#t9ouT&Kcf4QI*y z9bwyF;^6)Kb{~}&w#jphjrTF@SM@$S^C0wm+`v4XDbIo;#@@+qw^Q%~gaS#sKRX?& zPtu$w@mktI@g+wc8+$kNE@s#1b3oys#X~5*UNno~B6ShGc%;g&5!T0O*`r-XIq`u2 zYD74lo^(gi{e;i89N}hbm~R??XVP|~56qQTn-N9*zAQ1xI{($4COp%b)KqK6E~6lu z2yGK&lSpwFnixxKlS}<7b;reslUkbH=Iv$YM>bx0>p#dMGr0EJxmv3z6_J`wjWlSu zskVf#5T=%fSjwhl)RRgyVxol8SRH&gFK^;-?gZ(j8DUr5LiApLlBq5ItoW7`u3-BW zNm*4AEfx+Ar+N^Dy6?m+c#GV3+jH0yhz7KcgOAxI2Yt=JK7`a-akK-8-9ClFIg@U8 z9=S$jw1}!S=;O89!^nIIk^a0#Gf~*owHnP)<~kz6zwHZ&9#_(}YFF*CL?Hst8!HZ}9PF76~~F4=HzQ1bNqt|V*G);^`QXE}S1Av;DRbU?V)N=a1QfR)X}B75%gY4#827k1&b z9I*K{A>PzQPwH>zuOF2z%f6uFM+{py*(LF+UZgUO5t`?t&Kw&;+^#alCUwM@g1T*< z19ba)ld+*Pvp1dSh)-;Xlz%9aDFoIK?)>3P<~Hm;kIGQ+rgA_`Peh{df6%;)9F`k8 zeQ~VfX;q+1Aj0rTx%#Qq=(>M%-&#&lN&xZ1oUCSg-&G5LyTDFKWu%SlurM7wC(ZUPl{fFG_SVXAjQ|e2?14Vj<4H!Z5C(U1?r&*PHGwrR{c2KsY~W z?Yv_s_x;NDGrrR!bH2`(Js&&ikZc30nIOHNr0raCu+H~J|BXsaVi~8Q%pG~-`?GRR z%eagQw_HxP&#TNz#%4cKI+K2V_WpjOh$5)$M`9FCZL%U_!HZxJa&2tqFrS?u<4cUD zP|DcpZGZf6dRO#1B{gP|1BBOK-9w!M89Y>$zIddpp1UTCR2rHCuR&3+~$?!9A*evb`M z&#W3RyVW3dRqG49@Bbc zR!*AyT}-tlMK>MX7NUdcc<7O$R+?z26^C*0Yh(Y}iTcP7Y;l%r?~N)Cv+gLn;2DU* zp8Wjsx*ZZEU(H0a@kN&wbHgPKIpK;@Sa)S4Vx^K@>|UHx&`29xGNeBr?F1#SU232z zJ>s+9@2{Mxi{r}wvWSm0a57ELLw-;Hsiv28KTyV#mQ~k^ZXV_}^=3w&!<%)>Ht&t5{C#r2)%QGXg3Jf@qZ_i` zI#T^e(ToCLP&Llp7GH6G9_=O*(Gt2YOgJX|9ZbKlEJ{A$ai0FPzb`wij?Y=6YJH9XCG<)y_`@l$ ze!G`3H(t}v*%ZlasxwP0Hk#)@ryT-UeoKHji9Bhos5(V;sJFb2%PIhNgSjie(2&zlAS>}ZT9ZNW=EeV>|2R5 zS}{syOpc)&KCCCnAtd-c9r<=k0p``QJ|m?VYD{HF>)l4SwrxZASCtq#I*OTuRW(uS zwNN5daXVPDXhG=LTCaN2Mb&zuyzX6W@{zj({xXgl*JWSKt`R+I&XG3~`|5rka^tm} z;nMmZ$(7DDwK8nPp5XojOXLE*q9hp>Z6|Z4W8D+=A)uvl0MkAk<1=(Mtct$*Rx|1M zy`A`>6C3NVI7A6!w=`w+S2PKE?=$K7UvIO0K*&LM58)^SE{RdVhFishMgxgZq)^jGcYzXmd2iv_yae}_a|-<`*6eML==JyxnqL-%g${u-py}+(t9x#&*Hcf!ktgyh>H#h|EX233))|LBExQ{WH}S`0t<_L zq@FVhqmv{iZ{_!r$2xP~#7JX*qhiyg+){j(en3SfNKP*zs+)tkHS^cdtF;(j{Fr=~ zNtmwgfjM#+^xU~HSrEfO7kKak^W4McmRv;h!rRw=zTV*~}s^2~qb!4LOkM6rhpvCt@D7;w; zitEE}L^C0x{4OuO{<%lWEC%{PZf=7H7P;=Xhz|a7MNGJmp?!idssyGyc62r<;2uS-YPY%q1G~Nc zlp(5Jav6Fk`DISL^KM--zQ-Dhloq#grsJG5d5THI#ZWBeT1g^rl<;p%D8a#_3H|AI zhw_==(jcVZmh?@Shvshg&_ks!t^a$eB<_%PZv8<~my*lN(9fAaxBR=}^cK@{D``3; z4z!IfxMaN)8avwzKRI82vB7V24*oX^&6F{X^hub8GeYZXFP@p}(2{64$l;Xw`p+EB z`MGvm_xUzMGgAA}!U&JV1+f}SX?mOVt90x@Drr^FOL`MPK?wHrd>Y%Q_aEiya-zh4 zsP=br6(KAN`8UiydDHso4BY6oU~QH{si^eFD1IYZTbh9G*xHJRhn8<$*MjLBr%ApP zS)BUo{udP9E?j#iY4L{B!MU#Cz)#UCuA|la*_u;OqvP>qk#cH?)LnbW4=U{f>ybI!V~#2m+o8~&}tzj5|kn9lS>nT6SH*Pm#9{Pgnu zRD^8uDx4(y_yj znR!DVh|n#|a!cQ50%aapG-#k3y1r7FOXA!if-acWEY0N;6Ks(t**RqB0XE73Dxur0 z6^_-6bM}k!2kjvellAS_OCQr$k=+%KYD~nFbIZ9_UhN}MU;1L{=KaX6n)(~(7TF>2 zioi~}=P*~U88J7yrF+Cx8L|%EkaG|w!&{saqHa}ErWo4e$MTsC`&Ed~4V0Iyg@^a| z(XmK99egq}_YPg!pBq^(B=Y>2{=(*e{6!1Hd<@f(btcS zlQENe`Yjx_qh_y?`n7^&-@E5p+MNe4m%Z~*{fWScj7-RNQu~d+bv9}juf}zLX?jpZ zmHrO(k~nsr$MGwLp}5oEFLULEYXu!}cXIuGBX~I+T3jWw?%JOE(n!eAPovPKYKV)N z!y$|WEbu?fO?X0GClB4|n+vws@C9*6`FC>f<`z?3ZI+#-*Z*tm{By<~!bvTi(1_oWE{uba3v5<7v1$Xs;JWi+WR6TV48W+ub7(qm=Ry z3QnHbJ$`W&uR=h{r}NsHUM*bE^9D&ZIg^VHmI?2`yIHXEdTV%})5mnMuN)0;%Y&`V z+V(stXJ1}Ep@h*-@q5JZVUnw1bIxVcu^>~+XkWQGa+=PCE1T^UK5CZF;nduGXR;L+ zOb;@zV426rLC~THE{xD}<;Nv_Rt0(zwv(7KRnuo8LE9CgU4v5X*3;pXKW-k6l!&BO zmf`Vp#8w}72G0fszUz*kH=2+ZVL2Bp5hQEt)*gD4$X34YJB&%?+gW*yG!&p&>mqjY z{&~l*3bTZFj3Dy(U{2t8D17-#Yo@gK^wQB^z3UqGnz){1?KZoGd9A4i^E{;XF9%B% z_a!GmN%GizX8wnd!8jwevoTysnoSnR5neT+@3=a6>(tY1d&~P1W$%;rldftfcs?Aq zWaJZ8bFMj5eNH|gGy8%AdF zA)>YW&!A+%P@!y;2f-D4H++gc?pEZ%Y~DQ&Bjf4H76Rqm`EToYZ zdnNhnQnh2olHdXBhkhl+YFCX;0du-WRVH-KcpA*cSi{uMM3!u@ovFGsa~D+&jh0i8 z+W+4~Fb#1JO30%7qWVFrF8j=@5`(SC7^Nlw@9hFLbCL-%On7kTw{@2`ByjN*k!>q4 zb;~pp4N?Awy05>t+V>^6Q6rY)lQ+L}^$R@cZf9J-Yv}BX{r&K(os`9pG~tM>B==Va zoMjIi8UHIBHb&d4Hw(Dxc1xqZUu<+mKEi%?@h!P`tg6J9NJQ{TRmz_xPzb%VjwoH$H}qMGHW_=1h%L^MS80EP2%<2p@03T|rO ze9>nvat-2Lc3r<-PmF!@4*N#`bsY9+r_iPmU+r%GM+sQo_vQMkzI3ent%+BUt0`L$ z-G4|Xky|b!oR|oXy5mb*1QbRV?UlMv|8mUpGWhC_HR<;^AZLwcms|%|I-LrdW(x~7 zj`x4kiy_xn+%!uQT3N`H9v&v!q0P3l(3R(vaG#G3t~4HdL$F4T zft0)MZS8%KP3B#yEX(n{9`Q;}NK9G!(_1+u>#TYyz4;vb`{s-3{@wxmpO{L!vI1N! za%Wf9L>QD7%G!OlxOy8iD3xuJmZq>J?dAH$oQhe*yOF;BA;^%!1*sD}R=mIvPdPa1aa}7=1RsO4<8{R#aox<+@agKy za?^Vfd3GnrF+b;@e@)4R+E&`h>(0MME1hAx7E^SyJ8kCi=fP&N7Bf7~U`DPw$Eot3 zmf_`Ogeq!!E=QJRtv3VW>#bX_CP1g+;Lb#xGGVX#co^-*J5Z z`TfOpB399Y8T-M)wKIR|bcM5*$?;^<{^~%k`@!7OiRyUkmb=jhBF$P7H(f`xA9XYY z*9S9If@Ri6=oAB%Uo4p9t7!=E$5Mj_h8(cULqr`Ne_Ax5n9((L#=-ehjuI@J&wSBL zdPehDe$6Bn<)W)9;Ao!Pv`w?tV$zQjgcbTm0#jI3EPAbF-no7o z^6K?uXve!$RAF=^ismDV)d}=}49)!7%laUwZ$=TtfRU0-m$g88qKcuDt@ODmMX3<} zuZ@^u;q8Oq@ABWh?R-&ph38l#hcdmmMJvSgic6RRMJG$CG58W6hx;i*gfJMob& zD-=KEu+!~(VH>zLUqYCBb;gCAMEXyA&01s4kb-e;dAe!WRmNh|aSlP>wJF^!D`f?) z&2RXx1MwLXNV^$#1)_yiw$u?*J+4B2}b;tMXw^hq_pA{(lGs3K_%<>bB&qEV1mh-kP zYz_mwloS)%4otcj$3Orni=^tk-v8kZ6=cDeC*c|}UWS|asJft4AL^}lq2lC4@#lS! z7JXi@rmfA{EmrvZVwdcR8wEotaka}KIhPNrZzrI(V@Qf_(9`%LjNz)o_{B~wqm-k< zg&7mlq~=D4bXs(W!j6C9pBe{bIw(>XN!={eV@n7Nzx@YQ<2JxLDwM4x7r0!uAv!pm zDilT(1FjjfyZ0|ETLW#Z%vCw_&U{h~0V0(DAB z%{xBLiV?XPnhb8V?y*sdBCC2;vCmGJ-ticJFTa!sQ^fJd%FLM_@i+*C`A)93q0l)q z=4Wb^q%egkxvwb{G;OHXJfuVnZI2iKXrNazJbV^bKo>zOnshv`^uXWT3aZ2$l?^z{ zjj2kluV{Xw+T_TjM2nhohu)@AA}2>Hi^R#mqj&m8(U3$fe+|Nyr)5WPWW&g#=;xd! zaOyunEke{kG`c4-P@IKzr78u@Gsy;FRK|o}|6h9n1oU1P<@y`xs<4PDtSWoTpqN{i z3`rjNrdG?Mvk^rn7S%;2hI}s&9aw}b@|C`!Q~qHeQgZJjAVF&uxcQqxIG~@7BY=zs z`!Did2E*VFA9W7qrW*9j;)8G9z4#r-abdJo3O30KP`^#MQ}Ddom?@O_t&|Be5PwpA zQDU{~LD3l7cC9hwDH^tR^~P6Vi+SXWAd;d!d@EA_3BKmjszG=dvQKVmrS!1m7c}e% z9GMySI4udt=v)e~PHm6e+$eAOxg8^Ocrz{|Yb&KccK`xm@7;s9&zKy+4+TmBMmmSW z>4LG`4UOBlITULb1S>zYCeDM@E^OPnqX{R9)ojoCye5*Yu-RcgcA{Sx7t@dKhf}!N z1Fa5g5h9QH9crnB;R{c*uH&%Vg6JYlSYq6@XkByL5P17w$Bz27^V`Z1OU3&mi^S8- z(MQ_pEMj==n>`LlQ^rF~19hF5S=m|iXykx=tWU4as&ULa)qS;HwXfwHnA>m}ANxxS zYgQvHzK9wfXReXxeHIV+%X7*qg(X}NX}_1{jWaZaSc9YUuRfMHPKGK=CZppc9iOkzw5>IYX2W{!a%cV}czv^XLIk z%7IE&?_THfIoxNbcj1W6Am>nYlyLT5n$cP)X1y*xafMQ%FV{ zEw}QHc;1K_D?9bX4)t08o zvOgW%g(F><adc)=Q|)i|ELC!s zKTbI91A8?3TeO~hpzWMD`px`S!gCER=kkwr$noe^ z&N`fwIq8h^4bq-B^Cn5~=R6Noh?%W&$4SFIVFZeIc)BbgltN|RKpGO4~UEXv0s!EEb)2>kW%hJ%lE$#FsV z4)#ERo=b-kUpnFR+Zar&D`|@di7@_Kl0`Z$nr%(Ss3ek%HQ=kL@1xxkFFpb;wcWdx%J{=ZVW~JPgI@R1T1C z;kc~Sdv%SkwmGqEi*zJ+eF|oudcAu}@P_i4TWoDMkvhht@_*>Lk;a`-VCe7jE)fj2 zZT?kcox^!To=Ne!^QxoxfNPAbb)FfwRH@vy$)$jO_izr8f4peY8{v*4xOXTy^ch99 zT?z9XZP%u~*RPAGJcbt*G7G!eukG{T_Db=U{PcXRWb08&^d(xajMpnn8}zx=XE@WI zr~I9jEzJhX{(TI0)S4O@DIl=DzMa=^lMXmhPJ@&wY<0HTkU!ic(}kwxJ~IXg<-f#@*tKpicaMsBKe?DETy&37i=9bx<4aI~i zMBb;C?ky6a4^h;9D~FZ!cOlYC1YO9qYy0}X@UP}c=c&E=FJvbEylylowSvDIsM`zZ zF;oSwD)MUmy?;2mrtS>*z>sr}shx~`Kfsjw;_v+`I~85qK1zaakv_k?>g&Z^)YpAJ zijzcF{t*Zn9lZ7+^5#~ma@KN13vImEKzL_pX0|=U$j{cm^#@TMGCJMJZS)Z(M}nFH z@UCozI~w<7FFv%E>E=(Jt7>8gFMl*a8M)LHC%K^%UeBL)yV^1}&qExtkJHhN`Hud~ zEf0^zCyTB)NqqdhIsS;yzaI^Z0u@vn8w~0ffv;|hYPWXJBTU+QM{OTpiT&NHEN%b87kd66G;D{X$Nq>zV6` zJJs>}>#({&*XM&DLZ&cYwUA?^@)(GrU|QNc!Qfm^{+t`nm|JdVPjo0UsQPD`AzGq6 zdH^1-a(r^C8E^Rs#;{JGU^~-q%Fv z2;TN6D=C+B7{SdO$iG#o7cw)c8Xu<(Ug3@j5leJO|7Lh5*w|prgNFLekcKKRtoI!d z%75Jv71Z0;A??Rdem3)RJB-?DuIJgCG{85d%)(1ndYTF8ySai7yb#d;>+}ENBSQ3F zCbwL^G;I|Bv048)(Es~$=A18Qn}w(^rT(AaemU5~8^CZ+DCzxWFR}kW&(fY4?n^aj zesvz}O8b95MceDwpW}R>Ezaow&u_mB#ommPtp6h z`2R~7%-?FZhMdn>lNA4ZX#J|-UdJ>13`J4LkXq&r zx4l1RKY!568@=q4{{Q*{JeVd(W-z0j>i>?S|FP)A04mSCDI1Fa`^o+bJ$oE-FpruS z<(nl^U>={Tp_t!_*v8@g&!o0%2Dh_|^d(dsm2RR)zvS@`0Gu}XDE=4Ja!aG%gra}= zfqr(W`L*qglls3<^Zf-S(&rZNtK%*5fAv)vpfS!yP@C65`r2Y>s{i@LXL`3=; z0GPed^p+|_SEk9pXv%~?lTf}S3JN?~CciKG+3>7=)fvhE9KBua<8RYhbO38VpOBYh zIsErnq#$WDYhnI#M*A!BSNG1^B7MKAj;A(>|1R`GAf!tME;Xh|-m&bS+D8he3^THd0 zasQpG5kYVs!TUcA&Bh_^W#S-x^ig8@6<)^Mi2=sTDblya_8Q!*&5M;Y+s+HTCi&|5}~F&s816{_7cK= z?>K(ey*AdIx-2YgeK_XUr+RA{mza3Fet+@oA=kbBV=AaTq!$Wik{8kI)eLD*U^Q0bj!^DqAHE9BM9z+Bz1@hGS4(3t{F|0F zN|$L!E%L)hL28+iiAfIJ=xR`@FAMJw#(HI{s5q+qsBLH>FMd)m(%(}oLGpd8ns_$i@{MK`tMt76D*SFU$nfFwcy-&#^69q+qF#rkoR|nz zRp)M>y3>SQ&5!TAf|Ph!un=2(kJSs5HWfa9?vD-l#VF(|NG{5mQ%#hHK<*zi_KlsO zMUV*f`M{*Mx%q)84RVcWe85~-DEGL_<72yKleE=YSU8d!6Zd%WZJZsBKJ+ZofTdmn zK1%G}<8$bqM%<(({3(}6Urkc3;VgsePw}klfX-E(Sf94c%(bcF;$qXbfGW$R?NuES zc+#8+Ia0f%t;M9=;gSchP8O@3M(^gcmt-i(q${Dz8CFphi&ZCk<8XZA+tz`6$B-L2& z_W32YNVN=2w3p-8uU81|C+!qD0+^QYgY>Fhxn~kRrdS;@KvXM^E>5NIes! z7^O&*@&D$1A#F2ISNnRIo1QE~dMxniB$ z0@|h=!h-yRMSr*P>A4xQxw*+Bn09O`rl^M;C<1xh(9|v6zng!aG5hM)&U;_eQi(#7bGBC+5i{j!`WJFM1reBu1>8 z%+`X(?`8yxZ*6VWDym-Q@*UMvdfp$_>7R~CGkd)a5sfI-sQBxggoTB*Ax04;28T+N zf4iHP_UES;cQ42WJ^gz7Re^|g(e0tcbwm;Csbb3|@aFo+tkIpR2FL9sMI9YaGMt}} zxBoIeZObNpFs12%0k15C2wX3y5eXTz`-+EVrZZ2Rn$LmPXaO!0FzNeMtn%L3FOx1V zldi(;e5*v>SW!X2e#tamm$US9G)+bz67G_q-4eem105aR&Pcz)hi09v?no#$ zeXae7d|~0;9?%KhQ*%H?>Pk|4=pwBqVTWl#KCK>TWzA;;*7sM3^KD-DSI6AjMP|cE zsz`?p;G@%%CEC|}Y9(5_VBMnPezsxMs>b^r zA3uGU;y8joneizYwr#=%_&2|CSRh*R@wDV+>&2ogS&@Qsm0vdp#81DaAd<7V?eI8m z4RKAC>S|Y5O}-~i`3SGLczo@e$me==eZCFxc}lix-6ap8%Zi!85B3Vqt-&bwBzb!a#LZGN7L!f$3GQHR$lStF>L63vjfhiT;f2EkG-ipm26J z$ZQAZ+!0#@o`!_>?sA{2K{-zjP4KLbQePr#wCH0x>xuuhTr%h3(P)+g90EBP61fEn z9{=6HPiUrQCMFleY-U5cF=O5V=sw{D90v~TJ>h?|ZBJG@q`9}%m8CjstR|%veV!k; zi3Qx9p7$n;&`7xL$+&QSQuA!5F>92k(-ZvbL=$SxsjjZ}1LQq@DJCZ7b3#KPjV*SY z1=$|QGfqzPJnN4W`JtI55jhozKF!+WU!+!oc)c^a9*Km@6ox`7`0!Pvk1+sU#;G-# z#ZaW7bbpf6Z6iuvct=Hb7&0;}`EynY~Y3Co>-gMcr z%WS3T1ps+`Tx_hEQo%&2$p^Q^weFwtI9OO=rD`Q}X|m%Rxt2%iF;}Uq#^TFhx2WSo z9HaIJ`BYxjBpDjCnOd)T@OiY61XgHB(e-L)Fvc)9k5ShT&fDADXo(1-5Epx57%X_| zS{t+kHgma1*CjXeJ}~u`yb;lq0o>f*PF4zrlQ^0zC-N0Z;mMDbuyWKuDEF_9mwk-6 ziSz&^_d(v}xE2-8)$6smt6EK$dffu8eBOM?48Uj>-rnBkpr}{|v6e*_MMOg4Lm=+~ zG9rGtKMk3!FqR139nULjay`~NpVGE7jg5~#e<2o($X*FtY;0X`AgH}tLy6}${3MW$ zE}Opr2=^SaO;0UQe5RxRq>izjWvNGE=SwJQ}vTp4(<{n)jXM(GpmDBL)zta6MjfOUf1C zFzyM%%akf>d%F7pUX)49>-}(3SXo)wt5&SBnvl!$e7l`y*W$1)-t0&S?bnVOO(UBm z_0C4f^XBB5F2{zse#PHiz~gE`1t3k@Q*c@JddY)11z#4PyeSBcbgAC^@oq2xy-x6Q zLU|OER&K%|%*O5Ew0GVzsrJp^cH>BaD_q^_vLd~f#!(QQj8*$@uw7{>DJdb|ZEarj zwKmq5$IH*My3@THan7qnWvzEhAQ8MwO-%<~j}~|gcE@t!XKDq$uKo>7oUgMBW~oeN zy&8JBJxA-rD0jcyyLLHV@>*Hx_+I9;J7y}j-0HRbR-~EVYJ#p_IVpYd4U`|;N0yUT zY#bap+5q-q-KN?PfcgppehE@aLs&A)j69CJ<&g_8=`}UEYphQne>8zJ)>D&5lI3ZVQumZQ&-Gw8b zvD>F$Q~}8L`7nma#!lmrObYjDeXeET(&Y+|$=2rPBpm~T4v#6p`Ht^!c6#Lo9h^kZU6}F*XSbe{ZO>2lu4Hl8xVTN3<2R@4Z3Dx@HV%OEQNR=E z8caak@xiq$UoSX9DgaFIGtNp8p_u@~@Ot=~O*3muVCm@g)>nF|WTyD`N| zfmqruw4e~M=X-LhsXRq3F+-PT7T1u)k4G0m*V5P5PO&-WY-nQko5%%FnG=U2RXYwx8w&i8_ zvyJ}QunH(u)p(#F2cq-ps+azXS>lHy!^5fhmUGFSK`7e#d2*?7AZiO(A)>aaLOy~4 zjWb~SroAsG)wf9nJ-IJu&C>+3EayJGB=kO*$EGu2!IRfdZ>9mULp!8;TpukqgT!zL zWO3*3NRlk*b@$bN4ZJ+h2SCJM8sFG~yzBZ~2?@1@=UV0+wLN>sXGaq8eAu|Y7XoBo8Y@Xfk;?0Qw~no@Q-Ly4rB!2P zg9-x$NEyv1qQV}kiv4)yn2&K0T_4T9X>gV2C$V~dq)f%)JCL@W|B zr0WnOcz-a==W>v61Jv@Eo}OM>QRq-Ri8i>oQ+vudJnpzYo zJfh>(9nf&7U&iYl)@{K*E*09ApFaZaS?oIj>NkH4ge@OTG90hd&Wc=@ZPOuVi8~PzOa_9x)Ji6UVwu_*88xr*5 zHjIIO8aY_Yuou+QaDY$R?pjckc3uhYHD#U zfIr#*Qr5B>o%aP__)qQjVqCtBiIFidznm*kCL&G1!)5#$n1V?(p@)-b3w8o4kdyo1 z$cNUWncWS*>aGW(*pOk!AA zSkmEKjr1R%r(2}?8f&gx(6w-2!F$L;&y)g3TH*%w!QfXfz;P^*pr-~sBcn4sIg*pE z&q@gGA37b-d@u%i`w>eELxdkZf>-cM|8MF@X07U-Bu4Gp00eTfW9An}!KbQ9Tn+{w z23iq;A9D#ftg7#W2yK6un3~Fx0>g_dB55ZgS#CL=8(dny7J9rtT}GNLG)c|u+ZudSRW!# zH*v$MZJ7?nd4cH{YymwIPz*D*9kB+VY3X3#nlHP7yr;- z@2X#ep?0%oT5r&MJJnUN82rBqD9oEXU_)?FQvk?XKEqp9IPXu%zsu40oA{*Ru^!Il zZUd^0{nPc{x@q9r_Fi%fsus-E-F&OpeG2f}dU4UwpBh~bd3l6{d>--@vv2?bt%>(& z*n-?|HtY-nn6%m-5y#=V4A6Zxk{zp16C;^O1gC)rU( z=h~2oxb4~hOqUyW(r|)Wzs*!r!u4=2a6iplbLs>zcLbYk6%3W>{#401spm<@Zc=Uu z36Nl3QU29m{ey$bFEjHxV!fFg)FO|>yiWC5qK1!WD%P`sEHDtUTm1XhqcO>8 zGp93FX*#F|P^}LBxDen@EmSEOD;4{Rvncp*(y2}AeY2_oDug9qeiCMY`tF04Q6VC^ zVB_P}eA#zvv+hvr7B0J`2Y_?NGMOBuG*lA)8*!ayB^mIV2>tu_?m_5h&bGF;CCRpp@6MZ! zTZ`<8XtCg7N_jvW`~h8Z>@7B3%k|)O^{XP`FeHHH+!;W?rB<8wqies(7${JX^x8J# zv=PaW0eO7+0MlUdA@QwK>iX%i|+6S~<3mzUG%tU@9g1je&Q-=&Pw>3YWtads_5cAa*WM`WUgYHRN z0Gs*dKrE>@sE^1_67GG}b#(X}H=NyQ(_lnA% zlqU;St?NM5SGauKW((?HT@W80@+7Kn>@-7oaJz4W0}-w9K+L<*k7OrQuYnG|VU*~8-#!g_^ee84*5h6cIvOi^-gKwo3YVHbZFxad+_x?dVT#Xj5%Pwqn% z;F)VlKtpP}+}J9Ev@B9JQwpaWAJA1sp5YDr&Zsu@kxy9>w1 zPQKxuA~DXc-hbVSQos|smB;|wK-)1D%PQQH`xs|X8E-Z8j;($~#h-RyQ!(c=|f-V24hb}y;@Ho3@r+p+DQ@K{H%>0JoY+g)+ zE0OuO?`!BgAo9QM=4-=W)CF&D4LC77z(yaPoMaeql9QvO&&e_sAwtg&B`;jZ2g;3_ zb#Cf%5$mKYJPN+h%n2f%uT%`Z+iFE>C>RYhfS=7=fZx*w2G|V9Rtac_`5TDT+ATjx zX=-X_ffa~LkSTu<2dS>)(*z&KfOs^&G%arC?b-w^fU~h?9CSAY4T5l&gPGGMzT@U| z9XhNtA+R{})4-7!FtGyInCdVhjU|==#Oi*ove*us%l*q}rkKTa4sdekTGbZh?6TI1 z8X7ln@KiBl(Tk1FO*bIZNXP>cIDsTUtS9nETfq>gNqC(sWRI6x^vMJG+khG4nl9D# zXa)nbNB7my@xLku?FvgC9-g|L#y_BOwwMh^>iMr`vrMPqY}20Ga}p5kR+)aAcPD6@ zu8{01tEjNMZI7hVj^=W2JAppZH)b`jlMXm+pshmHK;}2;$~i%&r^#@T81SY^58fE7 z6o?ijSQ>&IQ?~&^m!d5x$vKBzb$z~7&Pe))BnAzlS@ z+e2j^OBf~KFAaYHge5n)?5%mg{YLmXPS3zp1R;`Jy;NTLpa#dS1z|Y{{nnlZAR^2Z zfyRKKuDzaax9tdm98L<0O85R%nahh}Su<%>=No}3nFn3D)NwEt?Ix#P|DDYn3`%kK zFM#8w->Vi*@PWqGJkXRN#^z-p%p?=+B^m47rb)T@pw(gw`NqHW2pW3&m7r~2H(zb( zC`R?x&9dG1b-DQRHMl)((Nk1PfgIM5PmiQgFWZ>T|M-cIhnS{Ob``A7WDx!Plri04i#!`Kd29=S3 zH9#EyNadg|d*2^=oh%rZybuWoXjjx119^BVsax9PMJI6w>kY}AHoVzjWhxd_;TfO~ z5|rBAnb!C5`bLLkMH((Hqz5Yd{qa2c;PA%fp+q*JFW^9_)YQ~zo_D)>$DrBB2v)cp zfyd`I0F|yhJLnN28Hc&-PocbHwjY5Xar0%xX*%s8q;)!&!SGrT0}Va9A_Y2?ZQ!iG z{bE%cYj8AYxQAigJ9{w$uD~ELOVdQ>&DGgGqb@B2C29fdwQ^ul#AN~aXg01zr*Sr* zUa94vkM!e}i<}%PBO_zD{RkVmd$rBH3zFz}mG`lrDdh?9$i>=o^bEA;NCVVatAk8U zVJ|tI4~#P8LytIs=`G|Z4&z+lMdrc5m(v;JNJgrFW=*g#s-+k+C74zgVlxk!&R1Ob z)-eZ3uiv&DDR}YD2}uP0qLUV$ybOUH{q$Jw-Qo9Ui*a^VHRjC}2z+oSW^?N)uhhG{ z56xURCBb09cX%HSJ-rO=%tyw$b+ z$Z7JlvJ|a?l#r6DbC6|{m>x_cxhQZrEOriDgOvu`Z6pB+j^4BkH!)+^@Z=t#%uY~B zBH;gK57+DPPwg3CW2h+r{SsSj_fFfI$G+g}; z9_)LGU6Ij6_@APMttpI=M-hH8qXz`WnHt&;V~ezl%qlBlAzM818#RwQ{69QhWmr_- z*QFWBLAn`07(zl?q`M>p>6Y&9PH7O3?xCc+Q$mMEx}_us32Aum@5BH7JUl#i=AN_9 z+H0@14pmZyoX|c)!6-HaY@`1hw`2h47Fl*so1Xs!zZ zZVNUl905ymTW6-iGv~t8jnwNkoaNO?TN~%X*|}{7CW}(YIXdj~Dj@EWmJ=D#e0LOz&*hUfw+2$oC|$&^EJVJDRT3m_xmmMm|P4Ds3mvkknbNMXy;6(nGfg8 z3wW*x{vW}QAu&alxM(OGOEMGm`&8lBgeYKD=oM*+8y)UoMn9R7%f#0_zteW(b&mx@ zDH`uhWPvhHl2{1yrH~@ad2|^-O%tfIM-$Oq_M4oo{Y(VPab~pdHksl9Jm;bfz!3qp zFW*11z?{)`-X^$xD}s)SVJPgT&h(nnr-%Y`;;xVl#PcE{>oQp+cP=-768F3)8BXB! ztfghvo@bH__ygkZNV0r0XC-w8uIB%PE6ycVL%D~H$$B*4;j|Nh0@S}YjHD^qHvU9t z@8?xhm01eSr)uwaqQ6{nY-l)CvrF59HL^Y%=qClW(U?2~hdamTD(`oJ+~@rOMmz*s zqbk`>SZZK_&GI%h`n%8wV&GKtz1CdXx?3k6zmMVpb7f`6bQv&fWlvM=&%19Zwc)6N zqRFyQ7eE!u#OXFaa*v=TfkRt@ueTK!JGvh;Qr8kdn(t`Dv_bV5-27uL4n#iROyaIK zbG%@wMQ=_wBEWUz>zW7Dt+4G_;!qA)|xP)40Y0s;j#%d~?W$VVSo5T4VP(=}Pa_Q97=C6D&SsaaX2kXG+OUD}~kQ@^NAE+wz^Sqm8DpXW|?YaKh^@Bhg!9;5#RV28%b{;Q1g@ zHKbvve-?OEP+6Wp9;D#p_r`?wN0OkV=#p(g?TA6sg%n%~E|S;)w(!NAiJ+)lJ1Le@wQ4w2ECE88LxcVu3-nYg{!a=y_; z_+4`i)3{PualqUeAtfZsgb=j!b$>89=ySPJ@T&jL)o&s108tI?;Ntt!^2)d4gLi=l zL69m~0`dl;P27tw(U{}*)Le4}ZZc_|R2IWWWLCja_W%UyCgBQ&sYHOIOD&57%KnB; ziL=Ng9DK1c3(vv^mbQkXad;A0?2r)v+w&D+&iHh*lZAEZHd&*$`8mnS+`O5}@6rBB zxfAdIyDN@@!sG}0odiPEFlThyZ|Gw_a}Q5Bd1!0RjU)I}F4zP`cvYlKYus0)CdANs zodoy6-cjaVJ2^aU#e(~Y$ic04=Jz>Md@A}{)q2ew=@t>Jz2@N9q;zqR3$~|Bu{tQL zZF0Jt8F{7KLbsMR=A6LwpU}QcM@^>(XV;Nr2a*OvZ4zleq#?{5yS1;>l+0nuk{b^M zp&^iroAG#j4J^6H!uQB^r`45*q|??-LNKD+!`g$61YTMLRi)uY;kbHzs*!%bPL!hJ z1)8&!w$hQoEiTY&=jAXWY%MKAFD2N~_qJPP$WHCS(=Xu>90=N>`RX4*yyag~;8G|I zlkNfN&%hA0Ym3c*Umj~70^S<{O@J(c+)rW&B_tkFoc6(o;Jlg<5tS4158jL*ejs$Rwl7?FmUriyscR2%zFk)2i$LFC zw0<_AQ~yeT7;mPWux;|@j6Q~vCC1h8ElR9`y0pLgXq+3~OMlWT>;FD%P}7?GiHg`z z>yuYcCC=CZLDU%0le6HnXrm0NoY)YfGtIM*EoU-9q}2t7==aVO+H5hQp$u*5>lv56 z*P(x=EfThgSYsLhf@Jv$sA$f;D@jsVd{m!wXPZo;Pd_LM7uAs*7j@K#8MklI3(UvP zOK~B2^8E*b%77_#hIA*TJm@Vs@h43iUkkq@6Y-CXwyl4^sX4JaNFZh>Ov?8k!d zX$=?F=xu3C!>}b(H@Fywg$uh=0oFDyt*I9S!h)sYN?{f=&ylxx^g zMSGqf2X)#=`pa{03+kIV5Gw>t=ExI}l{A%szRw|A<^n>BCCIv~PSXPB?L^TMF38B%10kWfr7tH_U=#$su>PG@Zjh#kZz#@8( zaB&EQw2HXnnJ^y<>_TY&1DQD;>rIdT-}wgiBx6CeGi5G`-V1;zG}Y4DU=9~o?!X~X zgE!p$&XrD;*P0V6Wmuz`h^8>Cger1sRwc+5g9oP8#ec01ya&?+3s?ODfcS?(p=kV^ zi^@^L%bFLz$EX2hWNfj8F_Z~{{88oq(~)pdR*c9mq}|F1gEB}O0K=qHX&PSGrH%9< zz>WiobNU$PWbkZZN{-M;2 zvx#1pfPXWshMAJJYA9kd$fa>d?&F>p`<7r{F^SIbI{3e%LD;PDOJgLdOYhU|h=+&E zBWuGWDl6$VqN(nOBtbi+#(hH18VgrHXr_aVKwBiX147+TX#Y-hC8NK(8%A-(cqY($ z{lUiTONNA7*VruQpVfdMXB_lteQRoJV{XfI%TYuxl6d1FYnoao@qyp%tfFNZmfLjz zo!3p@Jrz)Wf9C{+nE4u6*b{k5iJ@?lX2kR26u&mN_jr@(PX_=Zb9e)ApuwLXr+IJtWDHGJ6$y zrZkW)&n8gQBBn=Q9>1WQ1vDck0s!O2ny@Wy4+-qeFVkQFtCol)10T#`VT9>JumIdg zfV&aFgE{mqNwv4o^{=d|_kg$2PodgjvM}qb+?u1Wo1JtAm6fFDg^9IjQ|G**DNk~;b$P-?uw*^PS)+7BzgP3;pEBMMSa0RdOdAQMHr=olGW_!sE~ zk6+Av&4GCPK+$u>{sj||&W-?sX}mOJT87~v9-?P?{BE`65NlT2QF#MpmFd1 zj7hJfwsh}2kw53LX82|_*3yJNLE)U)s0sOF^KF`FP>hBz9~0?l<1Y{D^z*=aoiSG` zOf*E8v*7AvBkG_d)?bt2nxnc2tvqiWOdE;s*k2@+C+t^FzkHl_p`$PF(*-4~5$)=l z?CLIoiI;SBm#%A^P+?8J>?DIib`g>g<@exz=aW&f+W;_d?Y86BgpBOc&(@SwFUsd1 zMWZ{4pmc``U^BHWE^p5#ii|1^MZ=?J3*F)fu&Ph{U%IwaOWk=~I;GSZK$4aqRYS>| zNho7%suZzp+MKScznifA|s2i$lK48!i~`2g~(^aAR3p*koa7KyU_wU-g7d?st-Z zA|}prSQ%!dnTE5?PQw+iHz?J`HB|*v2ulFd@lKnZvRT~{eJ;us<{KWKXBIE~NkW7X zJBb}$GQ5F(@!JS}ln@nOSadsOdfirz6np3(QYe0SB=Z)DJWK=s<}t9#kJkK+18~Zm ze4_(FP%uN7fC4Z{S(8RDz3KcO%q62f@$FMN zrVs*}hRK+scYs&X5eE%LtfdT?_%hCLgo7vP&JU6$qpgPkNHtXZ?s%$KTy3DlPp6}j z_YQ=}y(d8=+GK4fQ}hWT(gHn=cxgRVFLhNTBnxR}I8^=1OOM_HrEC&PHAvRU11M~* z$uQ21@qHN|@KZH3U-h!^@Lcd*a?(ZA!CWUGG?MT7gr?pf(xpTNl;b5@_Y^^$&2b`P zLp88masMJE;3VxFovr`Q41MT_Z3;;b7Z&wwATTYRQ>c{WA|Ox#$vTaGTL6^Vy*{KZ zxFI%Y-xS0<9-@os7TnKG#y?#0#VxGs9hqb>PIne$bY3l5^~>+fkzt+}J^zk-2;0dJ+l*xvnB zq`wKJ=zKeRtC2H>S>z}$9GB>fr|DQ0X}XGn zi9+=X3|tp=5Z>iG;=-ZWS*biLo#rl`m~9>x9?MPB_C3)vP9{Ibjtr@_z5nu;=TG2Q?+8VOs&-Bi5CnIeyw#Io?#uR)3 z^)D+GUZ;&#+A{t+DF_W8GfYBoZHI|Ku$tgzEqT|$>B`PskAao*J^dLEsHw|rsMo*` zU?eixfGOO1Nn6|Ec;~tOd%C{p_gyH$6r&jXROy4m;o$OVRY0BCPlptqR3?Igbwxc* zD2<+v9X05PhN90)Vc16&25b6OYExI+ep^-N<&)e4?+T#n8s(M@KbO+dS_=Vw7twFf zpVS#h;lO@Yc)`9U0SZwe<98B>0-jvCtqu)D&+uUe?dOLo0%A#{Um7bda`*nCMF0BU zkY*frd?7$kLA^gx5^_k}h|7nAb@;7(ccVGvMJfx;LSLX8XlV(&jJWcS0PB3j`ogXJ zzQA*^<7gQRz&3=^lVJ5>w3q5MlZq|Xz|b=24iMj`7l#6hena#6#eBhivPFVjH?UxXoO z^D-*?HK_Q?Z+owz&vSK{MqP5WxrJ~eB*pgXOvS2T(T*dv$)k`dR4w;_p~jizYDm{=EmNxY#J-=^Mg2kQ}9nP|{7w>ZtgMBp+V* zG^Q={*vENmo0{f^{o$D}ix64gzu0O&;l|mZWt5GkcWHWJjTjlL9onp z*>p4=INwIYe9Zt6>cM{kO^S6_D;*O2f19s$P{5wf0PUz{68v+`n85*EsN#<*0WvTw zCXV2FCxB74pI%v66Ti?fp>UV; z^P>7TGTi?^EdVA9j`hm7t&U)~ftC~fSu@6U9vO8}Ilh3MtkM;RQKrh2Vv2Tc)na1% z`8H2Q7T56EtCv%Wr|wjkk53yjEw+AKWJKaz#u|k>r5Ctx1P4vZPNTinS1|igw{Z@Un-)CQR8;%* zR^=;9;tC_9DG6HHu}_Xv3)RXQibm2f;{lNX#{N~!xQZN*BXZddP!pu&cl^I7BsN@t zc4Cx87Hga}&c&!xrL0waN^Y5871{ZzPn`>In|{WWwKwsm8W4g{11h$S$3Ml1{D9l# zCg0Zd!QKB0K-lJRfF4dq&nH^%RZSK94BtpPz6{wwMafx_ZOl`u_aB{f1m?*+P*^Jo zF|pPKQ*yLkAAc8K$zo%GX-Z5?8v{Yb6^8txCv9n}nOV3$t4@LXr=!RGolE){zBIQ$ zzpO5^0BxMWJi~KEEiHV%-Z0yN#iZDsnnH3wKD^ZOSl-GTC9FI2hBI|0KBmZ@5G!J} z*^wYw`aS(|LdHXhTcU{FtI9}v$zZ)b&PQaV+pl=*4i=_6_qu9O_ z>6cUl^)L62@=;+YOQ{nr?k2Uqh!u7fUc+tC*imAQapf3Iu*m`w22YDE{ps>UCy*#X z_S6;V0Gc1Hes{T3VfrwmPq-gdL9aPWm^t#Q$+Tt!`bsgsF|pd}r#FvhyF(5S9HW01 zm6Mp;%Rq_l2!#0zYLTxND9ERx@P@keeFzcbz%u~>Kws0*(QR*(FwX;q zGt-yV(4PZp(57F&35F&hn9yjfPW0|eR4``cs_z$rD#P5|gmk_1h$eLZef^zOVAa#k z8*x(LZyork%o=n;V~AK{La+aEmndoa=G7e}T;KY0qJ{O>=M7Predkzd841bAD9udF z@h9>L>lS`AL>NgZCy~Z_6m9hHEN7EFe+ifw#7FB!glU>W`g{UNtExt~!>h$s8mekdsNaiyKr9WmBT3*1!GKe$NM%a^3WKb) z)LO(rHmH^IA-&flc5+u1la>!IyONQ09fXoe`b*^DG)1)G5{d_Jl5j7zbqoRPmmSVv zd6d6a6S0oD$kvPL+2%Ozl7554=7H{2{mU?&tQu9o?r<RH?qwqW(x~hU>Z; zEQjc+=Z4~nES3Wp;48WPJDCx_GUlTZFi>KFgk_+C1T!rIlenu2Sv6jKC z@nL;*tdC@_{UIz^Osp}XR-kYE8n~M#eVxIf-jk)Xv0))r#3 zyvc8o*toG&`qhyoCacZ2mX_4p>Igt2(nap(+&Jc>R5WexM_e}TksX5H-W& z>wAfUjlQ2)M)$H$?915p!!=}k)nW1bVNLi+OGl4^y>$*is71Xb$Sdx9$95hMh9ph+ z5nwg>GnsC&B~qT9nOPx}5O#P2&G81RUe{m{K;MXTKSP^$6*zzA^ zo){?Null%aWjEro!17T@G~l;W_$Xy0;P7|MIB29@tv#2W`27TCug-sm^%oqCNd&2Y z$ae)Nuw>NLES}$1b)H_0dq+OYC*J<`rg~q-QvMmazUkcWF#hjoKo9rHRA0wh>P+o4 zt@kMkNV8lUMZGrPLf6^bv?karv3nCx9ir5joW^sqnZm8&aznj8Yq2EN#y#O2@TUcB zeEWO$DrWPwZbM0h!-@_C&ZD9#K+AC(0Htx3AY|i}ZjIi45x17DlEk6Az1n%!6M4v| zs<`8|%df?~;_0Sd?|1>($zrRx8aG(Ic65eEGWvAy2QIUs*M!%?%XA<$;5tSe+WA9n zZbdYh%D?I{_UCYonh6<3kvQF!lqOK9?gwU|;b8C(cSp_OdQ3ocU;$Dl<;mZkdjX;( ziBxXtw96vbI-}aF%GQn+bwJbFwJliJ#88IU-Wpv4acw0qn@MJntBGu$VspkUinCUr zPC@}GI?MM}V!5UMs>D(?Mij8Ex^0oj>MJH7Ce zP6Jq?kGJiBCMcAfpIqIhO6YVcm4G0Q?i_fDt7bxrbSYp-I?!4yy-hh6M|j^3jr1+a zL#cKm-3nJ+Etxqp0n5K0VObFJF}`=&*lc@ct@A|ZjepNXxrQCt@K-vwgl)QIkII)% zg1vT_GV1(9!jdkcfCcik`Eh`jRuFs2-jYS~{X=HW5%eM4PL4Vv7?_PU4*Qi3a)%6$ zI1_|RLIhatvzXGY!{`X5@$Bf67gLjyL#(3AY}3|QP|3{A>uBfyDo%b1ErJ${d=nmZ zVgNRw&L20xwegSrUT5V|Z>6Oe5QRqTOh{0~S$&XWBuVZ#HHsPZqAk|@V@rvxaq{2N^zVO%>Z(hKDqbolV#y9PBO5?VX(W-5FqrxJUFN_1odO&Gad%9fqFQD_cEY%YCJkc7OYJP z_b?tb4=m9L_;)|A^w28bZxAHfUjyRZiQj-h#4?_1!M)GGNb#S1aVR7UP^|LzMMM zov$RQ;h?OR7rVtJ`R# z(kl`iqibr4tCuho@e-@qutGr~MFRV1Xv_?{1Oc(M^g;JmFx{i$_2KEmq`;5J?JoG* zE993;xB1V>tK+8=T|B}92>*@iK>4Bttr`QLc|afOtTJ@-0G}5_*j)u3feh@>VpP%{ zQgDJ0f=E|RRp>7D<=Gna7JQ!_e^zPrpudK{)#V_FyUI2|N0VfP*8;|wc)YR9OwRHU z4n4!^~ZQutWMT2;9E4 zOPUL!VW4X?#cHhP0#fMD0uwm;#vqVcCj>t&0P9Ed2J-Rjexc&kyd=}F}4 zsi%a!sofAyXVtQgYSoG;&Qy^cBo%)?waeEXkf!mYNkZV(EPV#`?iueo@5LX{Aua*OlRBn`j|C_zC)!U)dETpSwE%6 zvFk5j=)NGQ(LV!QwC5)>qR0s}>E}wk+RVNyRL`V*I%??G_{Sm`^t*NJJJa^`jINf} zoM!A$XoZ*a>Bfl5Dx;PbcbT)51h{37?QQWZvWUXGIvw`07^}u565YwF03@m?+|Ml3 z8Lo{+?r1+zP})vc?B~si9m#^%#p2KB;6DIq33*Z#JwtLHJQ7Gn1S*!ZrIv{AN zIyPAB@*wrUI(e(p{7+zX>}Nzrvz_>L&9(SG=CWk29$%kHUuryrG?zUJC(WpymjR5W zD+;h0Re-d|gNoY>ZCIl)iFDZdx2$dWk56VC?s4oRb2{0pn=jnAe#It;XG$Rns0fiW z2PxQGF<~>0l4cC*sK+UNs_D4p_9nB6vdK8@?;5(Lw}YxJAV%+KK%yGo_x-5TgWnj}7Ce3MFn4d#MUskmwJ{ z?u@$t74l?+pICQKLSM6Z!S^2x^DmnCpW6dC8OAQe^f7XmTZ1|GRD*9^AwEEw=dIot z#$)EI1aau6c0g(ey>p#SW%15E@8%d#9xr(j1?u!>wwpaSUeM``p)_?g6SyBbv#m~du<*83d@Oev1M1{>qpXwu zC@5Sq9WeTI*J!{L#S+N4&2UCs+?p51A5?ysTWh&lBEnY9W0e)+JIwW%y8nPuh0vN* zK3wr%q(EWNvl%Ml(%G3AY2{WKP(^ViYh#n?KBI!9){TKWk-9GFk3qwPDv^sS=AN3Q zUuOy;j*VmE!AG_I^64M&tJjUo2IZ8ysb^oC`f^K-K9rY3iRuP@9{C|?7bk&iF=i6) zcwp`j$ftT}47(Rc%I;02%7&7>V;OCRQP78qv41-H*FfpKQ0?h>E(biX1))NFFxodB zhni7zAf=!7m{*a;r2iGoVsxKPr7xRsCoUB_uH@SLQ(N5DW_uigUn(Kx<@6^uz}hlY z2CQq$QV=*`ugnw#GBj!cHR!-SnmN9gdy9th;fo2!!X&yxMoy4?_^r~J74g3Pi`1B^Dd8%MG$k*a~ydMg1(cWBmI zF-y>s9OW#6uGb>F`Ywcu;oAB(ekT$coCvZ_c^DB6I|`g18nj~-Aomj9b0I<7+682u zYtk%_=RcV94OReM=(4)HddBc8Wofn60aaAfzJSew4d#Iw`Zub$5rO!zkGvN|X?f6eu0K=y8o6%N{Y z(XIBX#xpTWTHPR1m84WnaYaKhh%bAbbLY!`;>8E0xj}b&K)!lSffLy}b#6JidZm2& zdC27lu3xH7-atcK#rnUQ_+Z&|w$_X#f(w}siiy_TcPWe_Xh@BK_S-CP?V)`G2T5(` zsDuvGYJO_?uxi`ka8@c7Qvz)EQGLch8{rIzSp}-Iz zjXjNyj^_H$2g4yLpM{r|Aa3*Dx6gci)yIKt zDuaS&^|k*jPQLec{_~Gm<5_(og+bOa8AEDl1uALd$jS3O2X7Q;B|2BKCBErXlE8Fj zI`(2~bulRa{99a;=@4C%=^t&Fv!%|-%JRJiiqRNgaO+m-%x6^CKtjTz_4w}J)KRC- zRdR(;5=QZn>L}9~2P++2dk>_;T2d z>iRWO34vsmC6$pN{7d6L-`ZeNQ)0`yDF4XCYq*wvz$Tzdc5`#fW@2Je1m4v+4#wbH z&45b3LJ+*RcnA1?SPHrTf0$tsWP(z+bsO zEq`CCJ1hRX5XZh^-xAJkKCGO|$YKH5-Nt$+99x5`pjcRa?qHd}oDau435l{A2b=nQ zhhDh1-z!S?VEnzFr=mO8K!Y7zQLb}CqkOwQa=kg~^>{W^!?>5#Vi^&3KWD`7uyw+4 zHb;ttQNYX!*#Ums_rMO213`>|gF?=KJL()Xwazg{S`Wl@0$Ul5}?w6Zc=~Z(UjWpeXj%?_ZV0-`_(@m;mc2 z#KeU0!SWEmZ<-9wGNTP6#eJE-F6u1TEOmN(2_K$D@T`552H8p@4XNM_Eq~ch7UFu9VYVj)5Wh1u5*85Rg9cv4NdaB0^qM3D~~MAfyk6i;e#t9Yt=_ zw)A#%dyIa#LJ?VRiV$vIk$)C-~G3KH=$xQdgnrjXYAj zTVn>iy6uFGdm%i-GlzcLJ`+V;cCnp^XzDPK>54Xwf9QFFSDTrcJ&lPWEh8govB0Ru zEO{yiMwU-JFBvNfD-U&4HaP?YT8Sqw;K}URp=UeEDhwOkBj52=>T1?4$t~-En?-Z(!k3&n~0I=KQ+*I`XGq$cSH2#7pe? z+k>wT=p=CSkx3H&vd403O6s84rrrocKgR;v%F=kJv!lopKhBN#L@!#DtsSSnfm@eZ zeiPiI1r}$5g}aawpXW7#vxmAL_Ek;4@7pry*B2t* zWl-|BR0E=PeW8RS9>DPT6={=3@xUN<=nZQ=l=-Ek=hDQpkNDf}rl)3SvDdgCf2ffr zq*Q4WHBi~p5PO+_eq@8E@8-lteblsn*YX1PtKVfcwg_t00d1norw>0SLJN680c;Tp zSQ8VH0UA;<$)x}3=5^ySY0yb7m1WOgHU6WuTOC zV^ZzZgRd(J%MKIX=~wbSrN&!>t0tTNJ$V;At$`)g@Hcxs9+E!SN@j)} ziYt=sdwP>wYRfu0)SP_BjFR%eY3>A!QT=%)FFOB|vd^t~Z9GO6LCGflm0a>+#-G?U6q-!@oZ>l%OMVMm;+}Us--Pnd{f+6`WWyt+Dyl>S;&jE+_$bUXb1xZPi(pevUA{ozfNtXDgl@mEl zt+k+s{X8clw(D6Sd5}w^ARy84JM;TIbIe!E5|N0U%YED9PL~TWHI2o>FZ?w<*5rsW7>qk&#ewH#Z=-pt4?ZV4xzXs z;zw9dd8Bi2NY10Q`rb%dU{K6VsdG*HyqjQ7%iXuwqn`1s_OgL^&H^s#(&3y205?zm zXA1IT)2{Ic95|0gn8C0j=mg7YYDS`E>E?b%2VAAXiVH^FkV4M|Osc0phoneIG) z>J6RBDBSPM!6dG}eomJFsE0(uNrTvWw$dvoJ42n){cl;zr3fZSov~5*UIiB?#exR$k~<1r#?U65b%2DW5;fs@OCna z75>*l_?Aq!W#z&i7MQt6it+%;7tVO@@~yGl|#)KjD}ZJ<>~FMkEpJlbKJf{%6wy|4|RhtMC1^qk7MXcJv&^ zI$?E#{MEQmd&RvGGaJH3`kYh3FB3p!3XeCNr3&dM`#SV7cR#7d{FpT)bOr*nXB>3I?R2I075qrj(XJ>31fWj>CrRugHPiXFKY;W*9(?OhQ}xJd;tccJyHV+OYp$z+mH zQ^)^2643%|aT^JQRV*_q4ET?wH6{eCgkG0Can|q6HM9FA zHjCkh4eXWmqN;DSUp#*OusN96V~UU~7^L_MvSNXZ;inRLD`*8YyY(E z(J$%#5TW7SdNnI#zBQPbPUJ1~Tcmb~3eTekvvOoUAWFE^;Ka%@rbtjRg^_WD1+Yl{ z?gAnl5)~B{?_^f4iT?jQGW%k&5xK-jp7UPdUhvHf$dX`iW`N_BYcIXt97(8+A0aCH`MMlwp<{#xB4+&F8`=`|-R03oNDpXyThunhuz`c`! zT8GVmjcSvq_tu$ncoY5`Y(bcxA_(vhLd6bui~jjyAr_e8f5jQ6gAnhW$#wCr)rM(* z%r8e=|09095YZI&U^=N}-Mi0r8XHK;Zm*TS&~_YS#%=^icSkO=tPJH+2G7oJKA1ZJ z%pC=?W1(n6s!sI$lpHJUbs*{N<1~HHQl)D=8KJIcV$S>Qf@bFGjtb!ZTkz#0QyT7Ewg@9MyBSEU*3* z{j+d*bHO!$0A6AbCMxrLSOeec212r+#m5S2Yo3JopB8}Sc(YaTQ+tWX=76r!U^P(C z=CT5y6-%QXDU5~X+ucBJLt-kM_Nj=XO~R5r4f(0a)g6F!C_`Q{z{@6cqNcaSf+u^v z6NP-m+A|EixC$9-7K(En|IyIPtGP~jtv1>qhOKUzkG>pE=O_a_OSZo;q$!0CUO3#~ z^9;JxgABZahww-AI4<`aV4^1P{*LQLP~ltZrd!-@eKijB31)|10av7mj78}=_?fUW`@oi(wON9>`YcdIvEL3Yi(lMmF9%qA7 zTHy*m7y&m_E_@xp+l)CBBAMutYVhCs#Sb$pRzDiEyb$rOW`|25Bbi^=744&8t!@EM z*emS3{pm)O+0yFPm6O(r${5Gx3^Ufl@55G}so!S2XXcpTg<}N9IjGdpcf!Y)Pa-3a z;k|>(8$x^3tfXUp^oyOe%Bj*ZT*X-wd}W&Eh97MaVi+Xq>mk4xEo{x#4q; z*`3Hp&ZlCo4>y0cTkV%-j@$S1>?lqjFM)Nlfbt(kj!Mi6rRg>fYq}sm`(B(|;1Mgk z3GkH{^M>r{LfjA<(R=|s+l5aQq=l(=@6!VeLu1Tz@uJxMM%3f}A9Rx^J{{F7uUr$d%Q_Xtxu1-)rlJ75Z|+F@k6yoCc4HXkDLsogxs;DC%OfU zfQ6g&3dOKiJ$rNBQlS6)0>(hOOXHJFdKmRjG^DUWNqP`H#%!?{?HpG5K2xZc|PO_x-9g4{R`A8F2UnaJ|9Fhxt+00%tZvcQPV`f#$be z2h&p>9&lj!skD7s^6J&dxNk-@`d>L?t)_5~0!MQu5ehOed;_RMzQv2z|9Hb{s_JirT=lS}oi5jIw6+7k zt9#yb@h*MVVLHD2-Nzh~l+-|Dz|#;HkH63W9>o==YB-);-swKz95ejo<))RIv0Kz( z8U?W40`|FYn}WX@W%_0|kL6Qp7to>wacK3BU)1#e+2eaw7yE)Qe+Pje0!V$P>O{;tK~LzAPqlNlxsLTZ=}3J) zKj_Qm#PG@hL#fTbbwnXjC%v`T@m}^`<#{q&DZ3^#;?WVc+TsOMVd6eLs1YSM46{#o z0#dKwJv4L?y5m?3@I@uADte)9;MPhJhiY~JtTq2u)ft)-S+9$ zc6`f32mM}Db#XfPLyhMo5aqxJf+PYF7=EJwf(deKH^3ju5#o{6nqvf-PKL$qO9X(L ziBX1_;6m;E-i}k=f1F2Nvy=QVmN3n}qHGd( zhpQBMA!a^Z#HHOqIHB#V+e}*z}%>PNl922%z ztsQ`Ng)T7Rw;Z5)m;4vYpX~sF>=Zy_wkK)tZMsD~u1@%BxO6!Ztkl~+31wia34Rhv zQt~cHN;N?-?uo*DTK?sP>=1bjkfTls|#8W%8T9z zGhL3jRgCP`b+K;eZ&J&;+Nx#-v?c9C{ua9r& zRgAZ);eGMnPZWbYn9^V`uU?W)QvVF0Y7tWt{@eMcBJ#x9Ld)*bzx_c-Gz+W&*eOq_ zM%Dn?Uj)aZW=hF)>dZ65r*6r9Rw&^Ex`elS`tCss_6j0tg5%y;<8g!=R{^+@(M;$g3j&>U;F+|(da(XU`zP4=VLNHhD zbFK`{ozbw`zifC1j|vjhgbg);fI%!VMRd_koM$|v0{{-yZLCk0DmR*h1K-6AXc5axa1*z*9_-#I36Q?8);^#xzNC@vAK>Y{d2ZQD165`l3~o?V+j!Upvk zkmfY~_wV0EG>JYP%m0Q-djE}ctNF<>dM^2SZPgZPv-vt~dIjtVju$<^mMQ_{?0ZSS z>IdoJFcQOV(*+Tf2wmW<9mX11ZH1>NM zejlfu?t0KXJ}xQ3^Yc3IfCPz#W`){8f z&L^O`bxUEqLJf{Wlkiqcw`dxkdx^HiqL4CdQkCug7vVY&5E-v4-oc;l$bU-6nnuAw z@N^%jiy~dEx1El$R>bfBv1CjY_O@^PC5lJou3v>F!v^lQ?z>DrHVwGH!1}N<>`_$G z9fjqm-ITGbsIsn8P-#o8Odta?h42ITy-qtEn3R15MhF3^`?p^;wbEWdl%&;js@L?> zcX5}PwfUGP7}T}(H`%7Lj@Fzugh)pHpc&eF>c8s3A|%n>a)$%0?K=5u{f8v$-70I8fS_eJ3c8ybZPH6sShJGgVWclPzuu6c59>D92Yun$?98Z&Kn%cwHs^534AEas@`x+y#6oV z6*%}8YJmJ09U$X|@{^TS8Cm@BE5Lw^E^*V+HaT0nK+yIQlnT;K|0Eno#)B%F(Tc`3 zTqN79SXIrenr}{tY!J?rNQ+YhCbp$@Z=hn;@a@)!7q83Mm7@)4GB7Y$k8`~Itz?VEbQ zD+b&ovQ8I{LoDMGKWEz=w%s;4RjCZBkYb?*u&lIts=i=?A1YE3T~|)v4czyar-@lS zMxrs#KmmSTf_OQI3w~Z9W=Kq#&;d8Ae~4Sy@=d5j&?J^A*xMg-E2KCNHid#+0d2no z(a_WKG9tyBm>~4hiX$E-7h{l9rYRk**&gDIHSZ@;~11fD;ZF zdpzuCtvjwcr;p17FjL9p0}GQq9CgvgI4xmCTv<`vIRK1Q$rQ5K zAP2u%BOJScjbFdE`UVQddLTlPvl5{!!-QpMxjtJrA}q}W3_L1lplE?QP**2gcy`z=6#cyK?j>HQm~$Zm4CTF3EDg~5Q@9} z)Hr|mZFct}NR^4qiIWf)V4aBjSOo{AKx`$0C=_*c)-NWeLwAR)4$)FT4hY8h(+Oia-9hkN;d`Z-*hK9s2m2kRAyk0RdmivYB z#%gs+-2Hdi`2Bsgk(E+R!_LiXVC9h*vofNCO@~%IhWM_a8y&=joZrQO0x;^|uIH4A zfQzP(ZKJ_Tdb#+aYxfH#BUh8jz|5gdMIqU~eA)&8Yy%$6<5E@++qD z39$5g8+>=YshaYny(G1D+$$s;P+tcRmZ1kGf(@+2W09+nt#-yZ)x!6;%j2G&o+I)J zgr1^QRNEo?rDT73ykX-aLe(-qkxdLoU)r|owUD3oMjJ?3pDCV!c3f2JbQSpTFozIu8)R9*ej zeZej*_CmJ?7>u>0Mc-LKP}c4*SNSuD-blNgU>ay8uXqD zl!{KZ5;99iHxN!^%t*@*;gfcQc@u;_8j`w;>~_sL9sdEZ5}8~T`UKYv7fwRCV$9T$ zT_Q)3ZZGBoaZ?(q|GhT#9N%v?GWlUIVS8}pJ66>H_zIHxejgb*P)cnH0=0kbKi&Tq zV0<;AWSo0*`y26|8k64;{j(h*;*LdJp7_#XrtSN>Ok`NvWu(-T(6~M8xP<}>8zBaU ziZx(6+5QZy9DBhJgP!i<5PrOEEW`*4%bMI3? zE~U}#kymZl__?}(x`zT&%B!@bWO$+}VMlHPv_Tq~dqe43w6KD&UB6Cmxze?o8RH_R z&K{jodETxnk6G>;_S|+a8GDFPC(CE*pVY26*}*9tPJK_*jfF73kH-OWiA8YWqARZlSjaqEVcHr?2d2-{7 zr;5!!vJqnSRyMaq7G?Yn4gcxA;qF=)X{Ai4!eU4?gh3xp&5oghPT5+8!6I`VWXw@z zvYUg7cfvpZFezeJKkH4j5p&Q+l48nwhODQGk5aG`hrvI*ImIg8eVE`rA@##5N7>| zqgCw1mSRQ@*U-=0iz5642B^H{y!&9vh5m2Km8zC`qo|*WX)%!K7?}C9bN88@KNoH3 z?-a0JB)NWh{F7A((jRP2;#nt>kiyU386IVA{5(bpYZ&9+*9~?ZaB1KDN`DS0h30Dw zg*?qeVd?<`t>30RO(tEQ1cF}IHi|c;3u_||yrC?+Ni1^(lJ!BTr*7`Z)yB{T2YnWt z(5X9&X;><>IDQ2Pr6~vkAHzRX!g6{N_2CG3kU|NVqxEILYhp1&I6`As5JWwpq0qTG zNTbGMJ>MWgJr%|)M{uV+?60Ozd~X3`wvc( zKj}|9HA}b9A?KO!ZB^ZHYt>UT9XP#-^uo`(b@984z&qc#XOaHLd40^>eV0BLJO5er zF+wWHh-n6wP+pHh$Uh4uh7@ujIQ~6AL%db?y-&?3*_@0aa_4gJPs3 z??6EdrI`BJ>K$0;e|Kw30_Q6}q!J)O+SS#=P<_sk$q1S!%9`na<}ujvPRG_C<-AJC z0|djx=as~uA8eL2uf8g>;(Zw0t_0tqo&&+NlIm&}FnbU&lkrC?zrGC$8^MZ9R!}-=6x~hWp=-LDBIIA=kN7?Y`0eCElrm8CnujM9RuR7xpqdX@X6;uk)u z!2L+^lI7tqL%_($3`q@mO5Dw+WMuKDHF%VG6PXgaI`~~0>J3w+JQGA7u?u5t`UXu0 zs*mIHkK{5RIVUmdme;P>c@Hk)R__(^&wd~OHFk|01*X17cX8t~C^S36GvLUJ29{!e zQ+YycVAUk^-&@XLqA?ip8N23>TY`0sr%ykX=9cj$(}+chyk1(V`3!m$m#QECn%-Os={w|IuV#`$Z$O(nIy#mNSv@3T4pdcl4!ZC+|FUHTSXg=bEf4WW zUq4ZD6_Q_ZnBmA59D6MGg(5A*o0Y#WnRanfSNHOf|8d2rlzl&+uMqTT`SWMYK$iec zw>~Hscv>Lq+W9E3;UOn4|H>Y>BW7E~5`a^0{qMi!AR7z@KXUv9hZhh*Qnl>+DQLgH zV~<5(SQx7DNd;@BUDEVJ0xHvSm|CW?hnAUQQ_#tO{@GmaXsAJ7aTe67V14T7X+EWl zb5WYf;7iC`IccFsjFg5O@AX&fzUnWXqs3zks$RjZ-@g78RMFgcLixj=4bghBv>s4s_o51TOTUDogC4`roEVm z4Z4}2u+HfxQfc3V5<2?012k^7E_z?|jx7>*H$JD|R-_9HB}${L(n9)@Bd1CjUq0SU zotb)v-{n=+rP;n8_*ZQ9Wodg3f4;u&w`vrPbS)HcH@gJxkQl&m-$x!4)ohW;%6dsn z3qswsOK8xRX|^(UW4_15o5=!HJS+k>MdNXO1f7=0nS04Gct71&&}x=m(bhZH;05@4 z$aJvbG{>TBgz-P11ymJg@zNSHq+b zOBFD7p2fXWB4G>&e3}1OMmyDD=wV5@m8J{RDjsPyTFKJ?rB<8+3M_Xdsz1PgBy6`` z-LHTG*sSG3IJIzx79VbUCWX3h&2|K3u0yU95jrN)`c4=wGfnR}aG$wT=ho?*yV zTSM*G{=laN5DNwI4R@>n7Cgilkljl9LvhHV9eJ zZ~Wvwq!3z5&T=8>=ePEk6<8HoZr+ls#qd-ep07;2C!9rZ1Guup{Z>RNaBNm_U+=u( z*Li9+s=e_mFh_32o)h(@clio`Kia^ODrr6iR5JOzlX;Us1OE5s2wIhRk%ei-Ti_&B3L8>KYbbYo%wc>hnf95jNP5+n zQcB2l#w#kjKMLB&313EIjRn$91uq(8%gJot2l)v-pj?SSeWh6K1A}4EH>3+q?vj8R zsKDyiE+{ve539&CNi+T(TCI*-yLpPH)Bj>hp+KyRRAvsPuZGC~dtS{j>jXR>S%QxC z>V98hrd!$CxTc-waFeGo@l)%23g?@K;QTmV>i*G7U}BBWVbXp+1rn8V{($_!Nwo?d zl34EPtETE|h4jah^)7*u@a!H<;pT5u?B}{=mNOj8aESlyt$P2PEA4v%qRS97lJ0lF z$D<{+iYu(Nv4se&74o438^beXNQAELsR|;|8$=={ruR?3)YVtN|5n-rZ6q#gwDTSIYxMI{?5iU9qJ5Wf&%ki*T*-SrjvuUcTY@oGC{cwH`|zBoa0k$n$d1 zrR6I_(~M#k?_)XOQTO#%K-Ax6ukxl^wmg*#XDSLo!LpZ!lvqGwiuo~htzZr4=si5f zesrEt?S++MH{;V#*~k5)lq^fN#~R+*$Ihnqjr^o%A?A|?t8s6zwWwmJ`xAL1D^=lx z&gI(o*}o?{)dM}!6_!Jnwi;C>L;=3S@k%)Vp9OHIpkB$+N0Iwi6|OUaZBG`k*tFj> z^<7_N+l0eg&rtg`xnJ-=^7-L5A%u9p;BTE|Q(d%andb~g=PkIEm;|oAtB88rQt-4(XYU(83;PD~A@ty3mOjmK)^b4d=g6q$XM5_#?{pP?)+@)MfVJ?xoj`4)Zs0PxQRrWF+$+g^-IqDP%sD8yg{D z%>#lw;GLK1y1xtp8|^4yeTz|2GO#+a{S1#r4D0^w0)C;^Sm0NC)Xj4S?Zjs73(i!* zL}Xm-;&x-8yTz&1>`;)aoG4dFquz;$E2WI>Ph;9CHS+ahCgs`hfgNtv??v?EkqmMT zo_K}@8DP}u&XWo80~0RBe+#=ZEGFwomjy7b6AY7eQIbYx6KnXeOFst>k*4=xdsf>4$F zAEV*@NN|G@-G@Z}W=!5mmaF=Z+eTELMlO7Mz3RnHz~r`P7r=3G*=gY7o%J-+bxjLw zx`%34%okY=2{vTQW=`Qv{OB+rEO;+pbP_a!~{CD58X0o%&d$1EsXyc?UuKeEI|h-_fI0>neuY2Z$oN8u1P3(3}q@ zF0X69%}sTjc$zpw!)VG0=>xYm#wG*fo<-eb4#& z_R`zK_Q&kH`XD*18@I+Ve|&8`NFb#$L{gwdEd0ucVwEzs?s8bT{W^#LOW6*pzvI z`y$#;n?@4GBi0)8M-Nf-A`Qx`$Jo{choKPgPyr6c%~g*=Jm(qyI^=@QpmAS6)An<~ zqD*`DI~>11r&)wK&;Ik~DlC;!L8QE2%t^#^MG|d4F9Y#o`xFL)e3XS6W1Q5qn!)2zv&akgZj9XERb>2lB5WL-BHQ@=ndh-OPUl-_5bw*! z>$HH$tg9B;CHyWtKwu3*5%MJT)cWl1@be2%Sy|beTz>x;2uR@x1s?H|0I;#VIB2G# z+D}#c_~+L_)MjNtyOEfkBNJEP?FkX!w)_KtQ@00&BIc6B(6Hn&B zd$z2+y7hS_f%Cw~_56sW83?m}4rr^dp;*h`unATnV?}VQXTE2f2B}gov_B2L^@0Q% zoF9b}6vIgr7(lui2J%=2K=u?IXp&&m}B=pWM z%AHLIC}fj*pNR6RTx(4? z2B24<(0{uUT6^u2*68(Qkqb>}pe~lJ;iQxMprOWf%#Cf&V2+bto^#xyH*=J5$3LU; zL1TLGk}r4rzr*-*mtL$BVVv$Vy5dlXTAeT z*X$1S-}*Q)cW|d_@Do_A$rMuy_5|H@z(!^rq{1FQF5HD2Vv zis|vN{=OM6M}=DBRZJDDHCaFK(NJ14V9g`~C6039?pKuX(D5Y{B&tVP$!x2(RS=#! zFRN+8;K83;3-E!}wfUM}&(H2;_-aR$aD^N(;~C@9d*6{Ym>NZ8awcG=p=4wu<=nX3 zMgL^nk*%h&Tw&&`C!VsCtC@U=B*r79lrbA$B1e328SATS>k_Oq9x~IIti|NK%(|Q) z;13dNf)eD*axLKDdAm$6CPqWj>2qi>F8BP`Em6Smt5K~vA!>9WQ~7j>VxpGt+NqB^ zKk>&uR-=+KGO#@a-&iLImlBAujpSuz8-G!Q#WdgXXvr}As7p%r+_pKFdx*;_nrUaR z*ZYX~83C`O(x(=GX;nhr`}^adQm){EScS_YN@ zZTw54@z*n(KPMMB_C-dm%;)D3cMa3MhMmMZN>I|I{u_v<6vwG|4{a0uhhF{5&5p_0 z*@)_5H@J^MFHdlQR}0-0wEGBR{v@1#0clp>;GBEsa>%SR7RnhN0h>Z{ zCs!a8pdTbgzyQ;EjXD#ddypisd;szi$v})lREq6dA#n9?JGlZLmQZGX+i-&I_9&sd zbK*Y;2u$u=`HvfkRbwn)H1rZFBnsL?USC|n{lEp*bGIoeDU9f)U@WW^|0Xus_GG<( zkfC^~&yzKO;Q@u{2i@b1{@=b*jCY8OpH9%Ua_A>~y7IBsSKYckb~ri1`VJ=$!O^zM zCy^&SQ7?=6oDiZCa&B=8xX@0wXBR8~%Oyht$!<2Z#L6Ik!V*M*QyI6^(;AvRt^Y}O zZsnP{j4%!0toc$z&Q7STvK$#nfW$hCR7yvn$R;m}S!LptKQ-0Z@9fSI`2u4ezOqPd zn8)=wSL!l820DRJdSiHQY6-eXVU)wLZkCRub}29JVy5cL#^c+b2qi_d?U%q6(|zaP zbJ^alwR_%MduG`<6a z2NHO8d|A(Q)`>L6a?RgXuFcCEbdkVbp8|v`T!GX%MK=|dAF97~sT8%WrAAqm4&~>D zP9w_Zm5-yH3=>&5v6<785M({W<`3b!2FJb?z)D&;<9D(fvEI95QLErH63|Em<@K%6 zl$Oe3%JQ^3pe`uzO0^8ZijIWMTZf`L)6#zLpc;TAXOclRh9gj;;-UtbnfWS(xtf@| z;Rz--J)O5dv@Yu^Owew5c1b39bKV4xokhPT!bN-+3Psznnoqe(Mmd3=Y$Ni^h1+2H zm9D|ac66#$(yi`et0|_61s4h{OO$HU;3152vDJgGOhiI!jE0M%dM4hv{!w*=itqe(xM zw8Q@q1W~&A|Ek}(kToX60llXmcFkx9f^}nK=6|1vjH_NmVwp}~_o6@AJPheegtCP^ z5g83$8_7yR?mXl%TyB&zQ&8n|b}8whx@Rue)8enw=X|m1z@X8ex;BG(ufy%nax(%* zPl38RP>UHkb0%z<1%pTfB#cO)YZQh!R5Fg2;>YhD%q>}W%L+qh(P&uN5V_Z2*?G4X zh* z%*o{n6!6OWu2FBntyUML3j#=|EVR&J#-7zCQ!&F+uFYK&!_e zVGEv@&t-p2mqlv8iP04OhTL#USfg}#;sy-b8iSKOb$)Sbwi%rx?QL1^Zozodj{zmdK-_FnYL!Tg-OLib`JoJYiTl&Rr{Qqq-cUw6YJ zWyh;LvU1I`L2y?0_U27bN(%cP=lLUmZ*P*ISoYD(<7Gfr(A$DQhv^_(!P*0*jX&gN zt%tyC9m(TunjM@rGMlmiH&1HLfTwvbi%w+&4nC$s=MBI2ZM8eA5iKMrb>WtMvkU(YMXf#pqHPB# z-RwPZSd)PyAk!Ju$!=jWvHT8`$5bcxCX=2Zf4h|19s#omH%^7j=akcd^KYp({(GIL z9{1m6D~)RTy*nP|(r>L)`=3V8weSQrDCgcrK2NUIGg-3#{FITsNJlT|_bCFyPBrF& zLjsD6YgXk6g{{w7-|EK?c0#dl2s4$!oPlQy9)Zow3HLj4zN0M7Me48p$(aGf;xG3& z*<6SI3e{#vQ_6Uj1K-;$9C`yOJLEPL8SxR!<6UL=1Uv-@rstHY{1Y|Q@G;-3$d;!P z5(>L8UI%v4@U|B_AV`1_639vOMWFi6SxO-uWc2)827S0v%wVx0`b>KMA_yK`KF)kU zGwpM<3T_Var7*4Zl75<>A< zEw7vVQWfPLRFDUK#xLS&&r}u?$XjFRNF*(+=7O!NiVmWG1A#0&-Ii$RSWiQV;y*#R z3Q9zd-H%>gUhqK3yu_^c#UYgzDYli3ipug81QLJxmjM_km`IZT{%Q5+w^Y69!oDmw zLv%;MI8!D}!;O%0LGMFxXrJdB8p0oN8^c4vW{$o6V3~ucj6WSilB(e~6mmg7(b3Vt zX!rO0;p#vl02Dki)A=F-iCS^czF~L+N+O5>k98>jzV($0OSMZ523=w0D5xFYPMbRt z0Gmu=b>OV(>?c@H){pPJT*5Ev{+2FNoB^M{u$j*ofcl)KA^W9%)!i59NY;kw8r}h_nLY@)^ zQ9oZ4A2DXV*G3kE0t!*sGpEfwQxYO9C^c!ed;1Y~gVqz}+Z?9N?wLfUl0Hay9P%nJ zM+ROND(n?YO?D^q^xVg~-QVo!Q`(%~3#f$WDh%z#W@1~h`5hrK+R3bco}Os}ucplL zn3JYF*CeW|`G4*tek-)(B>a7nW8J*6nN6Ov&|s|4u{7XVYNjv0<@67NDio+Q&zgJ6 z0KrZxyx*-FWdLOKbOUO|;ybpswYh4rt*rlCUXJFx+_WbF39a-0!2wv6b~s55px%!; zF}oOr;_95cE3O`3_<4jVUHAvR>R`@tCRy``n6Oa)lvQS9-Zuy6nD~e?SOgj3%MI5r zZ?{}lfU3zDq3hWKz!6BO)MP%XHtSq;gJ-gZxPsKETnljF=4)nJ;M zwEC3qhK_AzipjE%wZ5Q2^WUhPf|>{SWl$BSpw|>EDU^a@i*!I8mJ52pkZNg-z=kwS zYD9$gVTwv}40M!sprKF6dAwSt^uP(wRfh?T^dc*Z+d3vk;AaXSeaEk|%k}*MwBsoVs7xy z=vCtbNSZgJQkfKG(>Kdq{`R|Bg1(fuq?Fw1ihlzbLH2j#${1d;Qb!+^6R}|(;02ka z6x5Xtmq04@>#yC5lYH>-@8U|n1Jb7l*xIDdOx{2WHb%Ik0FEJ)Z7Zblr=DS=iNjEi zg&Uc{;zwisB4MvI6&&qPkfFt*K&A^^*d3`SR$qjWCzf8|+SX;Kp(z+d@Sl z2BZxAC)9ofS%8CYFwNW~9IMNxZM@%mA5tGx&cff{fMrd|&HM?;<#er)>@TW60X7cH zfJoYAl<=Yu{X!JnZal}Y9gFXy`j(qL&*#@T`CAX-ri2f&Tn$O$d9Zxdx{(NHQ`*1f zZd~!+G6g1bvvA|f6#!otzkNz~m}*QEL8Q1yTj`%s(wv?vTEAcJpEY_lS$@*%ETC6J zS{S+k!zNXrrEzTp3YtI>@86hJjB%Or^I+9rJoz&6Q3wUg_(;6N4SYBg0Hlk4wr=g3dxHvMbr;wyYgYsKl0wYxQDW z{J!>C8=G{?>5iK=edrP;&HF}(1UQVfyDgk8 z?_U&pIq8^gWF}#w)j93gKP8_ElavC46z-j$EUR_fgcQ0BwO~n<9d<*T6ji zr6FQ?WzXQh`o_#$m@*`FoB6Lp5Ks?}F%hG=_crpG{NI6Mw=4IecyBV*iq z9`>(5Gb>9+v>z-6;uLx6zg;4O`N{k&ntCnz$TROr^br9VXj{TwYRTuX8a2M0#{e7} zPd2Bn`p74uKeSqAGrr^=mEZEe(UkhhJfSN-#Z&`Sj1f;@ysv3$C$*CD&h+FUhY$_dgdF-kNZsM7B5H6P z)T$)JDIoa@&eFnrpj&es;m@mEaN&-|`T_}!AwwUb0hoOUm_#UIVPWO+F8WF#4s(hD z+V}<-ZrKOdg5OpTS!w=@6UM`d-dv3KE&QOSa#%$68K2h#Y_gIpJ)PjGC;s_o<+hzg zKA?z0ael`;z_Q(60S-g`Aa~1RUVS1w)wXNi`eW_Gbs?}QQbqIxP?g*B?dfV7$op6i1}#7BdyDDuh^%vyReycd&4>mi5aS3S%UBJ zHi94Xp)2jw1M#YVx}f9G_n=Atm4I%oE@fp3W9w?RT)u4Ow7Z|Dl=}r0%WqWP2<#Ba zjCem-mXRqJ?t`ybgCg4LEVeS}@>iPZ9{5Q}=?;CZ#sP*bnyc3*brG)eL-q>mnU zbtu$e_bf4Ukr=HZbaCWXcOE*wE16+809*5`Yk$1it#gf>Q&uK)HoJmv8?0T}+V>Zh zy+b@gR z&dXRm!`i&b1V~MO%($o92mDKgn^|5QM&>v9acbu-6}EDVmMMy2Mif!~|GaR2dxR1r(eX zePdZ>0f*KQzpbY;B6*dR>&EZtBCj_jMQMA<(WSpKpi}m)2)O&tJl@Q`u0WqJ$qyjo zR^{_LV&{qXurL~ClGXjV7w#rT4)>5tEfYYbx~r@XSaad`WxFltvlbdVCCcox;nm?Tp~)ld8}?02oO0{Z0Ci}PF5u`1t{siEEiram^8#8eis6 zRydrIQi>5uikYeYzt)_Pe36{2?VRLD>fp`7X8d@Y@q7_qCA-b$>5WG$0$;4@uayxD z#Eb3aEM7bfCmw<;g@T?|%V1QoPDMqf3C8#|G-oM3UGJf-zc4}S$81ViO@=eloGZHO zDTuSp1?P)4VJsNV}wQ82knrTs0WId(Ij~t z>rMjlI%Bx+m^u9JFs%462tSheD#Ls@#N$|y9KYd)6(BT81>AH*J3JJweCW#lKv(Fx zEtLcxfqvs8JLob;BSeQopGLmOoYhZrQ@z2d5=ZP)^|<*sZq=9o6yx%TP{8TX#0lh( zb&UhWzz3CFv|Yv9+xu1h+>?GY1Tof6bZL` zjkQd|%cPyu!pb%t-SHfnoykykn4D27jZ`zj@mtp0;nLvq3C|=G<~v~_?dj(nq?u}( zRhliS)Rg)fgOwrY1da;A;N};h|IY$oT3vWlw_?NgII=F{x*KKX70W7Rs!aTJ3~Eoe zlCi0bv9GEUYo==Zob!g)pIaNg)4Ksd@z4mB^cO`Id){CyOjEaoX81ybWp%VX@wgG@ zkY*nuuI3iY_S)A8TZ&jqv-&nw1%=U}HV2SBk4;K-Ci= zOoWhbYEv4hsf%{>jEC&^IJK=xjCB;w4=0j6+5$9fAkddDKFLHBI*KA}6@DVU+~8)> z?#o{<9y^t@7$@EFw;9u`304h3xiFO*&u$TWv-7>ti(iHw-#?yzvMR(xreC2#rkaVr zcoDWPF~gb6SunrRVn*oFX;XQV;}jh$uDXq1LWxYz$+h~&|HRgE%_nZYl8)f|`?w9X z{+eERUmJEyUf);Egc!=@d#mLup}fZ&t=sGY1fDhG>PCEjF}_7+?c_g~<9wS`bPvqL zi%G+~AJ4k}_bRa}YB{K=sMNQ&w_Ae5_PzZ|^q7b~sL8LH3l6*-M5xi)VCQpqa;<&b z#Qx>yo^B0M$YfM)pc?fT8AUhuQdrHWw=4g>yLkQWrT9JLzPG{tj$L&AMAGf;+Zst6 zQjrj$sCs^vebwLZgukm%wNZI&u347aKkrU-*Rhg{`H+63V1J^GL)lK|?dSYEwX-@Y z-|#w0WMd=T{zj+UDC1nOV*lyxf~O~PVs9^J@NX+iX`noI0=!-DIU7QMS*FRsOWg0Z zTcjDA{_2}R&ubadkUv@?`DX7)kz$uTcNXZryuMJf+R->IRmG^)^Fm+QT0L$^cswPc z%pA9TK*f+|vSRb$e1Gb82fd3OfB~M?Dp@?*<^C8IBCDlE4y1<&eN&oT-^&`pR&@f-K z-%q`%p8N$ zV!i*g(iyN0iQksfP?a))^{C16yfm_>n{y>Lncyt4Nhav0=ct$dT!@sHh3-l7Jr5;a zu{^hFjGRNTQhj{p?PHZD4g6M8n*I4>SSuWw;>NMN=2mhQ&TI2bn|bgNgkW$6Klzz-dRJ-a6~&Ptca*_KcmAht=vz_Hor}iv!0P*Z z28tngT^GEm9qoaR9rVhhaF$T5{#$)vQ#JbaFG0Y%4+l>2m;9#C-S1Rf(joqb@BQo@{ zxZS`2T>O^L^2K*+jFK^$Ci%CTrIf@zN#tT?luC1R<;t6$!uk1t^Mp`FvU;n~&+Fw* z>cux9C#IX%+nSmfvv-~yQyqyXI|`yOuV`tc~UAuhMn+4tUf-OkLK!l;g^^Ib61 z?l9{1(LA!O(eka+C#W1XsPb?xwh{%Z+@q*iB~@xOT-a#Yl=PP7=hh!eBr-KWIgv7G zn*G@>mYwZ56=hmUkN91)vg=0PiP*;G3uyGZE<^8VCzw#>!uBzisubmic58A_Yn2D7 zZas9GqJ$|n5X+hwt8R5kF)?hMiG>>rzj&Tc7zgUOVX@X=$oa&w=zZxsS#GxEUe|f* zRd|mIJ0ZLBfbJf|;`D22=c`%uinpcgQQW#4gZc7oDy=HOQ{Ot~Oxih55gM~9V$W(< zK>zwd*sPE$vvA@OKXmC65D;K1Kd_%4ek^4pv$r3mHK_Mp8m2y`NmNvnlsBaq!YQk4 zd%o2#zp_?=pGz%7e-5bIU#f-nhKg-|sz$D0@gP;U$92CNqe5 zme1?(6WaU15g$e`Jp}U|E@a3YK3c9#EPR;^?=Onlun3bkj4nyIzQ0=`%w}B zXSS@|MXixugaP}R`i;se4q0yH&xBY^%dK>`VV-eK?nx564f1CiN>x_{6_$uiQb~4b zSlZu1hl{B(_T^%TQToz`cGwgXV>)k(OrqPyD6NiK2xZdIB~OJhc2i=UJl_2&k(0D| zI0ZNf5OmJ3b-bOYQ~WeRs7wHB*)wKiB$;DhG4KB-mDYS9<$?xw?4j{faKOHl@VG9M z$*GCv<9mj-SI>F+g<3b+J*BAn$BphkxG&$5qZ0Es+tW-tc3o?YZiJllnu;!H_}o`+ zchp0MTg#69nyufaCo$&?R#rmfh>ZUfzP#@a7k!X~=rKtYW$-fX;7D0qk5SN#|A93^ z`t~;x`~1Q_CJyP_OW}Td6LK*BP8nLjoqIcqiPkWeZs%S4NhYz&6Zj95g84`p# zCukc8c2VR^m@+xt+qQJu_yffRIK>%r?FIc?8yrPmLx=^2Q{(MEg5Q2_t%w)?xhX25 zoFW26YGW!b5g)NCktS|)m0V#a4jb5dx!}W6DWsN#0fV6{=aA>~C>~Ge)q!eDge*|s z%Uut=>mVG?s}7?b3!hQ3zo2h=OCPi@#`cB(q=(Eq{yP+Xg^!R%1Q>wX-M z+(Uk6E5yW77oo@@S%LTP_t=m)osDWqUm);|EKiIeNx7nj<5DcR!YT6#S`gM7h}Xs> z)<9r$D5x*JI90>5R6p@v-05O<(%yt1zx$eS+u^YdjbQ(gL|~sXGCa8ARDqF)hbIli zQ41ED7ut7R_;1KMIEK#k7y3K#!s=NndWH0MtJII-3}iet|K$Td@)1u44F5tcoL(xV zm!ngp?i|M-{#6QQdAbn0n88N&n;#XaW(}>ZE_OMPFT}s+E^xPQ?stJXvcA3e;a}Bk zPN?*ZvM&r!wSCUjPil{91)@abH{-_!P7^AfFGmT?rmq{_fp59A`4B6DvHW^-EezL_ zO2WsU`n>MUaLyeD1AfoilTs}8z182e7ns04cqg&(rFIqSMYj76rmf=S9t>*Ew|9u( z!fj({M9O78wl#qWb6_0xx7N5#yxDR7vpaZ5%WD>#Et#}TVC``IZCxDikGUwcuQRM0 z3F~HgB3{Ivh+u;+39EYk9h#BYvnV>*ugO{iQ8ximHv~~Ljx{$56LM_a%l=)T3tJhI z1;=+YN8U4zL@mVdwNs-ZOAfp<&fjJnT4x;COx|U(8q@Z}lD)ksl*Ldo+>N6mHc&7xr0Ff_toW(< z5M~Q40qcdg!s}WAucWkZupABHw5>c0fwo3o3b^N;?@k~wSR&RW~REMs7~ z*}l`=3C1-HjtRFYjp`d}N;%5avk}XXRhBxWd)jn?E!WFTb_P z3GGYDx_W}%ZgP1dgw$)m)+ST@t-_*1)fk)ZWiu{^$9^DxyjAwhPbH_92<=FQ=FAEk zRuU#Zd2%Q^y9lPGe}+?Q1J81%ef#ENOO~-d>RUl(f>yCGJL!KJDwtz>i&;aGq4Lx# ztuU#K12jb2?`+>xSf4A>bKbWbwGz;yr^l*QFCuvAx+22&BybO6^FXUiGcEa4U{Lkj z{pKaFgLOe;LhIf@Y@R}oFhlq#jz}!+NO(gGd&pBX`+<|_;|#0>Bz|MWx!#=)8F~UN z_*}ez4do=D2VQc-Kd|B}n+qonZX2*DYM}tivc;%&#Fp=*(!JyugnT5tkD1>3uS3QA zH{)vB^Qts_K-9#K`S0Ux#?wWM&Auw8uXX4cP;wFJC~jB620y~xF+=F!P(Js>x)gKE ze4ehV$a+7B0fQ!DgxR+r zQJ09`I5Xw;3Lv@6Ix2z}(vs(v4^Jd$Y=OOd|F%3*A2BLd_6^Ijf7i<%YkFHPRP0rb zU;~35LI&@sHC5KGA013xSq_$nH9bA5BZ}a3UuMqVz#)0Lg_0YJ$w@jZkVM{9Ezyd8|>ofgpB;4=pE*Lq@`rdvW5 zxtC{OU1NLC?# zroq5LTOf0y!_LbRF|6F`Ei}{@JiH`T${%RD%2Y4AxaC=6$==u=9sF?A`Oa?JcR8~@YcWr?vyd9&5W>gN~3~cR*cHeeYkngC}@7@&f{5edqHW>nM zs!JX6C@(-qlDMkA<$MZ@ep)eyoSsQzwGW+KqazzMrGmDDNwgzd{wD7_8mUTM1>Xo? zbW`N5x^dJK0d8bS{q_Nr1MfIeP}x)BX&Op7v0#Nj%FD~EXcX4XnG`+Z9JAu_%a{kX zZ|dMU}FhRQ5PqWHQiF}{WRnW&WyjXEk|)a+(GXD50sMqLpyyZuqK!E>OH#S!z3V>NNSt1@vdo7vtLHhZRe{|f1_gr)%I@- zSlZhD*ZB4Ny~7_LZz=YE4GD9>5G(-V_!FO$?*Q$7j^nG7VA=d{&a}Q%Uqr%<#fyzL zb1zJ%$0(D}a9|b6RM`X+CN)JQ8_i$qLYZUVl$XLdX=Pke2&7U;bU|`gcPv;au11Gz zR_GPHDhwuF=09;+2#YRh07o}K)Q%Ow+=?5Fq){Uo&&n2VS8w< z?XGgC3wBfg(WCFI^eJYta7(yIh&2>3wBNerMI^B<@~zc7Z5U^YT%FX?ePShxv_zaj zk!Un|5}6J{7gKrvnWM{)R7oq%s}1YJihX8R;H6FarYScxWww8si zuFjmC2RN;s#hoV6LM045u^PJsEG6>o)%NF;X3^Td11;Hd-;F80yI` z-wA*u+XdF4F-!LJ2B2}V99k9!*+dt;(XCkgJ80LUZ0qdmBpgRR>gQ`ze%z~j1-?vk!=Mq7St%WeWLhOKA6>84^u0m6d^)pbA_xdkHOx~+r zIuI~{sA+TXihey_A(s893NG=0Gvl~$q_SY0L#(m+Fij146=;zWLQ zI?0nLJdpX%)4HiB%RG|@d$lj0hWc>D0x?4%1-Ije(C7j*+z}>w*-W)AFF7Tp(UID% zW|!w={p}_|Y>kpUL+yE~>HhS3?nf4Q_xO#+QN3)_yc%6c4NUZG@^`6;0{u5&)=_KU zm)fM)`A@ZsXn7iKLnveag{)olGC}+Gr|8=YnTSYMYH)%sma9;ogRHaF>MSE+*{AS= z>6E31I7(43N^h1q)YZ?hLbI30dKB3zEhdMA?&nTqQlH#TT>nu`l9M^eCF45&mm_RS6Tqx}T=o&8MrKJA)1C#s3N;T{JNi%#wQYJvS)1XxqoWxJAzwS$Y zHn-FST(j)dd|qnB}9?6+_Z#>O6!PoIr8>=Nme(VB>; zV^zA-kV5_X7a9gmXPt!6+$M8(a~ewleoob`j54 zg${hrD6J#tk4}#ec`O4#ZvPAjLJ5xpt`h*=~uY{&%R zOJ$4}y8<`Kj@Vb>r*D=>sZOsnYi+7Mhm!(;mE$`QGH2P<4VGMh$D}ucL#D!&WVH;q zg>!w#{{ZQ8Xn)s?>ohhU+jN>b^q3Z1^eFRc?$+9g6jI1AT&}Et5{bPg5zgQSdB`%$ zj;O-9JM~Bk2`2eOpSa)Zsu@kyGc5aGQ}buO@mHFCkqx&{L0Xk#BC&YKoh)oil?Tq} zNoHxm4!V_V$uTt4>tMkpctE8Ow1wyEb`|2Q*Pt6bZW@@23a05PBDM4caxc;+eAO!v zS2aprbZk14A`$WY3ij?2|0RV{nR|0n(=83A?-}7-7N|0z#HIz_($wbU?KrWq)F*f@*8ee7ri_& z2?-A`Akid=LNYR)%{+!9OqV_O^5bWBLnIeH@W?p)*U!#RyzMU^JYNoPJU<85wbqE; zBW?&h9C^l7`n9WT<@;a7aJv>mW*f3-Fgk>P>f*;eC&wRrxIf5X3B##8eU!Xv07+>; zt<5DFf6Ao$_nt3heDUAegWorpK8=J?k@C_X)bM|jYTNV4c1n)u%2Wcr)O%Syp@^Ru zaDV5-Rn>f6II`9L@V;?Jfe6L=DhuYo_v`(Yci|)-2I~N~`0Om)=PETq(o7F=m;l-7 z!N9yo9QIr`-^gGZF8Po}XY;oEB7zyHW26$d+L8V!r+3JhhlZmAENSjObbgA$OiU*2 ze+(BN;N^Ql2f4#TpOJTo2kB|C{Du#kZ6;3_cb%g*zm4#jfIYb2bwN9t{*1A!;Ty`J zs?BtJxHeb(eW-Tx;zI{1bWANdXM}c{{{Udqd5!;zadZT39PaiFy>!7Qa>oJxAdLD0 zr(aH6;!7tw+w_0tPl71wVHvadqUlp2yeU#(ogK?L23<2k&3|!#?Fnk;@hkH=OjH*6 zv`dmXbAXQUrX4Sl&tamT?mUc|qa(m@9+?%E>DmF0>vs~h)xra~0Loa?90}JEF`wHV zu*_Kb-h0Vl6jh!jr$479fck7Dn$8p$0nT@kW$Kj5z$y(;3LLQ*i|wZ9Kazc7xKq)3 zy$2g`*Vrukqa%8c4WK^X&P#^i8IRFZWv)4OttLd#&LaUOb1(@^Cv^j5g_L1M_@w07&>|>vP*UUH>EyZ~YKDxh%G>u1@DY z`uN6*w&V2&xaDo2?=-vwwAn_|<9{w9e$?Dzg9==H&)@J(qANGa^87mgEk%ECp=-K- zhxPH5Sakp-rLRtRhm*p^@;P0y6^qH}5Qb&II;dG`Q1J)Y*AQx}ZEfZs%YJQN%6!rJ z(Bl=pD?A_sV99tq2K?Ef*Wwq%crzvBfIqT^*($z83}n`_K-;F-v{>@wXs0$?J%Plu zL+)aN>!lR-$WjvZq=-Ic9sR3#-s1_2~tF zoEDoWPTZzB!?s>F=Bs4|*+zXv+|eK7=vV0m$}#b&luob)9>6wq4gG?xKfUPYvx7?Z zUv(sK;ddn}4M)sCax}G|2)`kb!b=@&_%ww`+(@tg0bYKmduXI@(tl`#I`6g1e_f|0N^9{na(m1YR~C6=42`21Tq=% zBz4zt#1O4OXKe<-6@_y$fSdAyZmEE`7)H{b9A=MTNaFI4>pqckn&D-8)pPPRd51DR zp)q$Adr-Bvi7uqk)wi%N;?VtZ*a$$FwOeHVe=R^e6lPz8;lHDQj0xgw5h(FeoS+t9 zV}(JTr{ly5bw4q(?H#+je-9vl>)1Ik%Vp0%B+?WRO57#)wr8(N{3c+-ZFjru2@&X` zyWP8$U!p%!6_+N}jTBVghV~T#(|?(*#XL9%@=~r}Wg@*Pu0^eu0ePA{OwhGu%nWwY zJW5Ii%g~9COKj}e*q9Y94A^3jYzdDakYWt%CQ+VpJ3UM`xu9$xol$2eoy=98svjqN zocvZqR#o$q6Vh9?HqwWp0$3Go%QktL@XH0qyaC|=)8Jvx>W!dudEWxP@78<{G8%UN!{D_B<5rnD-X!+SPOU!O_IcJYHGaKb_7I)snXm zT0wJWRhy|U+vOz3)sQh3OG+eBqz+$pz@cVBD9ULdj%34tLeHDILX)T3-d!?lVbrN- z|CT2lRt$ts_!XQ* zW;bd7F*4=WpLRFBs*?_O6H}*B>I9W}nC8Rs85+&oyAo}R@;oEq=x_!7+z4D z77cpaqz_3D*dFa55`ub)%l0id%l-nXD$2kE`$mc|bCQwp}(Na?p$!t^5AtEFG$-3E^(1@-% zvoOhuBHc%jRx?YL4yE<*hv-TD>Ng&WLrtioK4kcmLD!g?kl@#Q4fGR_P_wPm7@BD= zghCD|5uuQ@0$w8hr(k%71tVQKVjjw51d^vF2J~B2cb7y}Xn)~7Zj!-TeBM_Oi2SQG z*~c}2^XgVgPSKMQvFAKo7D6S==!j-xontXtCW$6`#lqt%@88l@@t66jCskFeGaZdj zJqyvO+~7iup8Q)O3XYlzZ~GCp6ivwLlN7Cb9BY;w;_yCB-idLUvW`iqyZB7_VVVcg z;o&dQ&NsV6gfRw3#rpMLv)A?k6kb~}PP^5Rs4qwZZUx5_K~~cri*U=Q$X;N`h5H#f zt_#4^x?NW)<_riGHEIn4w;oHGrz`3wtI7?oG)Qhjst}*;>&o`L2nS zOs22AJviDWi&kZ3{v?2gVbo+*Y8VJKvNzLtcZ=SRU?N8SApf#c9-i!}2eYD6El(_g zcGDBo;4{oy;DIhzpTEJpsRV{4*eX^9&ny*h#ntL+bi&YcMT=Jd0w0<^%M6c7h-8P= zzy+Fk8vp8X$5~ZE)x4Bx9;Ikfc4og>bBPU4wOq+L@>*?(wx!&yHIwL@n+lX$msmF& z!!Obw$vs0gOJ99NFGTT@oyO2pNh;X`4XEeUW@iBaZ&C(wp_)$jd$NxG9q|Xr9Js`` zn|qy-Cj&RZi}UO*@I2=#VR0t)TG596-q7LA5ryNL1Rn{qj`+aG_3hTfKE$UBHl*bZ zE|%7w#fjXh$53m3%iu61hCN+7x&2$O^ERRE(5h7!}|><*P@|9h1@TuA~{$&csNKzxQFp!|-7 zf`lABOkvfnp$LEsBC!Ec_~@&4$HJR5)E8g6=qtnhIx~1_xAosGQn%C9xJYJNW;oYS zsXZ6M`H?PBOoP$#ZCM*;8%mdm$DoJls{n3%rI^Vnu9$^A%zDOZ2Hi5Z60om00)96r`9?4F?#nkU z4%)C+`LiW9#`_B{3|ai8zQR=G=Ucm)mfo4X{yZ+6xIg#Up)rxY9m~C?idCf0|H6=n zh2N0c#_s6U{bHJ@*TXo@{6He!T+U~Z?4NMQ1?B|497v}n=0$GlYGQ?^v#)lix1Nmp z7LaEuPmEJp$e@zOAq~f?(KY^>psF3pkjAFA608s*rnCFu)HM5MJw?^?P7>8j@})do z3dB&#cb*8uzBH8NJ~%U7Yn?nnP4`U;d__l5bst@|<_~0z(PJN^K3T233o01CIRlc3 z6nWn}?H9_ZYN`0dAsjl$fXaM5RM8vrAq_GBmj;(a0MrIa(?{C9I0e{S!@%B|?KEY> z&R;RU&@wHh3)#5(j)JtJkvh>;#c;aJqrERy(9a;P#lSPJ-Em!RJ-W}A_X5jwtB;=y zs4RN`Yny>|w-Skl+jreE;Fj)0$H2H(H8V3q|1BoG#qn0QUQF#vw+YCPE*D|eIW>x7 z{O?z|QgQCSVNKyhnrrbNPE(`huH4@-9rKZdPS16}@SLb(&|czd=zV>F5`4eKHX3pn zg3ft|2-ZXXT?Y{j1wO$8ea{1mmjxM=$9~TPg)aR^2**?f5>!u@J6<6aR{h()!S}Z2 z8IaB=w2pkJ6_~K4y^!s;KDH4N{X>vA>7&H$M@gARXJVDH-MH&Sd6+$td6MPaS&N-{ zF~Jhlyd_J6e`b9KGft@Zy(*w;ompQ6xf3XhD3_ZrY}FK|xp(6a&(Lfo`kQ7baTgr- z!qFaI6!iK{;KEzpe}Y{~FhFBXLjR?}c+@x>NO7d<4t-pxUW*xrc zWU43{MEpL_hLZ7C$b>xXKj`VBr7Kr$ILE##R2@PIp%6X3_*}%xGx#bxQ28j-xp_}1 zQq^|YZfz~r`akbL%VZ4MroYewyLzs5!!FT?sS8?df5w2e9zRr0y}cVgZQ79Yp!im2JuZxiKoQy z%moin7Q@h?PFaU=%(>!_uS98>V41)lc>ZPylq>&MJW;DmlE+9W)7AaHLHUi>?rmVP z^5A15{dQgK_oF%pvp*K~8+<}ev23HgV8 z2&d>mF|yev{Ed82o+7T@~j!x2J zyVpZGH#zyQdRs-8_IS}mKzURyQj%#}18_mexTgnbhcpf*DtJF#^geE0`brJ2cs;D^CNpnuVHHENZ)9JIt zlMALqejFs-A~|adFN8(dJ69)3yw?B7i{QIUqJ756LjG25nntG=!(SJxC~g*TW=1m# zY@!U7F1jv_cGx;+_3e4nc>}nri2YyAKn5Ev)&vfOvIU|{20yxh-7Av%XW=iO*lx5ugH6Sw_KnqnO_+=x~uxuQoo-N#f0~ejUAZ}z|QVyH(Ps9N08}j-pugTY?ote zy7I^VzE(|MM)fI9l`qfVzr+{krnmLnzRIo9$vQ=6IM=AT23LG~wsI zR~)C$^R+yH-o8B;Pch$U+r(~B0mK#N5SK6-&Agd7tX@PTsaa;bbjeKp-rg+yp#Gn( zsGVMogZB}>hAhf2nt8j~fd{Tz7sG)C<}AL^nWDMKYPV+xF2bWU$|Oyyo@l89?hB{W z5DOw$^idlU_4TwXeXW|UnKyrI9O6ch=7q`3Hgg5f4mV36&}jA&Xtg_Pd@Ahm`gBXARU-m)r*9`^Al*||v+ORM$di$*CH%V@T4tXOPaQXq{cAz%E|{heXM z?)Z;~-BJU6*uOi#k55~;x__acWcG!Yc;0Y5KXgj}MWTd~kDe8py5#0_L`CU^z@!(5 zL$;a~=Y5J1MQJI|%Q?|Gd6!VtuuPDbVrEkxERJ>4qB0*kc7J)L`4${AgC77h9TUi^ zKne}Ro_F(E8q2Ngeckf}Cz^*$P#)Cd1c)s8Iq-h}KAg_c4}PSmKq;Aj0b=5zagF7z z*zuHp8EHz2cTfI(Rt?E0GXxU3j9>3H09#Cmd+>tHmS@Qkz>!fmc#$hN+(Mu~%I1m~ z#bU6-q~EGEOQX~_!nPCHfTsP{;R1ule*@R8FBuRO#5zEp*<^jTwCsJ&&N`QHbCl*R zAv^v{uVxsWgDwxCB0kIS@t|*TzSWd z-v>9=!tq(}qzv!zMW?(5KMvrAe> zGGkyQoR!~p6KbJGl}Mf|>C6$9zC@4zTJiyFylo3Ga(%IfMgE-yj0NLB%RG*Q-;cv) z#h(5Itbe&MnpSKY01cOP&b0gX`(rg7Tt%pC?!(|Cc->J6vZMdxn9f6bVkJ}dO6qA`wPA9gGkj)uY=4GX0YE94cp6c=zX6)^bND*~i<{1 zV>t(BK~>h_c@@9x2FP3P z#Ze5YeYtQ&vq6Rs2j-&^HC8t4-nS^*Z%0O*R*A)JGSpsV1?iXl3 zIR=Ud93f_>bWYDnrO>FixqJxdemGrKO|k5;+Z#ew-6YKqbj&f>@nJ?p2Iwm%LTYUB zxx_;iFYbiHpz#Z#?ms!(RV{z>)F$g^q$Nd(8iIOmfv_N13r?q#xgwqIKC^}@v*6G-;fbGRXQ+lA)GN9!@iY!dj?etQGQf6V9M?s6PK+lC5pPfx& zcA@5e0QrqC5NbMZ5|<0LayoHaBz^OPju00&VtM(bR6`NVV{Bw(`y%s)=bQk4VA@%J zzyaWAxf{nB|Hv&w%yLJVo1gzm)>H$jn^SzzF*P8BNd`)ciD~stSv{_v$sSe~94xy zPjJ?g-2A5>JF$ikxamIlwYjG^@s3OBf8epwEL958nL0u>jK!oEsU~|h?7K+qm}yNY z%0CW7hyfTorZ}+LL8Frs7oHpx(LXlAXplmwwybGtGyEnL2X`+L+^ihj+%KkUSHbw2 zKgWIXbxWKBrTzdNq0rh=n9=;JXQ^TX7Gue&V&7?ha!%JyfZM;sK#BSg5;`(O-&aKO zSVsB1MeK~cuj{l@^%_t-PER~Gf&jw$RhZX>sRBsSC1-L>mRH4 znB-KMqSKqfbq+%cBC59G(f0_^1pFw*VVxniyvR3CTW1@uf@IIzw=mD=dm#>HI=oj23IjQz zH1e^DxmREBmYt=&62s9SxCa3B#5h6?anVo>0-vc`|IErf97Baa8zY*X8EE zQ%&DU4+2?WoZgy91%Hq0diNc}SRu>{|Z*x^f(n(L%fE?zSp%oV_K zlmF|gXP}Abl$4-iv1Fyy;ss*zx4{OyYRZgmZQcLtgA3$=w=C4R!XWKj!h-c1GsbEY z9{oJ0$f(-<-@A5DV5MN0^Pl@^(Wq~J@^m{8&!j>`2Ndy%v>3tJOAHGNAxbF1fKUwC z8@m_{`PMyR3VV9+4M7b4$7^}Oafo6eEY+OYOixjTl$S2Eq)IG(l*s6lFRVWwduKyw zOlklJpSH`^Ja1U(+CVEx{j||k{xqsTH|$=htEfX69*KZify>HQM?4yrV)t)}0I`Yf z;T<)Vbg2M|&$0Txj@@3F1>5x2H?I$#>Y$BXfu?7#q-oZoI!n^y$Q2vE6#!0t=ka5| zD3eigj=N$}s?Tj*N-Dna3DlxP?+QLIFV7J>cH-olnZBkDPGHSNJ5qsf3#vw!2~YXv z77kQZEWC^2BiH3SX2)hHS0fmT`zIKsOlBX=WRX;8zHf`-@vE6VP^_SSKqCl+xmC?x z-=r9FhKCEdqtL1O$EGPm?Y3VW)mh9!YjhidVONvat`~d}ER?N*Z1D2O{4qA$z8OBv z^Bl(}kwOTIeUDsmPT8)QrX;1{AiUVbiP<3=%AF;1y?Nl;+Kb&n%HIk=ojH!KwThoD zSaLu(dE)u!_rZ2q^8mBjv7Y|Mvo<%CyS66GV)JbAW3iD_3c9_GY;g5KJI*u71uP1y zMY|z01ye=QOF?7R+;FvHjU%Up^g7AmvBcRFe~KJKbXU&=9@Dr8b@39 z=SDttmrava4M4Z}P7qUrV~i&m{{t(kNY>2k(D?CJ)mrKuldZ^snP`0s?L;f4y>ct+ zi2AnmTo^NC3OH=|hkvBk!|Bc*@=<}^!@FjI&N9o#YH#>zF!8-tCt*-KUfakf$f-MfXE9k^?M+`I-yEi*ri2*Zcf#?W+Bw;n$yr)E}Ot><&U#2}y>|z3O&{8rVU}n(wka#7%N0tNiXlHPNANH%%y*%KzNBF7LDOgqJy&W`FIb8SZ<(u3n`?{X363v)mJ0Ox(m`nflpk#|#oXjke@ z-Xq7ZoRtbe=srraYKBu{881&qb9n1~NK`Mxy;*z9THQydvV&`suRtecEU1Y!_E(v( zaF=*oJe&^Pd^FKzto*J1S-cyK|68j0E=(@9VA?;_+}yf32dIEdKDP##$`>zQr1Py< z$ay>6oqCem^-k|6lBmJmn23lIibx~6Aq@y)oA{-(4Pa4^4dQ1S&FB8{{+Hu&K^Xi_ zxnHfmeZhT@+RIY7t`U~sqKB`xTlhXP&+SZ9-*D* z&4+5S7CzsQZkr25h0cJxJKY45p37z#1saJU0O8r#Ze-%LGh7CIC{Imdft;9q%8U@RuQ*Awo=_ zcjp%93)y@Zc_W9X6}{W#Y7`q4veDxLXePJoZY6r>E0TDN-1PRKIo{2LV|SN*!RrTN ztNGnnlYr6u)7G_&^G_w7O+se$c}gTNH}M-0SP>E!G@BC`1%}0On1| zvrkvn33k^FOm-CNm&>G+fov7438W@GrlW=ZUr$5PlFNOTJ1aVY+K%T0VgtD90Itn# zB==i3Zb#Cg=$)W1u7S>K$*u%r5%%+=Y}>%Vs~=hR4fmVwfS}*OWmpmT2|BR809#V^ z*RpXegY{bTg&2@sj{%X#WSV7Je=PS{(^>_{JC(^4OloEFb@%l>pO=(LdOEJQ?Vlf> zb2hAcM9Ve$BMqJk}(lpIlce1Suv+#bo`D^GyMchv)@gEgRtM-AEcmsA^n z$yghTN5X}c6J&!4DnVL|3~^%pB>aBta;BE(YZkxODEQ()VEss0FZqZth6z)N44OpZ z8B7*LIYrCYp30{IMQ-KJy-B%oA<4Gnin^9OyY27&oR zswglGm}=5gf|jh{)zYRI+74bF@q}*+F_oP_xM`mM1@xShw~=HhtA#jmulT2Jj@2;G z4YFA}A$lTtn41c2);E~T9+B!BLp8M>7}CPfKLKVw^(0!=?Bn?oc}&Upj(Bng*Y=Gw zM5z8F5I^u4o%lk?qZaqh%IZY~m3}qBYOoma-iDupQ}FaD4BE-&5t10vpzR_+=CF(= zG1$0NF}H)S9)>+$rC_<6V(;xF&_{re-?A8g++PdU(u05GdCm-;-Jan~%reTTr9ILi zXsB%oLa;|{$~+Osx)E-+qk@M{i)=k@4n{FG1x~x+A+5RU$V!N+B!$AzX@;5yxL?&l;X_lx9Buuq%B~kmjtKr~ZE}fJ`(Age88KqaOsz_jY>=-oSHUVNGVS-S;NzO@%ep|C^Nf<#j#jTFY z|Nf1d+M)_DiI&W_O(naOCVt!wh0!ivlDsi(ki#rApRhs{gdz`A)cgn=QQ(^Xe8gkN zPK>puGR=gJ}X{DH;irJsggFEfK-^RED?@9P+;E=KT{KjGGgOTZeAr!97^dpG?| z(jiP=QH3TBpB2gwvOhdWh5FC8%YvCEZ~I28SCZ!U8N7-xWKarNk@s8t~2p z64+KT(2M=UQ#EsT#@h5ztVWwsX)U5Hd<_B70#T(^R0&{7yLJ2)C%DqLxkA4MtTUY z=}7amCBTy1GX6eN6AReKiX#Z`JzIEJ`KTJgZsEMH*6OQss-Lf!yQ_edukb(QqqH!6 zT8bC~f5;;=Fr5gM{@2tv{MXdn@q|Uo;QI-9#w@AXZg`@w7sxBt$RQ$S=Gfif0C1~M z{cqPzuVY-~w0^DR(j` z_1{x5Qjvp0Wq*d0qIuNFGkxaCt!(n5mVEy0Q|*U-^n%4K=<`T`SZ>ouV?yN_d1P0) z8ll;j@+B*gKZ5aMDL3yro^BmKqU+foZ>r0y#4S579}H==K#eGJGzEH$jQZI`RUmhF zX8AVVfiZ~O{FKIoCBGhpyxccg6I6BB9L4_6nfc^tMw`hNK}qyn?wW2H(>l+E^QF;qkqA&SB6Bn;BAp#8>Ji4)oI% zO9>c5Wjt;I=IzUr9B1_uKVUdVFaE54y^yc0adQg?M;2``ImGIsvzNNEtH#T74l+)=ccRC$BkrD(kw+MuA8y?I?+&<`lHPaS zig%jIad>VdP2AV+55ukXF$=wz4x;FGKw&%tj4Ho}Bdt@$g%TdWRXn~^kW_iRp}~|K z1Kn}y`L9*=-~+%5bLVQ+ldsl~^g~Y1uiq0%69D5@XKf{^6!-2Us$|%=tl>+2|2S=!CWQDJK((`Nis4q5hEI(Yw9Yns!`Uq^W`vO-CDcz!n&q2 zbZATtF?ae?lriQcid~PVZl5N6cxvGF`Pv4aS#gf0WwN&ZH}F z%W}04Ap>=(Gs7kqxr{!95Hyt4-5#mrC#;3(;u6-+5+^m8%&7Zn49pOrwdsMWP87wJ zK!AI7`yc~mY>(~l9zxLZK)&3Sw*>#8ofPnH$8Skhyhyt{ugDIsX{_jaf;CsVW@$aQ zf%B=nWppjU@x9mvXr{U45T?I;;v<;U1utHS{f84%Uwjg{cp7*6g!D8>m~TSqsRFge zNaw>?P1D)(oJYIkczYn;VebUyhbIRXPG>KY5iA3e%%N6j0DI#BgJ+(aKA38$F<+g9vpvsNQFROT`+pi^j%Ad|+0G z`kc={j?Q?yFKw_783aVp4Ot$Yo;wNsQw$6;aKh4Uj`7+Kgod-_RaKZ|^=!G?^oK(i z?}9Z}z-S}|J#dv8_+F+_Ig(CA2?ayt*@#dBdsFDD%^=i*p#tNQsaCfWWS_=e z?REVI+E8TTz{bQ15C3iIf#|c*hQ9fus@{lsbyR#dqB8ZEoh;A>1J1N-zivMZjc(dd z1pF|(#W6tbBRlKAptMk>jNQNKpQhdX;dJeefYS!kxpfh!zUDSEtCMT3P6S4v(P6@! z&QTqs^`yF;633s;T1`bz`F`9Q3XuN?Vzi?2Eeg_QhBSr%2Ji2kogI@rV<0FcZqbVH z=i46`C8W}7pNoNxG6&r(Y~7~^uwGW#?MEVJMC8SGJcfFhCR3aQB$2TxV$i^U zgaUDxJLlDb2@Jic{bY1pcVCDKLr;2vrab(*{&0Ue0X@O)73o+el%BC^U*XRXI3KhE z0N(UYqAC#t0F1lm z&=*&0u8hYv1ca@;fA_*UOgcfX!u-Y}Ittug(52@Co3O3#{j7KF$#2)_7RCEKg?pW)?$A=EoV}hR3UB=%%p50AW2rS;5;OWzN!3^_yLvUk& z@iR?BhxHcCMA`x-bMhXpV!{(W&Ug`nv#Rwi*rVPrPVpws%laIL{bibobWRja%NrbC zTgcE@oEWL3@`L4?5nS*blpveLKq82D40{8xCjMs$TQv$I%8J#{=f%8vHX9s1TMn-W zKjg)6_%=aYO2%ekPY1AV5%{E&Wv^^hiojaPan4;WCEB4FBDktlCvd_JW^D3q+s_{v z0g&TdhM$R-^+lKsf^?KWQzRgKZSCu;h~p#Y*0 zJV=m^@uYPQ=7E!D;qSy+aT&c3xP!6WVcsi8L(ZyH#eJ`sfFwK;Lx2ln9$@wTpml1}Z>E+x! zlg}Tq0J`}`IUZnlWY0faGhWtc=lS5u5Y3{RB?N9Y+^;Lc3Ty3h-{aSTn375v4jhkW z_C3u%(J`}_aVJmVM!%g3MEQ1~55*Z8Gkd-pyq8!y9~ZGt#W}1o8Lsb#e1AEjkjcUe zPG2mjyKR7cHaUpZU~do@6HOA@+A*QSj&Kat8w$O8GB^xs^7#7bvUs{xem>hLy2tyD zugl9nnNxI-;}ZzU7FHm10gUC(+oZdDm$ohkGFR9XO3Yv_$f@c%3P0`lOsU5Ow4%k3 zRmDG6XMH+U%S**ac*Id4B09`uDbaJP>Q`)t;lVtIchUhf!=^9b=m7S)9O*R}5Qf_; z47xu5&}Oya$*x^*T?n)~X7HJQynR2MCWv^(AOi%on8d!VFxCQT$2qN?nN}dCj^im# zH|KWA%Hv55(oml7X-A1N4A3*CQXVKq$qmDRq+X}RlKYsUYD}SIrLhBXOb-8nR0?{0 zO<$G`MJU|-4+9jj59zZbEQ&gH7JMR2yDW)L$+vG@p>Rby;qr?+UZL3VN3w?(Xb%~u zg-k^_JGaa}9WGdA6K@!o3m+|yJWPU!+Oknd?*WYT8c2msbQh()tbwEy#yB3N5O{d( z77pOK`L?%Uc=v(+JAIHYm-O0#<-&-pFgB>vu!UYnT;VPGp}r5(SAI4Bb_>Kuq;WxJ zt0V_l0PLWHRRAEH(~Bop8k9qUFhrM)TfKOcjAlV=C5wC%2Xa(c?{;q_^d=gw%$3Pz<0pvB&rWaV;JmN?aH8 z|5DM+`;?FY9u~8$mE}gyjIEHLA04G{xdEyV&ojPK2f~39FnHpS&oRW)ldN_IQq&0; zz}V%lp%V4ACqkx=Qg(j`Ja)d>WV%mZU^e!F`Kz{^Z{8?*W^dma5SCc|8K|P7qFsR=_~jSDD3sW{|6DD{vCi8BJ&CGT z7CHE^>Y4k_k}(wu+s+3mcvr?oOUc*Yptf;N!++5ZhazN(sm7DWksMGz+(w z1+d`?qv~(iN@9mArn!VZcov`QBw$lnKiMM$141m$R@hhK_O4{smI)aVavIT)`8D8m z|GJEobtc!iE9L_YDPX?7#QHgyK$So|VmPFMC>alg-eBpG0QPzwwxPo{^rugosITTC zt?eiPfxB3Gpk9E~Xr2Fo=};N4u~rx3^|)gG72A0h5Szc;|CQF+zJ75@K@R$pDo=_j zC_F#hpoG?qo;sI=Boc;NLl6T(QBZVWv-VD;Hdg~hv5pJEa?V?}bM3#kO}+U#w|u(> z0~JA??8rf0KA)PJn6f*}_7Ma39ZljhC`C@cbMWnC)Xqo(Jz|EODZFD@ZdCt6crbisr01tZb>!O?1oG z!zF~;retHccbLWQ@yu?uUWkj>?)^RK?mW+Z^DR8v-xQRO$CA|aU#XmZH+O=b$Aht# z&2IlfX^fnqnruCZn~gS8jjf}V^B;{7ue74^S03Xhmk znEG?+8QC|u_1pB_1xt}=p$4cAJSTKjwAtOPHX>Lbp?){VjCDpKZqSr>bPU_lFkZ-6 z^LIMDz5B&oPZwc2B$i?v(CEV(9LMA*CBcx+MgGp}SMMJEXgnZloK&|GU0();f1{F*h^6+WXm0U|N45 zy+w8TiHZTAninf(vCrarA(HNEJP^uHdb{G27JV>7fY>v#C&r(!mDfVxyxl7>ItX5( zWt^Z;8XxMO61iWOaxy1%dI4Y%Dd&zit_h#Z_pslPlYbjiE1la_kb)qTz8pGw=soIg_ORRTs%YSC@w~#C{a@4O7{{}dy#rp zP-5MokW4|Ou$OGXHLRy!{?Q4HID+AvJb#2bC6u*k(vl>q*P^Q4hAUry!@wsdU7=l-di%$F2j6Bcq1&nOe(RN;2do-L2g+m(y4#(XE7QAP z#Ic02r9O@xWYRVm*$W%Qq3SYWc?$)Y8`vN+ubs;bq5A9uQ3Hi*(FX~p@v#mIh22wm zC=?$1UDykP)op>t})VW-!D+JQ3Y@D6*fR4qhs7RCDM z)6{HV7;4OufdF+DdFB@it%`+j!i$SkYTS(R&@ro2uu;D~23I4{vBe#~Y12G}Qolm^ z3~sOsBv&8KNbDC>1Jqi!);}yL>CpN&dsCXagavgLVBf0V_-ExX)Re7VBTgqiW zh(|78g`52xRc}(L_|ZBofp2BYqF_ai#>(cs;yqnD%vi8owRIc zQgC5E6z0HA4IBU_7r7C#W15zFg-mwKC`F5%lm+cxkJL;)PKA6dh0961G|H? z7;wMUZhVrA+zP`oIc^i(L%5DJo_ zuSL24kltZqB)Iv)eOeI3l`6|82TZJ4=v`Ca6{RUqDammp(e9pKb0Yfx`qt^9%~&65 zNOOZu95eQ>MdLA#ect`+ z>WL|%Ci>ugDWDArH`2F!l>WLGwsoE-&Y=Y8AQ;9LJZ<-BJHHR|eIN~&xddf~tdJ<* zkhiWOI#b~xb`MfNcn4u13?Yi8ZQqiQ%l0>d=WKyU@9BdJ6_H&p#I1liv@=GX`~5Y6 z11v5_F89M}Mj|32FEk1O6Q&%1FkN0UrQHSvVy&z#wDsuUd z^Fy;ARfzjL>I|mXMt1`yM?^;tC3Q|=Kr0<6jhOn8t}RJj=BQYATJ8PZJKk!+1%cy`NWBN#Iu5MKf3O`RbM(kbO=HqO8 z;N)9AyXjdzdm{vJzw`Lo_T9YOe{0bD@=nb@?bwW-dz_9A-v5-KVg1Y1l@xL7Q}WZ+ zYJxsrg}ebDTiT*>^Ez+RVx_S>EaDd9u+_+)E?4S2gQb`< z%2h0G2}WG{6bUU6x*j-&$?gurv#7Xws&w=KWZ+@Dl+u<>F@-Li>dIeBiUh~h_4ahK zMN3Z~v;mB842OPJm6vnFU_|s2=%$8Q)>80*+Oc3v-sx)n`FTz)d@zFcd4^$9ce*y6 zsII+y-}ai1HsH5ZJ(brpkc$+G+}o<0F$M2C*lycl$HzGdgpI&7+(g%_ry#vw3E>I8vE;sgFa)dQWy&&Mm*<>kssqf3 zJuW&M&Grs%{)E0y2NZ;BR8?khyWUXGx5@bkM+cU5zhw z9|55re_2sKLM0ZQ5Lz%wMh3Q32N9zoWPX{1rb!f_W<-Pk=Lm~&^4@ix*972rPf%PC6+(UpeU$=B|m9!Lc=u9e1 zJ=XDQSV}Yj#Dg;oiH0^#ym!!(lr55|*);e=_bLDL>Mv|HOI+eVB$~&+qn~B^R1s}A z`1we!G&}t%W%40dA_$+WA#FLual$s!jcO%QTU~xP8aK<0dX{@+?;}@v48P*PXoC~? zl0qBB6WF4{;|z5mm_0kHV9c&};8oB9>_wsKI1$AGHB1EJE(-+A8_+ zNVij@TjbZL(GTr67scUc(sY!i2!c(phtbZFq|r|&yDTBl)7?Qq;7&N-TI$JgPX1NM z-UsMZ9miXBZfi9_6rqTB?>c0@Tx-Tv1qDVNd^R-vJ{g53_VTaa9jI?`qZaUTDn>qd z!F6CD^ke7_1@u6^#Vfnaw1>c7C`21l9FxR&Vi0pj$|kg>Exyo*9nj)~iGH^4oWxr9 zn-EAER+b;*1&jBbxV8p1IQoTEpO9hRlkuVLy!F3!oZ47X|FcODXTi_({B8Oek)!jX zM?QXdj|@NV%>Hl*^$!usjuFpte)z*0Qa=?DvQhU!?vaX!^8LDtfXL&iFp*xX-?FOy zFKgI|pQ3Tu-n+bG6R6V`Q~u%w=R^`eUM@+kc2DEexg**<&m<=@c?z>gy3@X%cO^E@ zWU#9FUTk-2?KxX9Utlk1H%9}K zwShjwUI`0gE?t{NUBFOWI06=JdZ1*p=qTY%r1}~~bR4L4vWsKaFjI>>@CT*_e9fuI z!3bwWH#WOmV%NXtG#h+rkNsx^lB$(IR`+8>pAIZ{$Y-@1E9`0D`W_z<;T*C(S;ySC zR2kF8Igtn_5%Y~Ck++QQG0g@QtK@F@)q@9W-*6*SOCpBjih5zI3)&XO$<;~!mkS`m zCD`P1!bZGYQ_{EvQ!1`W1kdg0@U@46#M7ugOZ`_nSCv6~baqf6zK3WHt%u##&ClV} zFQx*7Xl$e+zU~&?TNFfqx+_9eaWD=hxMu}&-3*NzigorH_->4xs!`sRw(ufIc0gJg z9>9YB!c09rtbzWl`}AaEtdT*?3)l6B=eIzCrz5fYfL5}@-zev4RhDySrLtY~Fqy9N z%g1zOe8e5|Ao4)U3I@JG;YB(eVZCC1ny51_9_X%rwIxzf&nZhuTQKzSFsZI#DCZNS zF+6RNYI*8fb`;{$(=wE0Hh)sQjl~lAn%^ByXPmjuiMJqmqwOE5A zD)M!41A0oUx}E&b%;lL{!N7&vjMtmb_`N+q-g=GG=I>UoDHaqAc>`9cx9-wyT1f9l z|Di1$NeA;-AIdj8BslV2NP;SE(eSE@Z~7o#9LBw&PSu9|8T%>7rU|TcZJep;ZWnx% zPbT0r+tKt}Lw2QwJ}RFx>{v6N*{q;)4f!8Ssq!AbDn=;3l?b;bIu&^A;n_nw*pKxXu1d(T7WsMSf(ZGT z?;M|PSN%az4Q^o)X_VA9WldI?UV|+jo+Yg~JnkuGuIBP$JCWVy5;F@%I%5~6?Pt$Y zZ}~_IevBU?R)MHAb(W#L^d!b)GHZ3AVUowZO-=xv%p{}@@=T$G$fj&B(vnR*VNhIv z`>@)zAF{A8`7_CR?am^w`yJFc!x1YG%lW{Nrh&URtIM*lfoqkAWp2VvEK&lDf?M-c z4+Gbl?2#!uAlEs(fu0upRdzAR^e`-Gy9B4g!`o05-tL4?C{Wf|(UOd8np%(ylO!v` z?7^?lu!VbsJzSw%qza#S)v(}20K+j8j8vkM$C&2vK>QLj3SsNR_dK{`2(Qqq`#@*3 zMq^BZ-es%rzji%;;h6W+FWbiPAJ=9|tzp03pKoNg8OHtae8yvAF08c~fJXuBbh{JVv1QhWcPZAz1JkZ#mrE%8)ls=#2J_58*3Lafg+hr5~ zThxGmmpH!f1++=wuZV6bnCAyT$F=odA8(xTDj*{8X1zkk0#%@3MJ2=nZXaPDqT(H5 z{yg@t{i1`uw_>|OnX_2K*!a6_d`k?%{fRPb=0b^c8sgEJ*H+1MxUpq2vi36?Cdi2rXRv^rjV_>{=JU+nt zj9>Uaz*gz~N|NmDtna=hZp;O0y8;z_wtSN6F4cWcm}#n|%&^6o{tq^)_tgg){rhC# zwbU)}s>UbqX=KId$t~_ih1qxvvJv-L_JuaKaC+0Pc z3Vd`cw*9)4S=;5;r`zXN<4)hV_VYGHI!*dYnSHu^TxeS56wv0HVZV&Fc&EkNeoe*8lg-d8scE9j-aBU3#)F` zg`kNM)FrZW9c#lJ5~VqdI_BBGdn6j{{}X~Qqe=Ztl>@b7-rb3VdvI@kFavuZm!hqS zDZ-z0lXB&HJ-OlzlSNd9-u>E?Bx=y|^_+Yjq-|h`$Ac5a9W`kF^B^+}D~1Q;$H4i1 z;5rH0S#en19~KlaePf*Pm7q7v(Emm9fCnvh6=_8S{`8Se6^K zRCddTrafgi8w2HM;PvCYP`RS{^7J%nVC_v}v=wb>`qs;kr^&3{`Oqr%KwtCgFj-z) zCps3OKc7`Do?Xo}mZ{8W%-N{2+_%pb+mDK|%ltW6W_q=+W&-&1hw|?(XvRn*LKBS`LF;d!$;Ww^eWb z+J0q5DA59Q)~TOezp0;;?uZ7`yM)p1vs}{y^7__e!(S(r6cqHW2#4B%m>Nf+fG77)#Bohof*y9T9MKp&)>fvETO65hn*qW= z5&b3Ydvo}DZCFfq!6OA*qQGwkM}3}Df!_q~c8U|3^U0_c1q}3PEY9ooX>>|{nb`pi?puWHl!rj_z=5FBUp`x3f2LkZT2(%{83HOu7Q3Cgue@0P;tDaoZ*PDnj zw!`~-+$|OU&1o6JnrP(zt{i!0DdPo}rAQEH~T|2vkEI z($#o;_u?)2uAcdMnQI$y>^RpiyFem`!wW9Cqx-YB%8qbaXQ9gSRBSzao9^2V0mgRE3$0dnwCc^kIAat!&~ELZ9H7p&;T!E#n>21qaiL)!;B^u91cm+- zbBieSa+LVPnj2!Lzn`Y~Yb0vg3un4_)0QoiP8~$4#i2a&9Zq|edKOFs*2w(8nRiib zc1c*W5s}}a@JmH;dn4sJ?H*UuRX4u;>;E;ut35#m-EV1V>V6(9MoY~(Jd8|fCp9ut zUx^5?!M2bQLSiCIi}D$=0z|ixOgX$oEGkVThhdm{xpLeVh6B#!CrbHvsZZ9;A>rPb z?@GWOAQDdR7C`&WN~T1v`1cR{Ht~;NxV$b(SK}E3muadDvZ>~xFth-ab)(_EEEeCj6#npguy#7shCbW zffC;XA3e|mIt~q$%!@^eZx*oQ4)*D3G;x z{otU>q@L7?JftMk>LX8ufe`ZyuT!2PF*kId7FZT}N~IqHS*tuQ!zmops;7J#JQSOw zC`vQVVL!25&h3H&Q(v^EiRy8b9P$U={r*5*rq!UtxcKS5ll`#z?|ss+L42RU_+jtm zd!B(aiKHG+#fJvaQ4JcNZRB^#KD6kP0u1z8;nU!>GL@~a`4%Vr#J)RPH3pv)Qh5QG0ko(U$h0NaECmJI03Bl2xOd4lgKhKY%!33hOGZLV-E(EpjvP zLWAh+p3cYiJc9Y~FS16NYms|~U?7&&LM6*lnK!~14r_r~k>6XM8j8X9{P$dFJFuT@9?OE!YkVPw z6E85yix;Nu3I+IbYlRZ!F6_W1EAT^Vjb_m@4 zsC^pyd%CgDwc>H0n!Vu34Z>hm0n>65EB^tCX0S_^86oCs(?+Bl#?dJ@|$z>HP+96#VV&+mR59RApBU(@j-f6oTb4#WPAj%$u8p{=eE&TR|#=ht3 zOWMN@_zZ|P*i?w70a0}4aEJ#h_#I<6DEfnsg11uhCKb+XK#nBqj(< zGH8!w0fj_KBoHIFWO|9qmaoxYjo|Jk9n%Z-1^=~FXVoG80A*0d`a?kgh7)FCfR|QD0t; zBcA)S_z@2KcIDUUr2%8TeFkhQr^=Q=OSIErfIs|Nd{=~=XV~zj5nO5fZuEOcQ|M}r zAEyN5;KG>0ge;~hX%&T8(CAv-I9Q&*-8TE5xSS{pb;2?m-i}(X0F*WSCPl{-!{8R| zZ%_wy?Pum)tte!u?bmrQ#AKx6GuC;-;t0h55O08Wqe<~Q+Pj5Au8Wy^s8-TrtBlVi zmk|sYDGX7!b8rxSTrteiTWtLKkBIshAg@Oi0awWFjeuIO$cAKl*H8mR z0Zin#o?hPHGpN4w*t&Bd%Jmc44lmU)OXwF`mV9fHaA?fFUv1UYHF~M43@Jp**Vpu> zx|Y1{bY+b`br0h?wttL1l)Ci$-u#hDE+Xb(n1>qk`_MI-3lf6DSKG{LmDf~`k!NO1 zIa_h!qH-$#Zpd&Q5kufjBQ>vht&c;x^R(eZPK3Rgc|lh*oRoM}<(JGL6QjO2|0*sJ z=$NG0r@X)_KOSy{Ys0%xjhlnXpn8gsMKZvWFwSO2(BmfzLq=h)zuYt zUDJa1l7r$Qry~4LrUJOp)5WYpNIt(~x*meNp8xW#c6gM;GI$9eaJSXY5xGz5m8fs5 zE+ot=ABP6-1qeVB-wz1eEz~m_Exg9UWx2!@;#2vad~k7Lz-7^uxcKoHSPL>&DrdET zW-4gYCe<7_)J1?oKi&<5B2}~dj6#QBJtDW>dMV`llJ0ew?RQbqXjx?28!ITj6nd64 zK9(vFgTsG~i_ErztAvl31h-`-R;In~$z)7uC09NjaGy}*M3Aj;kmGiGJc+3CWMg8? z@Wc5pv?A9x()UdPP32e#wRt9J&mEti$QitD@~TQy(NM9m;duiRwIE&{S{d9-w13G% z@|)7+bd$aOunyEYq#yejFXKXf0|^P7;bCY#W4eEadmEBTPIkWwE?LZo*)B`BJx>N| zR$RTk3={+$DZ-8eW*0#R#hFg!)438M&H@J;{09WSoyG!@;b!Z<2{X;t?i@+P+Fh=D z`(&rOyF!V%!62;XhjxOTUtJ^Gr=M4!`q@tys}w$!8x}n!LIV3HU4(nc{92QU&BLh* zpSG8scAWUM#zsEoJ?t5JT|M<0M_o0-?pOF@;%p%~?2G%O-pBl7x_V{oN(^_a`-`u3 zJO<)*PzSiP{P_C_(JSBbC8>X0P%fOLV(*g)#JYO26f|mP<#Xnj|5_apTzqZ%erI)% zY;r$pxLeFRMMXe#b3}j5D8%9YT_9{E5<*Bf*HCCNcp~c( zSMot=4nng2>XFyFQ=rI|X{=R#3viW1_+Gr%{ zf?EvjM7C!?U3QP}P1Ct5Fa=im9bI8Vy^0ET{?#PXJ9se;=1ZrKN)p`fcsrhW#D2Eb z)U!mA$Wnrt1N8QQq55*>{5KQGIuYGeeQcCph7K0_$dd2$3FP)2%bfqfKYZ$dBnB5^%PlzE zbe4uIbEG1jCP2O$EN;qi&>?jmE}+L|Z&MJfpEX7`F84MPf09o7ZLg52(Jmmn(Oei{ z-6ov8zD@qfav37xwi?6DxZMsqOwr%7HSK0raHwvKM=|(&dO|$3 z%taYG?a^sjNM?fg1KIyIi@vWr*Krsw^)DuhL`_a=b$(5^fox3oOQNXBRRby8SBqtN zqv<2}V|Qhea-EW)nSCaq{=mK-R|dn+UI{*7<(7rBWM1oR1ci}5O*vU44Dv%i{2`zc zx-2col`O|f;M(~qtTPbiK}a>GB%2eR&!O68)dn{OwGCx2#%`eE+c!@>0*GyRaU z<)xE~?3!vvsXgU+t~e?r4W(&5_VD9Ptw?iYENby%?$Dz4*R>^OAJ=UU1HC1}sRRaC zQbw2DW^bf+)^@&hhPyGyW~|cq@6X{1?pk4>T6saiW2z(*_WsgSUs>`J_)fZBnVXwi zHNK~ByLP8!v=os-<8ZSxi*CtSHecwoX5qAz6Y%$bD@aAQSGlcF>b0QjhfKM@D3mMP z(t8zVLR;64j|3py4CwOa(>e8#_rTc$OEP53?#nEp>eBe~vu&Wc}w)!=q*(QuF2z?2N_LF5&A70y^zL-+A zf*~%~m+f&kWT>qQI*rb!xTBl748@uoFMNL$&T%Rp+8Fg0a=`|la)*QRUu$5U>NKtf zF2=!M8s9FGq7AvjB>lz~T75oh2215lucTUajuJ|VDrn9%8ie%zK^gjKonf5}{E@h{ zP{iSvRK}oB2hG*Npc=o1jN53pWlW7tZ+l7+ZZLk9ubpDXEe&b;=W_LnF*l72F$E$< zmt4|cKMTby8XB}^KY!DDVBYz8E~08V-nua)QX3tWLlMu!mDA$u8wz7417o0C)O;F} z#s%f*e=Xj%L{~y4&_W=8VHx2|7UhDxFVQP2Ax&g%8Ocv#2DRrcO$wzq+h^h8$GFR1 zP}NW29c&D_rP^~2QJ$aqL9Ny=+D82T*M+Wl`4ViN_& z#=zTb0IV*brKZo$(5wAGLHPrJmV+L?<3^j-TUYF$c}jXzqZn1Gy%Z-o$qCerp5A3j zo_sRh3}C-12gL5>WjeJ*0Va%e34p_2%121Vb?(SUi;IO1X_F}cu@wQDq*)+U3k;-P z0*`T3nX5uC<0kdv|)P%WRhKl{4 za({mI)bBLB%#>`6kTg3lC9#QilX(c2@bFZc#P~-cjd3j!k}^)AB&4$}aes&hStCjpx{xM1axG2Q zKIfFo87veSixjzyyD%o_q_Y#8^&<|Z_s{mBWTH}aY}JRK;1-t;`~->Gi_IZ$J4!Fs z<^9oX7iOZGB}QCXc5~icmS%h>K}J+BOkXFBMD?g*Kc*HJ;N^t) z+r<(=c?)xO=V)VQc&BI;pLS**Cd%a$DzXd231;IpcL}7b1K`+F&DOqb9I@5?(cA$n zXioMod24Gcj|e%YaqV74@u&$64ok`63q!@x!h1<5%CS>y(ahDB_6WR~LV;}DZceZ1 zcV6dI!0zw{NEcjbJe*TuARQ}-KRedgs8kr{@^}5T+dpV;yR_-QT4zD<%W*Dx(soT@ z@pdTlV*2jPCA`nX$G=xD2DpmIG>g8;0+qRBhCUN@OX5^{h&Tq=~D&=8B_CiXi%aG!6BnOtz!>qzs;Nd3(Oo{G|g{%C-73b=mUR)u#?} z{Z?y|PF^qMM~|#}yTZANrEEnA=TktxvhQsb`(CGtvtC-8(Y*fKYd<^hZ2g}7=f7$` zDvTM!V{?e8-6fquduv^*M+1ol1_Z&ApmYUXDQ|6u{p@Z#g0}<1< zS*dxt1M-p~NuHp{#?-Gy;ke!rmcdsp@6<|9jijyWorXm=_ze#{BTgk{=kSzoQwBt&@bf-P4Jzd6vazCL46h};6SgfZqC^}AxJ=rQuQ zSD~nd+Jv+IE5vF+h1w3c{sX^?#z1e@eQ$nJCzeoeZKOKcxkg1(RI3cSm(=cUh>7Xi z9co+kl;jCoKmU(NsNC*dh>f%U`t$t>H(-2LTi&^2t0{ENe=&eq&BD!%$g+pn_M6&v+4eV5K371fA>0i|DK1XP2K{jT|wHy%R-^(v?N7q8Yo#xJ)DOeT3$T5JTx zH@JK*D&@b85E#O7*a=+3I?SuS{4W(B3u%H}tk*cHLa&ET-MWS?jV!qAN@=KCLLJF4@IJeq z?S(Z}R;cQg0bIl6iOoO%sn07X@tUy`r_Fa^>&&mL0c_WQ_v;vTEq8y zxZjVjlTdJP5XVlgyYh8^=bx~NJ!8(-zpHn^GB|lb@(i(%MxUe-CFRx2^aY%>kbCQX z(vCzAj?_Ea>9Ui99(-10>sd<0hEIa6VaIGts=i+!?GyGG4!f->wmqcm)7JJK=$O_q9`zZZQ>LG~w`YZN<VuPIbPti_O{pvCl~q-xD;t_|H_ z?;iPfzSKCX`kV-JRyqBx&Akk)Di)K$T`6_8mc`Wj@M)@3M#$@|fawSG*n68{EUCTh z)XPCPz`L~vFxV$|iIRp=ZPvm`4&Uujy~fk#h50`zZSH@4&dTI>5bCpMFqQKYMBlz0 zFHYoN?#Qa$qC_ZbF*^|vq9K_4gz6IdM5-%QBE2SBf@Vs!#7bStI$hos?L1-pI?d=+TZ3xh z@UX>HM?b3pTk(tjdgJ(G?DwufdXY~j7J@~AMptFTj+-4}REd%(5lUQAVV9&9HEEi7 z=G>5^B*R_Tie34uq!@z%TnZPV&z!-f37HPeOa$eYM3%@kaCJ>&1$|${Wo=NPqc2&^ zX$V>-skpirKuM_bCOB`CyWi9|hm)0zF%y@_OdNcFp>U0*445@rjUpwTRg076J76hl zC@EaE9G$^$&9N@!&=)ul>jTgw06>ioHaT7cVW|pW_c`u&`?b5rRI99x`({-q#lGHJVH@TZdJ#H;~KI%A5guH@mfcGmYTRY){ zW~w*Wn67LCv?9MC*9NTPtHXrP%1gJGnL+8C(uDT(_|2+9v%SzpD<_-me-Ib!81$Zn zQwycX@7eo`>^yI8IF7ntS@Zu*tkjOv|ES5j&a;gR3JK5JZDKTC#>L1?$)qUObmql_ zL;EHLN2vGgsswPwQQ*)~5TqLEOH}I-Yq1G2jpucT=p?qGw-S! z!d;~f7pN+Ee^y&BtU!TBy#k-yi8KbTtjP(IB0?gWH4zeTq6n9U`=e%_=BA_Uz{=+HP&>@z9N~fylCRZ=(0;CYLLdtdzm^cM{g=YHz7~dIT}MaC9d^-TGF+{o-c{ z`1T^x_I}erumJR_?eFnqAz0McVU@Yj;=mYeuUDlYGYU{23b^1yfE`}YXnkh=ILYIv znI`DL5Bg?-YnYZpk@6c$vP^h;RuDenju>1VwtTy1sBjj+#4Ja%$Xyy|T^k-JZV zvil;D%Tnx{m$2l@%414uDod47yCLpY>EPvrfv39l1e5zgkA0bPRg3oLj_fndW=~Mt z{uQd#SG2u6xts#LilzUEDrNHht2a-Ogrl&d`!xQ4gI&+65x=n{=k*(HXPU1MrY_b2 z{;nCBpu6`sPF|(C#(Tx`Tr`&V*6Y^FyIDElV;YBSq;~H%;9l{SOBxDSzE@FqljDUb zfyI>UgUN4>)^>s^PJB&K)^yjaZEVdloAG~cP^N6bL_J@UmgK?`C6P129EaVw(m%p0 zkxjo5CbzmrDvNXCEm0B>Ji6Co42^{4FgkHb;TIK)|5~G~<}hDZRJu&+%P*{z)FW8M zoJQ=P+tI`71ZHBGL~2=my`8uWHyXd!CGx)L?sFkZ@}}pN!nG$#O3VMt`5;q=EWm3^ zlFx~;G2=oYIS}e1VE@`&)827b7@%#}2LqG&|TJ*J-Gp{+Cg8j)5iAc{${YLgUqpB*9{SaD_cO5|d-m zpm2YuwL5UCRoo5)k^kTtaQ%Gr@AAvyg$t1=HI>36B$4%VH|qXGim`-w`e>#0fz}E9lgDlFY;rkSnU!lF<_$8 zYr5}#`UT6~4fTqgGX_+ugHh8n6C?WEMs zM${37^0;l|JYHU2ejL5u8_*>U5{A?od4T4eT-lqCb4#LbgYhwwPcRq{?&nQ^l54C6?ut*^#-u)(K&x8hkT^1fs}<%)lC zZ|5mkXml$V!{{nq zS+52XFbQTduI-JU?Yq^T0=fY(8^!32d; zxUKqrY;;s!UO^#)VoWcaX!%A~-@3sV3MZ(vClFcFRyy_l^U9BdJWW=8n#o(aW2Q5G ze~b?~b+gK>&yQQCa8RjU&QDC;vt<^92OSkt2hZ!=AcpsB;N`N3t&2xyw`zsOK|^pp zfS7>SnoN*f!nQ6<;3a4#A9Y$uHea%aJt68zJxg1;6;>byl3*OUk8G!eqii}Q_nDCE z3;=9`y$#0)Xy%91e%;1R$RMp2pGTd)NzO|_NPWINY3>5V!CM$aT)Pgt)n(;Oam3g% z4J;@2R8QDcCbVT{IcTr!_7*Eh{M1jL08T%-d18v*JW4)#vA#QK@ z77jnG^mKKNqB{cSxJ~Hu_a6Ah`^*>bj(6oZ}nH6Wax zMOfQ-L(8B%)lwT@-!{#k+@JiPgU1!OkuIl%Azg4ty)e z7q9rQ{R~W7w*Tz3wAeA>U_9_-9?yU17_YRsAI&-ec5_>*z%)9`ZO8J+12{T**_wJn zDy!HG=q&daTVnT>4U^_TvlFx2w9$KUUxO=|!Uww6q0%n{XDCU+=!)u_6(oC3j}gBm zkU|rgam2DNsB|_In-PkkQsNp#pk2S26Al?tO0!vxOo z@TqY^{P&Tg%Y@UE-+IRHM~iJ_+2v-0vILZHq?r3$Lr(JEjPKt7O6!aQ(j~M73rpxB zaLgl+pEU@8iFxcH>TLe^3liscDK;2^CdR_XZbKCG8W_~WJ;B4QUAfF>zgYPM z%#chL0hn+1nxdYRtsat`Os&;Hz-FkseG8E3@(GN+gJ);Aw^M@}e`kvr=sQT@y#VmS zOTofx-pRYN^Oa|6qEh-s0a)|^eEFPdE32z?mpdUzsI`KMCt|28OD76 zR$hJv*>rw!>O;>v&H2M)pgku0r;rSKaL2FuiD>Rq7}l-QoP+!Kv@3US7F0?>&LK7# zOIAX(%|QXg+MhM$$7EJ$G0Ar`@606UyDU1Q0juINYMZ!{v40?2z`6Knv?Ml^`xVqn z`23x-*dUc>_2RxecqiQKMF+{HdVJ4r=hk;g3cVe8otaB!|GDr>%Z)m;*Ynw-%l1QbWBRXtFRHrmPESo31}=`#{QQw#5{JmfE_0j?0C?I zy&CZMw$-t<&vm=Q{ZE}4HqDX=L=q!L!OuF^o-J~tlV{fB;^G#z8&>Byt{GRIw54zX z8wCR(ehn=jKoOLe8(U;p`%z_l4D}{A4T(g+#%A%&kFCSVC_du=T`N%z*h{-JW5)r8 zsFmeRS5j&RRFu*ox-6dB3JMB|buDIuk*)=xTCv1I?up^3(wyI)carF}C zkgvd4D!{vh86sqeFexvO=2bH`HU?5|L88pf?NFF&oP2zI%vd`MOpDaUR>QBhiru4Kg%>QSKJm495yC=p zhgwK;d&`le0#(o?|S^wp-g zgQXZZ3@KM>y_`Rg9X*qBa0ZZKRVnTu+Lb04@M;a z0}QOCe8qWW)Fv-|J6*GZFP!inygIBI_X*ULmml9E=jh&iKv~j}El`Ukp|Us1pE=ws zD=W(p*l{2n3W==?$C9ARf93}H@!_Hv0lSkrwyp+XHDA?0jYs#zR6nN$YLB%A;KY61 zRyH;|i)Bp3+EyFV6{8$*2N;m7cdS_v6Vy~0<>1xW0U^QcNT-BB^8-LG#@@^M)rzNR z%)4xpv9h+-!uvhSr8$9f&|)o2;z(d0H@i@3O+z>FTfT+TB^P0UKpJX8|FNFn3gS9af=HD$?aO$vs-S9frT`_#X7BYrD~6_6n-_wV3icI z)^Zp$g9q37OyTeL4zIkUM&w(Z9rAjMVyslnDU07{Kj`|FMdMN3!y{W~JlR*&`UO<7 z$3z6*OqSKC)x^T3XoF9xBsF_l$Ep9Qfl&=cM8|_QfVA)pKEs!2%RYLK}#lHuu0t(@sER{fgBz5?q=IrZmO|BPBm=2F^!4_kO{=Uwg0Ib1^?3 z5RN%OcWovnXxL+0W8==Zo;+DtohYRVuO=)qgzO2OFkD z+o!lEC>+)n-Hn`e{aeubAF?_3?;riqFBNGC(TpbuNdRQt9&&9TSxRq#Mo#Y*ij{yh zvr(se%GA6j0v}x=8F}q)^v(k-73@H=q4)gwSl$+4DBg1T4%Z)IR?#6w?!o-H6Qa?%geKqOJQT=er9B&*Zyo-3XA0|4RdZ{IoSrd z-vR%af5ae9k}yQO3LLgaotR~8YHKa{js&11gn)=FD3B@dhRo~k0IQ>My#MC(1CLvF z83cN;7JZDWA4|>@xNpiiIuK zJb4dDNpLY~OUF)tULJRdFdIC>PkSC_t1YzKQ&Lhw0Gxf}<&VJu^~GR5n)A3jQAKHI z;F_2^@@K#$2J0>2L7msu$U~knPJuw;1EK2yj@XzeB$O_8x5=sNNB@oM>+87Ss$=+R}*;(q(CA(Tp50wVGh;kShjHn#DR| zNxBnA-gvcd_?g!&3qjl(pIOH1i6y*Q0i8soY{Ax-NwJ=AqdJ=>80_0Gt%fDQDpn~A zx0g#IIjB$IO0L`LEhaa298tWoofpyn=Fq=i%j-($FYQfW}pc&>SfdIZPLsG^UZx1L!4XWzRng5n-J- z+ET1XiT78|xKyRz`@L?tpqf;@z*sD|kYLW9I#{1sF8J{K#h5NPqie5jj^MdDq{AEM zy%($NH@9C_YR$R<1@!_SU0uqP#$IUtdiT;~XDx|4dt+sEn3tR*`E54Na`+DS@mIU! z`wN2@gSw}eg;|~UqZPOD+&Z%tA7JOD)uw`f=Q^MaCWjydN%SK3+P;l1ajXCBV*K#e z+uFSQIJ|u*ZD_leE$0pCC5eqbM{I>EUy=DdT$80e$?@Zl&e`#p#(d;!k&^kg&Jx{9 zFWZ@wYpaVBc4O11so#3jvz3}QBoSMzuI}-5F6=o1j16-yNEnJ{s0n!*MYHa2740{K zvNa4?4ZqDxt|VzxRvYz3W}e&ogm025qpvEiyq|uB~Y!^TJ--LdU#_o|KV+Y7V4;)={RpMieTsW;@2Nw zPRqg!Cf6%PykV%&F_+snP=A>6#Z8j|6S&+3E!g-j|AB!gZvp(VyAU>7*>h)qS(zSY z&y{39(M03=RXfs?D{9qnm15=m99=tyYK;xGh4ixUM4$hDGT&nTu<8+M`S`*4nGeli z{N%r?l64-^Zbq4$bM*XcrEUMD4Oj!#%lVu(Gm%_?~ax(80iol z^1AtpdxImO`{K7aYdxzTdEIAwfSun@uZTaeml5jmsZauL{f zmeiOcrhw0}WnU=a)*XeP8^~?az*eDsxzYA5qK2XRaONnI=fSB84%^@h_FRD@sOr70 z9o(%>JtXr8aA`y`vptVn@~tS1+MV{*pC;m0`GVaauhe_8e1JBsB$8ghd)0Y<(y}B= zX0^?=giXJ7a5^1gDZ$=#$2D6?af!kH79;w*Ja+Q^_Q55)XQ~G?JdQBJMy87Cv$V10+mNs1%V!!P^k^bJPh zDfUubEuuUd_b4s(endKOnJH<&IinSW^{Aq z@u)(BwQz|CTB}Use8j}5-x9V!l&#ZCNjeLvpSNm%_|PK8rCe7@up#_X%7ulvtr?6` z9^m$hIT2xhTBy{kvLq);M%H3<9J^g=^iHliP*k3-VEiH`1zl}Cdgs$cqvn-)DQIfb zRBUPS!k!?%NNOg<(Wimn$>gLf>#)u*hnBS_ThFLs%cH<rQ&8`(^Ifm8U|b6HPv+(lKe4da zvAds4K#s2aQlE&;C?Wl6NjB3@%FMh=vP{=%MxoRPD7f%5Z5gfBhv2n z?rsyjMK84ti4cWl_;=oczzu$NP*n0T%dp4Fw!^i}a&|Xqo8pHLBQLa-rVp9FkdBcq zzxje>-Y*Cia2IjEwLe z=cm3+X7$hGK7L<-SP;)_d+1(J(8cVvvce>3dXTzobQSE9hO}z(M&+w!7Tu-3E+{o-5&vT^~f&TmsW? zHPPos>A|HUP%n;>?@&)DsFMw|L9+)cFMlOVi~G`voM_XxjW74wdgh-NEU` zr1zZQr%4Ddk(b^HR*-~Ir%~O3gz7)#>2~l8ab`N^5aeU*xX}`ne&M?Ww;zJ zIdX=$m8)9XgULTsb@wM;;;`zOEdhuQ2rRm>A@ z3gzjofjG-_Bpf8NU6Z=O-7{9G^4aSJ@vmhbE*lvUmq>>>=tfzvbj=lNTg-tlPAGV3 zT$li-z36iZp`rtiNf}vjadGgx8xTmad}ox-zh98cNM9(7qmT^~p!T=mhthe8WZLGc?`Bh( zhCFpb@eg>U?EF710Mg!V3MiTz@xG&zXemj}aGkcb4;L$EcmQi=IUhls-tWBSd1Yk{ zWTsrmuCK^ zZ{hW`iz4TV#$-uBZO*#j+x%R5vC&>zdY)&oA^$Y_W2dj#WVDYZyY}Mvu*}aBt|z$c zX;G_dk8i>IuFiCN-?9Q%ozjHdtsQ~aX~q0&jYk=ToT#h~xw%M_^io{egc7tVbt#tV ziV5-Y)tLQNLQ2%)(&$LY+gI=O=gkU-IU+5pOT{&au*!317>SX+_1Wf4lg(BSE^lj6 zlV1^GMFo&X*)FeA)sXda{K3A#rt^@WhOj_t2*gkcjP^}K)N#wIjY)Gj2FRbjtb+T9 zXB3A8>CVC+9&f&XJnfdtG@Rc(d+A{zMwT3Ej*Kc0S0qL|K|sjDBJ_nIi%0nugw9A{ z?e-SgjrLjM9y@GDg)X4zKp=moQRzb80QokBP4ONOWFO&%@d?a@>H+7PX$Z?wM+o^~&|CtB3ZvXr0CU0Iexpca%cZxaqy|Fz}GEdQBEe-yJ z=kF<1o)_9cJ@rgYH}z>lqc^ML!pmlVca~Q+F6A?Y&&kJs8NHt;#a!&NwY21)Icv8D z0CfBe&(g~3hA%uH_`E87t@B>zUAj(s`_b;O_1HUmSNYqlm5qz9ArldGO#Yr-B@a;M zD}?(vS*Z+}&)d|yB`Fkjz}o#rEGwB!#&lX7%w8AGU@LrWKWM$xNq7CKL0*x#;jyB_ zL^R^s2UnusGGV~bIS@`%7u

E3p5@71Y94_L}9-uJ0Gn4?B4!k+o6j= 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
Z07@bB7AaYEUJTjjx(YJ$iX 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

}h78jCIcp?g8{ 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 literal 209724 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 z Date: Thu, 26 Nov 2020 12:42:20 +0000 Subject: [PATCH 179/241] fix format --- qlib/contrib/model/pytorch_gats.py | 2 +- qlib/contrib/model/pytorch_hats.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/qlib/contrib/model/pytorch_gats.py b/qlib/contrib/model/pytorch_gats.py index f6bd427f5..5d2dbd9a4 100755 --- a/qlib/contrib/model/pytorch_gats.py +++ b/qlib/contrib/model/pytorch_gats.py @@ -169,7 +169,7 @@ class GAT(Model): daily_shuffle = list(zip(daily_index, daily_count)) np.random.shuffle(daily_shuffle) daily_index, daily_count = zip(*daily_shuffle) - return daily_index, daily_count + return daily_index, daily_count def train_epoch(self, x_train, y_train): diff --git a/qlib/contrib/model/pytorch_hats.py b/qlib/contrib/model/pytorch_hats.py index 3ba2d676e..bdb68be28 100644 --- a/qlib/contrib/model/pytorch_hats.py +++ b/qlib/contrib/model/pytorch_hats.py @@ -175,7 +175,7 @@ class HATS(Model): daily_shuffle = list(zip(daily_index, daily_count)) np.random.shuffle(daily_shuffle) daily_index, daily_count = zip(*daily_shuffle) - return daily_index, daily_count + return daily_index, daily_count def train_epoch(self, x_train, y_train): From 52c7076917cbd88bab59b3707285396b71e73a55 Mon Sep 17 00:00:00 2001 From: bxdd Date: Thu, 26 Nov 2020 21:15:21 +0800 Subject: [PATCH 180/241] dnn model opz --- .../benchmarks/DNN/workflow_config_dnn.yaml | 14 ++++++- qlib/contrib/model/pytorch_nn.py | 39 +++++++++++-------- qlib/data/dataset/processor.py | 16 +++++++- 3 files changed, 49 insertions(+), 20 deletions(-) diff --git a/examples/benchmarks/DNN/workflow_config_dnn.yaml b/examples/benchmarks/DNN/workflow_config_dnn.yaml index 0f9ae7254..bf5bd7c5f 100644 --- a/examples/benchmarks/DNN/workflow_config_dnn.yaml +++ b/examples/benchmarks/DNN/workflow_config_dnn.yaml @@ -1,4 +1,4 @@ -provider_uri: "~/.qlib/qlib_data/cn_data" +provider_uri: "~/.qlib/qlib_data/cn_data_new" region: cn market: &market csi300 benchmark: &benchmark SH000300 @@ -8,6 +8,18 @@ data_handler_config: &data_handler_config fit_start_time: 2008-01-01 fit_end_time: 2014-12-31 instruments: *market + infer_processors: [ + { + "class" : "CSZFillna", + "kwargs":{"fields_group": "feature"} + }, + { + "class" : "Fillna", + "kwargs":{"fields_group": "feature"} + } + ] + learn_processors: ["DropnaLabel", {"class": "CSZScoreNorm", "kwargs": {"fields_group": "label"}}] + port_analysis_config: &port_analysis_config strategy: class: TopkDropoutStrategy diff --git a/qlib/contrib/model/pytorch_nn.py b/qlib/contrib/model/pytorch_nn.py index 9bad755b6..e1b0736e2 100644 --- a/qlib/contrib/model/pytorch_nn.py +++ b/qlib/contrib/model/pytorch_nn.py @@ -49,7 +49,7 @@ class DNNModelPytorch(Model): self, input_dim, output_dim, - layers=(256, 512, 768, 1024, 768, 512, 256, 128, 64), + layers=(256, 512, 768, 512, 256, 128, 64), lr=0.001, max_steps=300, batch_size=2000, @@ -78,7 +78,7 @@ class DNNModelPytorch(Model): self.optimizer = optimizer.lower() self.loss_type = loss self.visible_GPU = GPU - self.use_gpu = torch.cuda.is_available() + self.use_GPU = torch.cuda.is_available() self.logger.info( "DNN parameters setting:" @@ -107,7 +107,7 @@ class DNNModelPytorch(Model): loss, eval_steps, GPU, - self.use_gpu, + self.use_GPU, ) ) @@ -138,7 +138,7 @@ class DNNModelPytorch(Model): ) self._fitted = False - if self.use_gpu: + if self.use_GPU: self.dnn_model.cuda() # set the visible GPU if self.visible_GPU: @@ -157,7 +157,6 @@ class DNNModelPytorch(Model): ) x_train, y_train = df_train["feature"], df_train["label"] x_valid, y_valid = df_valid["feature"], df_valid["label"] - try: wdf_train, wdf_valid = dataset.prepare(["train", "valid"], col_set=["weight"], data_key=DataHandlerLP.DK_L) w_train, w_valid = wdf_train["weight"], wdf_valid["weight"] @@ -181,13 +180,14 @@ class DNNModelPytorch(Model): y_train_values = torch.from_numpy(y_train.values).float() w_train_values = torch.from_numpy(w_train.values).float() train_num = y_train_values.shape[0] - # prepare validation data x_val_auto = torch.from_numpy(x_valid.values).float() y_val_auto = torch.from_numpy(y_valid.values).float() w_val_auto = torch.from_numpy(w_valid.values).float() - - if self.use_gpu: + #print('valiadationx:', x_val_auto) + #print('valiadationy:', y_val_auto) + #print('valiadationw:', w_val_auto) + if self.use_GPU: x_val_auto = x_val_auto.cuda() y_val_auto = y_val_auto.cuda() w_val_auto = w_val_auto.cuda() @@ -206,13 +206,15 @@ class DNNModelPytorch(Model): y_batch_auto = y_train_values[choice] w_batch_auto = w_train_values[choice] - if self.use_gpu: - x_batch_auto = x_batch_auto.float().cuda() - y_batch_auto = y_batch_auto.float().cuda() - w_batch_auto = w_batch_auto.float().cuda() + if self.use_GPU: + x_batch_auto = x_batch_auto.cuda() + y_batch_auto = y_batch_auto.cuda() + w_batch_auto = w_batch_auto.cuda() # forward preds = self.dnn_model(x_batch_auto) + #print('pred_train:', preds.detach().cpu().numpy()) + #print('label_train:', y_batch_auto.cpu().numpy()) cur_loss = self.get_loss(preds, w_batch_auto, y_batch_auto, self.loss_type) cur_loss.backward() self.train_optimizer.step() @@ -230,6 +232,7 @@ class DNNModelPytorch(Model): loss_val = AverageMeter() # forward + preds = self.dnn_model(x_val_auto) cur_loss_val = self.get_loss(preds, w_val_auto, y_val_auto, self.loss_type) loss_val.update(cur_loss_val.item()) @@ -255,7 +258,7 @@ class DNNModelPytorch(Model): # restore the optimal parameters after training ?? self.dnn_model.load_state_dict(torch.load(save_path)) - if self.use_gpu: + if self.use_GPU: torch.cuda.empty_cache() def get_loss(self, pred, w, target, loss_type): @@ -273,16 +276,18 @@ class DNNModelPytorch(Model): if not self._fitted: raise ValueError("model is not fitted yet!") x_test_pd = dataset.prepare("test", col_set="feature") + print(x_test_pd) x_test = torch.from_numpy(x_test_pd.values).float() - if self.use_gpu: + if self.use_GPU: x_test = x_test.cuda() self.dnn_model.eval() with torch.no_grad(): - if self.use_gpu: + if self.use_GPU: preds = self.dnn_model(x_test).detach().cpu().numpy() else: preds = self.dnn_model(x_test).detach().numpy() + print(preds) return pd.Series(np.squeeze(preds), index=x_test_pd.index) def save(self, filename, **kwargs): @@ -331,7 +336,7 @@ class Net(nn.Module): dnn_layers.append(drop_input) for i, (input_dim, hidden_units) in enumerate(zip(layers[:-1], layers[1:])): fc = nn.Linear(input_dim, hidden_units) - activation = nn.ReLU() + activation = nn.LeakyReLU(negative_slope=0.1, inplace=False) bn = nn.BatchNorm1d(hidden_units) seq = nn.Sequential(fc, bn, activation) dnn_layers.append(seq) @@ -354,7 +359,7 @@ class Net(nn.Module): def _weight_init(self): for m in self.modules(): if isinstance(m, nn.Linear): - nn.init.xavier_normal_(m.weight, gain=1) + nn.init.kaiming_normal_(m.weight, a=0.1, mode='fan_in', nonlinearity='leaky_relu') def forward(self, x): cur_output = x diff --git a/qlib/data/dataset/processor.py b/qlib/data/dataset/processor.py index 2201c0891..32b42462f 100755 --- a/qlib/data/dataset/processor.py +++ b/qlib/data/dataset/processor.py @@ -90,6 +90,7 @@ class DropnaLabel(DropnaProcessor): return False + class TanhProcess(Processor): """ Use tanh to process noise data""" @@ -122,7 +123,6 @@ class ProcessInf(Processor): return replace_inf(df) - class Fillna(Processor): """Process NaN""" @@ -202,7 +202,8 @@ class CSZScoreNorm(Processor): def __call__(self, df): # try not modify original dataframe cols = get_group_columns(df, self.fields_group) - df[cols] = df[cols].groupby("datetime").apply(lambda df: (df - df.mean()).div(df.std())) + df[cols] = df[cols].groupby("datetime").apply(lambda x: (x - x.mean()).div(x.std())) + return df @@ -220,3 +221,14 @@ class CSRankNorm(Processor): t *= 3.46 # NOTE: towards unit std df[cols] = t return df + +class CSZFillna(Processor): + """Cross Sectional Fill Nan""" + + def __init__(self, fields_group=None): + self.fields_group = fields_group + + def __call__(self, df): + cols = get_group_columns(df, self.fields_group) + df[cols] = df[cols].groupby("datetime").apply(lambda x: x.fillna(x.mean())) + return df \ No newline at end of file From 38cfb22cba1574563ce25abf0dd07d380d4f45bd Mon Sep 17 00:00:00 2001 From: lwwang1995 Date: Thu, 26 Nov 2020 21:34:16 +0800 Subject: [PATCH 181/241] Update setting for model training. --- examples/workflow_by_code_gru.py | 2 +- qlib/contrib/model/pytorch_gru.py | 9 ++------- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/examples/workflow_by_code_gru.py b/examples/workflow_by_code_gru.py index fdd0d9220..dece520d1 100644 --- a/examples/workflow_by_code_gru.py +++ b/examples/workflow_by_code_gru.py @@ -70,7 +70,7 @@ if __name__ == "__main__": "lr": 1e-3, "early_stop": 20, "batch_size": 800, - "metric": "IC", + "metric": "loss", "loss": "mse", "seed": 0, "GPU": 0, diff --git a/qlib/contrib/model/pytorch_gru.py b/qlib/contrib/model/pytorch_gru.py index 2dd8464e2..282ce72dd 100755 --- a/qlib/contrib/model/pytorch_gru.py +++ b/qlib/contrib/model/pytorch_gru.py @@ -46,7 +46,7 @@ class GRU(Model): dropout=0.0, n_epochs=200, lr=0.001, - metric="IC", + metric="", batch_size=2000, early_stop=20, loss="mse", @@ -140,21 +140,17 @@ class GRU(Model): def metric_fn(self, pred, label): mask = torch.isfinite(label) - if self.metric == "IC": - return self.cal_ic(pred[mask], label[mask]) if self.metric == "" or self.metric == "loss": # use loss return -self.loss_fn(pred[mask], label[mask]) raise ValueError("unknown metric `%s`" % self.metric) - def cal_ic(self, pred, label): - return torch.mean(pred * label) def train_epoch(self, x_train, y_train): x_train_values = x_train.values - y_train_values = np.squeeze(y_train.values) * 100 + y_train_values = np.squeeze(y_train.values) self.gru_model.train() @@ -193,7 +189,6 @@ class GRU(Model): losses = [] indices = np.arange(len(x_values)) - np.random.shuffle(indices) for i in range(len(indices))[:: self.batch_size]: From 07c1ca69a709a54fd0f188c1cad06e950bfc3057 Mon Sep 17 00:00:00 2001 From: Dong Zhou Date: Thu, 26 Nov 2020 22:20:59 +0800 Subject: [PATCH 182/241] alpha158 & alpha360 support custom label --- qlib/contrib/data/handler.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/qlib/contrib/data/handler.py b/qlib/contrib/data/handler.py index 07ef2267a..5e6616b41 100644 --- a/qlib/contrib/data/handler.py +++ b/qlib/contrib/data/handler.py @@ -126,6 +126,7 @@ class ALPHA360(DataHandlerLP): learn_processors=_DEFAULT_LEARN_PROCESSORS, fit_start_time=None, fit_end_time=None, + **kwargs, ): infer_processors = check_transform_proc(infer_processors, fit_start_time, fit_end_time) learn_processors = check_transform_proc(learn_processors, fit_start_time, fit_end_time) @@ -135,7 +136,7 @@ class ALPHA360(DataHandlerLP): "kwargs": { "config": { "feature": self.get_feature_config(), - "label": self.get_label_config(), + "label": kwargs.get("label", self.get_label_config()), }, }, } @@ -206,6 +207,7 @@ class Alpha158(DataHandlerLP): learn_processors=_DEFAULT_LEARN_PROCESSORS, fit_start_time=None, fit_end_time=None, + **kwargs, ): infer_processors = check_transform_proc(infer_processors, fit_start_time, fit_end_time) learn_processors = check_transform_proc(learn_processors, fit_start_time, fit_end_time) @@ -213,7 +215,7 @@ class Alpha158(DataHandlerLP): data_loader = { "class": "QlibDataLoader", "kwargs": { - "config": {"feature": self.get_feature_config(), "label": self.get_label_config()}, + "config": {"feature": self.get_feature_config(), "label": kwargs.get("label", self.get_label_config())}, }, } super().__init__( From dae28139dab3e308593b9d8e566a93f2fdea28c0 Mon Sep 17 00:00:00 2001 From: Dong Zhou Date: Thu, 26 Nov 2020 22:22:39 +0800 Subject: [PATCH 183/241] black format --- qlib/contrib/model/pytorch_gru.py | 1 - 1 file changed, 1 deletion(-) diff --git a/qlib/contrib/model/pytorch_gru.py b/qlib/contrib/model/pytorch_gru.py index 282ce72dd..02664b6ac 100755 --- a/qlib/contrib/model/pytorch_gru.py +++ b/qlib/contrib/model/pytorch_gru.py @@ -146,7 +146,6 @@ class GRU(Model): raise ValueError("unknown metric `%s`" % self.metric) - def train_epoch(self, x_train, y_train): x_train_values = x_train.values From e25fc3b243728d7c4268df1ddc3c9ccdc500ed91 Mon Sep 17 00:00:00 2001 From: Alex Wang Date: Thu, 26 Nov 2020 22:28:34 +0800 Subject: [PATCH 184/241] update config --- qlib/contrib/model/pytorch_sfm.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qlib/contrib/model/pytorch_sfm.py b/qlib/contrib/model/pytorch_sfm.py index 7fbbd7c6e..e013238fa 100644 --- a/qlib/contrib/model/pytorch_sfm.py +++ b/qlib/contrib/model/pytorch_sfm.py @@ -340,7 +340,7 @@ class SFM(Model): def train_epoch(self, x_train, y_train): x_train_values = x_train.values - y_train_values = np.squeeze(y_train.values) * 100 + y_train_values = np.squeeze(y_train.values) self.sfm_model.train() From f5ec21913553eb2ccb35afc49cc7f5fa630cb76e Mon Sep 17 00:00:00 2001 From: Jactus Date: Thu, 26 Nov 2020 22:45:40 +0800 Subject: [PATCH 185/241] Update docs and README --- README.md | 23 ++++++++++++-- docs/component/data.rst | 56 +++++++++++++++++++++++++++++++---- docs/start/initialization.rst | 8 +++-- 3 files changed, 76 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index fd20e7c3a..3e0c985c8 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,8 @@ For more details, please refer to our paper ["Qlib: An AI-oriented Quantitative - [Data Preparation](#data-preparation) - [Auto Quant Research Workflow](#auto-quant-research-workflow) - [Building Customized Quant Research Workflow by Code](#building-customized-quant-research-workflow-by-code) -- [Quant Model Zoo](#quant-model-zoo) + - [Run a single model](#run-a-single-model) + - [Run multiple models](#run-multiple-models) - [Quant Dataset Zoo](#quant-dataset-zoo) - [More About Qlib](#more-about-qlib) - [Offline Mode and Online Mode](#offline-mode-and-online-mode) @@ -188,7 +189,25 @@ Qlib provides a tool named `qrun` to run the whole workflow automatically (inclu The automatic workflow may not suite the research workflow of all Quant researchers. To support a flexible Quant research workflow, Qlib also provides a modularized interface to allow researchers to build their own workflow by code. [Here](examples/workflow_by_code.ipynb) is a demo for customized Quant research workflow by code. -# Quant Model Zoo +[# Quant Model Zoo](examples/benchmarks) + +## Run a single model +`Qlib` provides three different ways to run a single model, users can pick the one that fits their cases best: +- User can use the tool `qrun` mentioned above to run a model's workflow based from a config file. +- User can create a `workflow_by_code` python script based on the [one](examples/workflow_by_code.py) listed in the `examples` folder. +- User can use the script [`run_all_model.py`](examples/run_all_model.py) listed in the `examples` folder to run a model. Here is an example of the specific shell command to be used: `python run_all_model.py --models=lightgbm`. For more use cases, please refer to the file's [docstrings](examples/run_all_model.py). + +## Run multiple models +`Qlib` also provides a script [`run_all_model.py`](examples/run_all_model.py) which can run multiple models for several iterations. (**Note**: the script only supprots *Linux* now. Other OS will be supported in the future.) + +The script will create a unique virtual environment for each model, and delete the environments after training. Thus, only experiment results such as `IC` and `backtest` results will be generated and stored. + +Here is an example of running all the models for 10 iterations: +```python +python run_all_model.py 10 +``` + +It also provides the API to run specific models at once. For more use cases, please refer to the file's [docstrings](examples/run_all_model.py). Here is a list of models built on `Qlib`. - [GBDT based on LightGBM](qlib/contrib/model/gbdt.py) diff --git a/docs/component/data.rst b/docs/component/data.rst index 3323211d6..aa01fe226 100644 --- a/docs/component/data.rst +++ b/docs/component/data.rst @@ -33,13 +33,19 @@ Such data will be stored with filename suffix `.bin` (We'll call them `.bin` fil Qlib Format Dataset -------------------- -``Qlib`` has provided an off-the-shelf dataset in `.bin` format, users could use the script ``scripts/get_data.py`` to download the dataset as follows. +``Qlib`` has provided an off-the-shelf dataset in `.bin` format, users could use the script ``scripts/get_data.py`` to download the China-Stock dataset as follows. .. code-block:: bash python scripts/get_data.py qlib_data --target_dir ~/.qlib/qlib_data/cn_data --region cn -After running the above command, users can find china-stock data in Qlib format in the ``~/.qlib/csv_data/cn_data`` directory. +In addition to China-Stock data, ``Qlib`` also includes a US-Stock dataset, which can be downloaded with the following command: + +.. code-block:: bash + + python scripts/get_data.py qlib_data --target_dir ~/.qlib/qlib_data/us_data --region us + +After running the above command, users can find china-stock and us-stock data in Qlib format in the ``~/.qlib/csv_data/cn_data`` directory and ``~/.qlib/csv_data/us_data`` directory respectively. ``Qlib`` also provides the scripts in ``scripts/data_collector`` to help users crawl the latest data on the Internet and convert it to qlib format. @@ -51,12 +57,45 @@ Converting CSV Format into Qlib Format ``Qlib`` has provided the script ``scripts/dump_bin.py`` to convert data in CSV format into `.bin` files (Qlib format). -Users can download the china-stock data in CSV format as follows for reference to the CSV format. +Users can download the demo china-stock data in CSV format as follows for reference to the CSV format. .. code-block:: bash python scripts/get_data.py csv_data_cn --target_dir ~/.qlib/csv_data/cn_data +Users can also provide their own data in CSV format. However, the CSV data **must satisfies** following criterions: + +- CSV file is named after a specific stock *or* the CSV file includes a column of the stock name + + - Name the CSV file after a stock: `SH600000.csv`, `AAPL.csv` (not case sensitive). + + - CSV file includes a column of the stock name. User **must** specify the column name when dumping the data. Here is an example: + + .. code-block:: bash + + python scripts/dump_bin.py dump_all ... --symbol_field_name symbol + + where the data are in the following format: + + .. code-block:: + + symbol,close + SH600000,120 + +- CSV file **must** includes a column for the date, and when dumping the data, user must specify the date column name. Here is an example: + + .. code-block:: bash + + python scripts/dump_bin.py dump_all ... --date_field_name date + + where the data are in the following format: + + .. code-block:: + + symbol,date,close,open,volume + SH600000,2020-11-01,120,121,12300000 + SH600000,2020-11-02,123,120,12300000 + Supposed that users prepare their CSV format data in the directory ``~/.qlib/csv_data/my_data``, they can run the following command to start the conversion. @@ -64,6 +103,12 @@ Supposed that users prepare their CSV format data in the directory ``~/.qlib/csv python scripts/dump_bin.py dump_all --csv_path ~/.qlib/csv_data/my_data --qlib_dir ~/.qlib/qlib_data/my_data --include_fields open,close,high,low,volume,factor +For other supported parameters when dumping the data into `.bin` file, users can refer to the information by running the following commands: + +.. code-block:: bash + + python dump_bin.py dump_all --help + After conversion, users can find their Qlib format data in the directory `~/.qlib/qlib_data/my_data`. .. note:: @@ -99,9 +144,8 @@ China-Stock Mode & US-Stock Mode qlib.init(provider_uri='~/.qlib/qlib_data/cn_data', region=REG_CN) -- If users use ``Qlib`` in US-stock mode, US-stock data is required. ``Qlib`` does not provide a script to download US-stock data. Users can use ``Qlib`` in US-stock mode according to the following steps: - - Prepare data in CSV format - - Convert data from CSV format to Qlib format, please refer to section `Converting CSV Format into Qlib Format <#converting-csv-format-into-qlib-format>`_. +- If users use ``Qlib`` in US-stock mode, US-stock data is required. ``Qlib`` also provides a script to download US-stock data. Users can use ``Qlib`` in US-stock mode according to the following steps: + - Download china-stock in qlib format, please refer to section `Qlib Format Dataset <#qlib-format-dataset>`_. - Initialize ``Qlib`` in US-stock mode Supposed that users prepare their Qlib format data in the directory ``~/.qlib/csv_data/us_data``. Users only need to initialize ``Qlib`` as follows. diff --git a/docs/start/initialization.rst b/docs/start/initialization.rst index af89a098e..423d7edf8 100644 --- a/docs/start/initialization.rst +++ b/docs/start/initialization.rst @@ -12,14 +12,16 @@ Initialization Please follow the steps below to initialize ``Qlib``. -- Download and prepare the Data: execute the following command to download stock data. Please pay `attention` that the data is collected from `Yahoo Finance `_ and the data might not be perfect. We recommend users to prepare their own data if they have high-quality datasets. Please refer to `Data <../component/data.html#converting-csv-format-into-qlib-format>` for more information about customized dataset. +Download and prepare the Data: execute the following command to download stock data. Please pay `attention` that the data is collected from `Yahoo Finance `_ and the data might not be perfect. We recommend users to prepare their own data if they have high-quality datasets. Please refer to `Data <../component/data.html#converting-csv-format-into-qlib-format>`_ for more information about customized dataset. + .. code-block:: bash python scripts/get_data.py qlib_data --target_dir ~/.qlib/qlib_data/cn_data --region cn - Please refer to `Data Preparation <../component/data.html#data-preparation>`_ for more information about `get_data.py`, + +Please refer to `Data Preparation <../component/data.html#data-preparation>`_ for more information about `get_data.py`, -- Initialize Qlib before calling other APIs: run following code in python. +Initialize Qlib before calling other APIs: run following code in python. .. code-block:: Python From b0c09c0d6a5ddebebc6b9baa3b0de240e3a67f9d Mon Sep 17 00:00:00 2001 From: Jactus Date: Thu, 26 Nov 2020 22:48:59 +0800 Subject: [PATCH 186/241] Fix --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 3e0c985c8..c890afaca 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,7 @@ For more details, please refer to our paper ["Qlib: An AI-oriented Quantitative - [Data Preparation](#data-preparation) - [Auto Quant Research Workflow](#auto-quant-research-workflow) - [Building Customized Quant Research Workflow by Code](#building-customized-quant-research-workflow-by-code) +- [Quant Model Zoo](#quant-model-zoo) - [Run a single model](#run-a-single-model) - [Run multiple models](#run-multiple-models) - [Quant Dataset Zoo](#quant-dataset-zoo) @@ -189,7 +190,7 @@ Qlib provides a tool named `qrun` to run the whole workflow automatically (inclu The automatic workflow may not suite the research workflow of all Quant researchers. To support a flexible Quant research workflow, Qlib also provides a modularized interface to allow researchers to build their own workflow by code. [Here](examples/workflow_by_code.ipynb) is a demo for customized Quant research workflow by code. -[# Quant Model Zoo](examples/benchmarks) +# [Quant Model Zoo](examples/benchmarks) ## Run a single model `Qlib` provides three different ways to run a single model, users can pick the one that fits their cases best: From 814ecbb488afd5d824c815d122504f91169174bd Mon Sep 17 00:00:00 2001 From: Young Date: Thu, 26 Nov 2020 16:00:58 +0000 Subject: [PATCH 187/241] intro doc & abs cli --- docs/introduction/introduction.rst | 34 +++++++++++++++--------------- qlib/workflow/cli.py | 2 +- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/docs/introduction/introduction.rst b/docs/introduction/introduction.rst index 3e4d11e28..06fac46fa 100644 --- a/docs/introduction/introduction.rst +++ b/docs/introduction/introduction.rst @@ -21,27 +21,27 @@ Framework At the module level, Qlib is a platform that consists of above components. The components are designed as loose-coupled modules and each component could be used stand-alone. -====================== ============================================================================== -Name Description -====================== ============================================================================== -`Data layer` `DataServer` focuses on providing high-performance infrastructure for users to - manage and retrieve raw data. `DataEnhancement` will preprocess the data and - provide the best dataset to be fed into the models. -`Interday Model` `Interday model` focuses on producing prediction scores (aka. `alpha`). Models - are trained by `Model Creator` and managed by `Model Manager`. Users could - choose one or multiple models for prediction. Multiple models could be combined - with `Ensemble` module. -`Interday Strategy` `Portfolio Generator` will take prediction scores as input and output the - orders based on the current position to achieve the target portfolio. +======================== ============================================================================== +Name Description +======================== ============================================================================== +`Infrastructure` layer `Infrastructure` layer provides underlying support for Quant research. + `DataServer` provides high-performance infrastructure for users to manage + and retrieve raw data. `Trainer` provides flexible interface to control + the training process of models which enable algorithms controlling the + training process. -`Intraday Trading` `Order Executor` is responsible for executing orders output by - `Interday Strategy` and returning the executed results. +`Workflow` layer `Workflow` layer covers the whole workflow of quantitative investment. + `Information Extractor` extracts data for models. `Forecast Model` focuses + on producing all kinds of forecast signals (e.g. _alpha_, risk) for other + modules. With these signals `Portfolio Generator` will generate the target + portfolio and produce orders to be executed by `Order Executor`. -`Analysis` Users could get a detailed analysis report of forecasting signals and portfolios - in this part. -====================== ============================================================================== +`Interface` layer `Interface` layer tries to present a user-friendly interface for the underlying + system. `Analyser` module will provide users detailed analysis reports of + forecasting signals, portfolios and execution results +======================== ============================================================================== - The modules with hand-drawn style are under development and will be released in the future. - The modules with dashed borders are highly user-customizable and extendible. diff --git a/qlib/workflow/cli.py b/qlib/workflow/cli.py index b9c040e87..08c13de2a 100644 --- a/qlib/workflow/cli.py +++ b/qlib/workflow/cli.py @@ -8,7 +8,7 @@ import qlib import fire import pandas as pd import ruamel.yaml as yaml -from ..model.trainer import task_train +from qlib.model.trainer import task_train def get_path_list(path): From 5796363ecf1e533da0b9ab785968554787dd6196 Mon Sep 17 00:00:00 2001 From: Hong Zhang Date: Fri, 27 Nov 2020 00:19:23 +0800 Subject: [PATCH 188/241] revise settings --- examples/workflow_by_code_gats.py | 4 ++-- examples/workflow_by_code_hats.py | 6 +++--- qlib/contrib/model/pytorch_gats.py | 10 +++++----- qlib/contrib/model/pytorch_hats.py | 8 ++++---- qlib/contrib/model/pytorch_lstm.py | 4 ++-- 5 files changed, 16 insertions(+), 16 deletions(-) diff --git a/examples/workflow_by_code_gats.py b/examples/workflow_by_code_gats.py index 984c1755a..20f3ae552 100644 --- a/examples/workflow_by_code_gats.py +++ b/examples/workflow_by_code_gats.py @@ -61,9 +61,9 @@ if __name__ == "__main__": "d_feat": 6, "hidden_size": 64, "num_layers": 2, - "dropout": 0.0, + "dropout": 0.7, "n_epochs": 200, - "lr": 1e-3, + "lr": 1e-4, "early_stop": 20, "metric": "loss", "loss": "mse", diff --git a/examples/workflow_by_code_hats.py b/examples/workflow_by_code_hats.py index 192d97ee3..64bc860b4 100644 --- a/examples/workflow_by_code_hats.py +++ b/examples/workflow_by_code_hats.py @@ -58,11 +58,11 @@ if __name__ == "__main__": "d_feat": 6, "hidden_size": 64, "num_layers": 2, - "dropout": 0.6, + "dropout": 0.7, "n_epochs": 200, - "lr": 1e-3, + "lr": 1e-4, "early_stop": 20, - "metric": "IC", + "metric": "loss", "loss": "mse", "base_model": "LSTM", "seed": 0, diff --git a/qlib/contrib/model/pytorch_gats.py b/qlib/contrib/model/pytorch_gats.py index 5d2dbd9a4..d951f1873 100755 --- a/qlib/contrib/model/pytorch_gats.py +++ b/qlib/contrib/model/pytorch_gats.py @@ -43,13 +43,13 @@ class GAT(Model): d_feat=6, hidden_size=64, num_layers=2, - dropout=0.0, + dropout=0.7, n_epochs=200, - lr=0.001, - metric="IC", + lr=0.0001, + metric="loss", early_stop=20, loss="mse", - base_model="GRU", + base_model="LSTM", with_pretrain=True, optimizer="adam", GPU="0", @@ -174,7 +174,7 @@ class GAT(Model): def train_epoch(self, x_train, y_train): x_train_values = x_train.values - y_train_values = np.squeeze(y_train.values) * 100 + y_train_values = np.squeeze(y_train.values) self.GAT_model.train() # organize the train data into daily inter as daily batches diff --git a/qlib/contrib/model/pytorch_hats.py b/qlib/contrib/model/pytorch_hats.py index bdb68be28..1eff35203 100644 --- a/qlib/contrib/model/pytorch_hats.py +++ b/qlib/contrib/model/pytorch_hats.py @@ -52,11 +52,11 @@ class HATS(Model): num_layers=2, dropout=0.5, n_epochs=200, - lr=0.01, - metric="IC", + lr=0.0001, + metric="loss", early_stop=20, loss="mse", - base_model="GRU", + base_model="LSTM", with_pretrain=True, optimizer="adam", GPU="0", @@ -180,7 +180,7 @@ class HATS(Model): def train_epoch(self, x_train, y_train): x_train_values = x_train.values - y_train_values = np.squeeze(y_train.values) * 100 + y_train_values = np.squeeze(y_train.values) self.HATS_model.train() diff --git a/qlib/contrib/model/pytorch_lstm.py b/qlib/contrib/model/pytorch_lstm.py index be43d3698..22cc21e7f 100755 --- a/qlib/contrib/model/pytorch_lstm.py +++ b/qlib/contrib/model/pytorch_lstm.py @@ -46,7 +46,7 @@ class LSTM(Model): dropout=0.0, n_epochs=200, lr=0.001, - metric="IC", + metric="loss", batch_size=2000, early_stop=20, loss="mse", @@ -154,7 +154,7 @@ class LSTM(Model): def train_epoch(self, x_train, y_train): x_train_values = x_train.values - y_train_values = np.squeeze(y_train.values) * 100 + y_train_values = np.squeeze(y_train.values) self.lstm_model.train() From 6b4156ab9fb5dbe3062dbe4bc347fc6ddcb8cf86 Mon Sep 17 00:00:00 2001 From: Dong Zhou Date: Fri, 27 Nov 2020 08:58:44 +0800 Subject: [PATCH 189/241] black format --- qlib/contrib/model/pytorch_hats.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qlib/contrib/model/pytorch_hats.py b/qlib/contrib/model/pytorch_hats.py index 1eff35203..a0da88dbf 100644 --- a/qlib/contrib/model/pytorch_hats.py +++ b/qlib/contrib/model/pytorch_hats.py @@ -180,7 +180,7 @@ class HATS(Model): def train_epoch(self, x_train, y_train): x_train_values = x_train.values - y_train_values = np.squeeze(y_train.values) + y_train_values = np.squeeze(y_train.values) self.HATS_model.train() From ad210923756aa04287e98c1b0878c1ecc45c83c7 Mon Sep 17 00:00:00 2001 From: Dong Zhou Date: Fri, 27 Nov 2020 09:00:43 +0800 Subject: [PATCH 190/241] datahandler support explicit selector & PortAnaRecord support strategy instance --- qlib/data/dataset/utils.py | 3 +++ qlib/workflow/record_temp.py | 3 ++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/qlib/data/dataset/utils.py b/qlib/data/dataset/utils.py index 3fb3768a0..feda19044 100644 --- a/qlib/data/dataset/utils.py +++ b/qlib/data/dataset/utils.py @@ -51,6 +51,9 @@ def fetch_df_by_index( ------- Data of the given index. """ + # level = None -> use selector directly + if level == None: + return df.loc(axis=0)[selector] # Try to get the right index idx_slc = (selector, slice(None, None)) if get_level_index(df, level) == 1: diff --git a/qlib/workflow/record_temp.py b/qlib/workflow/record_temp.py index 1d0811d16..ec76343bd 100644 --- a/qlib/workflow/record_temp.py +++ b/qlib/workflow/record_temp.py @@ -15,6 +15,7 @@ from ..utils import init_instance_by_config, get_module_by_module_path from ..log import get_module_logger from ..utils import flatten_dict from ..contrib.eva.alpha import calc_ic, calc_long_short_return +from ..contrib.strategy.strategy import BaseStrategy logger = get_module_logger("workflow", "INFO") @@ -220,7 +221,7 @@ class PortAnaRecord(SignalRecord): self.strategy_config = config["strategy"] self.backtest_config = config["backtest"] - self.strategy = init_instance_by_config(self.strategy_config) + self.strategy = init_instance_by_config(self.strategy_config, accept_types=BaseStrategy) def generate(self, **kwargs): # check previously stored prediction results From d5adc4934b27cc5411d47266d1cf6e6399ff9362 Mon Sep 17 00:00:00 2001 From: Dong Zhou Date: Fri, 27 Nov 2020 09:01:35 +0800 Subject: [PATCH 191/241] add portfolio optimization example (WIP) --- examples/portfolio_optimization_example.ipynb | 492 ++++++++++++++++++ 1 file changed, 492 insertions(+) create mode 100644 examples/portfolio_optimization_example.ipynb diff --git a/examples/portfolio_optimization_example.ipynb b/examples/portfolio_optimization_example.ipynb new file mode 100644 index 000000000..d507f369c --- /dev/null +++ b/examples/portfolio_optimization_example.ipynb @@ -0,0 +1,492 @@ +{ + "metadata": { + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.7.9-final" + }, + "orig_nbformat": 2, + "kernelspec": { + "name": "python3", + "display_name": "Python 3" + } + }, + "nbformat": 4, + "nbformat_minor": 2, + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import sys\n", + "import copy\n", + "from pathlib import Path\n", + "\n", + "import qlib\n", + "import numpy as np\n", + "import pandas as pd\n", + "from qlib.config import REG_CN\n", + "from qlib.contrib.model.gbdt import LGBModel\n", + "from qlib.contrib.data.handler import Alpha158\n", + "from qlib.contrib.strategy.strategy import TopkDropoutStrategy\n", + "from qlib.contrib.evaluate import (\n", + " backtest as normal_backtest,\n", + " risk_analysis,\n", + ")\n", + "from qlib.utils import exists_qlib_data, init_instance_by_config\n", + "from qlib.workflow import R\n", + "from qlib.workflow.record_temp import SignalRecord, PortAnaRecord\n", + "from qlib.utils import flatten_dict" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "output_type": "stream", + "name": "stderr", + "text": [ + "[20768:MainThread](2020-11-27 08:15:01,096) INFO - qlib.Initialization - [__init__.py:41] - default_conf: client.\n", + "[20768:MainThread](2020-11-27 08:15:03,120) WARNING - qlib.Initialization - [__init__.py:57] - redis connection failed(host=127.0.0.1 port=6379), cache will not be used!\n", + "[20768:MainThread](2020-11-27 08:15:03,121) INFO - qlib.Initialization - [__init__.py:76] - qlib successfully initialized based on client settings.\n", + "[20768:MainThread](2020-11-27 08:15:03,122) INFO - qlib.Initialization - [__init__.py:79] - data_path=C:\\Users\\v-donzh\\.qlib\\qlib_data\\cn_data\n" + ] + } + ], + "source": [ + "# use default data\n", + "# NOTE: need to download data from remote: python scripts/get_data.py qlib_data_cn --target_dir ~/.qlib/qlib_data/cn_data\n", + "provider_uri = \"~/.qlib/qlib_data/cn_data\" # target_dir\n", + "if not exists_qlib_data(provider_uri):\n", + " print(f\"Qlib data is not found in {provider_uri}\")\n", + " sys.path.append(str(Path.cwd().parent.joinpath(\"scripts\")))\n", + " from get_data import GetData\n", + " GetData().qlib_data(target_dir=provider_uri, region=REG_CN)\n", + "qlib.init(provider_uri=provider_uri, region=REG_CN)" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "market = \"csi300\"\n", + "benchmark = \"SH000300\"" + ] + }, + { + "source": [ + "## Model Training" + ], + "cell_type": "markdown", + "metadata": {} + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "output_type": "stream", + "name": "stderr", + "text": [ + "[20768:MainThread](2020-11-27 08:15:55,319) INFO - qlib.timer - [log.py:81] - Time cost: 52.158s | Loading data Done\n", + "[20768:MainThread](2020-11-27 08:15:56,107) INFO - qlib.timer - [log.py:81] - Time cost: 0.669s | DropnaLabel Done\n", + "[20768:MainThread](2020-11-27 08:15:59,716) INFO - qlib.timer - [log.py:81] - Time cost: 3.608s | CSZScoreNorm Done\n", + "[20768:MainThread](2020-11-27 08:15:59,717) INFO - qlib.timer - [log.py:81] - Time cost: 4.397s | fit & process data Done\n", + "[20768:MainThread](2020-11-27 08:15:59,717) INFO - qlib.timer - [log.py:81] - Time cost: 56.556s | Init data Done\n", + "[20768:MainThread](2020-11-27 08:15:59,722) INFO - qlib.workflow - [exp.py:180] - Experiment 1 starts running ...\n", + "[20768:MainThread](2020-11-27 08:16:00,133) INFO - qlib.workflow - [recorder.py:234] - Recorder 46e50379b45a4a7684c683cd423535a9 starts running under Experiment 1 ...\n", + "[20768:MainThread](2020-11-27 08:16:00,134) INFO - qlib.workflow - [expm.py:251] - No tracking URI is provided. The default tracking URI is set as `mlruns` under the working directory.\n", + "Training until validation scores don't improve for 50 rounds\n", + "[20]\ttrain's l2: 0.990556\tvalid's l2: 0.994299\n", + "[40]\ttrain's l2: 0.986919\tvalid's l2: 0.993683\n", + "[60]\ttrain's l2: 0.984485\tvalid's l2: 0.993495\n", + "[80]\ttrain's l2: 0.982363\tvalid's l2: 0.993365\n", + "[100]\ttrain's l2: 0.980538\tvalid's l2: 0.993251\n", + "[120]\ttrain's l2: 0.978755\tvalid's l2: 0.993265\n", + "[140]\ttrain's l2: 0.977079\tvalid's l2: 0.993324\n", + "[160]\ttrain's l2: 0.97535\tvalid's l2: 0.99336\n", + "Early stopping, best iteration is:\n", + "[118]\ttrain's l2: 0.978921\tvalid's l2: 0.993248\n" + ] + } + ], + "source": [ + "###################################\n", + "# train model\n", + "###################################\n", + "data_handler_config = {\n", + " \"start_time\": \"2008-01-01\",\n", + " \"end_time\": \"2020-08-01\",\n", + " \"fit_start_time\": \"2008-01-01\",\n", + " \"fit_end_time\": \"2014-12-31\",\n", + " \"instruments\": market,\n", + "}\n", + "\n", + "task = {\n", + " \"model\": {\n", + " \"class\": \"LGBModel\",\n", + " \"module_path\": \"qlib.contrib.model.gbdt\",\n", + " \"kwargs\": {\n", + " \"loss\": \"mse\",\n", + " \"colsample_bytree\": 0.8879,\n", + " \"learning_rate\": 0.0421,\n", + " \"subsample\": 0.8789,\n", + " \"lambda_l1\": 205.6999,\n", + " \"lambda_l2\": 580.9768,\n", + " \"max_depth\": 8,\n", + " \"num_leaves\": 210,\n", + " \"num_threads\": 20,\n", + " },\n", + " },\n", + " \"dataset\": {\n", + " \"class\": \"DatasetH\",\n", + " \"module_path\": \"qlib.data.dataset\",\n", + " \"kwargs\": {\n", + " \"handler\": {\n", + " \"class\": \"Alpha158\",\n", + " \"module_path\": \"qlib.contrib.data.handler\",\n", + " \"kwargs\": data_handler_config,\n", + " },\n", + " \"segments\": {\n", + " \"train\": (\"2008-01-01\", \"2014-12-31\"),\n", + " \"valid\": (\"2015-01-01\", \"2016-12-31\"),\n", + " \"test\": (\"2017-01-01\", \"2020-08-01\"),\n", + " },\n", + " },\n", + " },\n", + "}\n", + "\n", + "# model initiaiton\n", + "model = init_instance_by_config(task[\"model\"])\n", + "dataset = init_instance_by_config(task[\"dataset\"])\n", + "\n", + "# start exp to train model\n", + "with R.start(experiment_name=\"train_model\"):\n", + " R.log_params(**flatten_dict(task))\n", + " model.fit(dataset)\n", + " R.save_objects(trained_model=model)\n", + " rid = R.get_recorder().id\n" + ] + }, + { + "source": [ + "## Optimization Based Strategy" + ], + "cell_type": "markdown", + "metadata": {} + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": {}, + "outputs": [], + "source": [ + "from qlib.contrib.strategy.strategy import BaseStrategy\n", + "\n", + "\n", + "class OptBasedStrategy(BaseStrategy):\n", + " \"\"\"Optimization Based Strategy\"\"\"\n", + "\n", + " def __init__(self, data_handler, cov_estimator, optimizer):\n", + " self.data_handler = data_handler\n", + " self.cov_estimator = cov_estimator\n", + " self.optimizer = optimizer\n", + "\n", + " def generate_order_list(self, score_series, current, trade_exchange, pred_date, trade_date):\n", + " \"\"\"\n", + " Parameters\n", + " -----------\n", + " score_series : pd.Seires\n", + " stock_id , score.\n", + " current : Position()\n", + " current of account.\n", + " trade_exchange : Exchange()\n", + " exchange.\n", + " trade_date : pd.Timestamp\n", + " date.\n", + " \"\"\"\n", + " score_series = score_series.dropna()\n", + "\n", + " # check stock holdings, if\n", + " # 1. doesn't have score: target amount = 0 (force sell)\n", + " # 2. stock not tradable: target amount = current amount\n", + " current_position = current.get_stock_amount_dict()\n", + " target_position = {}\n", + " for stock_id in current_position:\n", + " if not trade_exchange.is_stock_tradable(stock_id=stock_id, trade_date=trade_date):\n", + " target_position[stock_id] = current_position[stock_id]\n", + " elif stock_id not in score_series.index:\n", + " target_position[stock_id] = 0\n", + " else:\n", + " # need to be solved by optimizer\n", + " pass\n", + "\n", + " # filter scores, if\n", + " # 1. kept in `amount_dict` by previous rules\n", + " # 2. not tradable\n", + " skipped = []\n", + " for stock_id in score_series.index:\n", + " if stock_id in target_position:\n", + " skipped.append(stock_id)\n", + " elif not trade_exchange.is_stock_tradable(stock_id=stock_id, trade_date=trade_date):\n", + " skipped.append(stock_id)\n", + " score_series = score_series[~score_series.index.isin(skipped)]\n", + "\n", + " # calc remaining value\n", + " current_value = pd.Series({\n", + " stock_id: trade_exchange.get_close(stock_id, pred_date) * amount\n", + " for stock_id, amount in current_position.items()\n", + " })\n", + " risk_total_value = self.get_risk_degree(trade_date) * current.calculate_value()\n", + " traded_value = risk_total_value - current_value.loc[list(target_position)].sum()\n", + "\n", + " # portfolio init weight\n", + " init_weight = current_value.reindex(score_series.index, fill_value=0)\n", + " init_weight /= init_weight.sum() + 1e-12\n", + "\n", + " # covariance estimation\n", + " selector = (self.data_handler.get_range_selector(pred_date, 252), score_series.index)\n", + " price = self.data_handler.fetch(selector, level=None, squeeze=True)\n", + " cov = self.cov_estimator(price)\n", + " cov = cov.reindex(\n", + " index=score_series.index, \n", + " columns=score_series.index, \n", + " #fill_value=cov.max().max()\n", + " )\n", + "\n", + " # optimize target portfolio\n", + " target_weight = self.optimizer(cov, score_series, init_weight)\n", + " for stock_id, weight in target_weight.items():\n", + " try:\n", + " target_position[stock_id] = traded_value * weight / trade_exchange.get_close(stock_id, pred_date)\n", + " except Exception as e:\n", + " print(e)\n", + " target_position[stock_id] = 0\n", + " print(target_weight[target_weight>1e-4])\n", + "\n", + " # generate order list\n", + " order_list = trade_exchange.generate_order_for_target_amount_position(\n", + " target_position=target_position,\n", + " current_position=current_position,\n", + " trade_date=trade_date,\n", + " )\n", + "\n", + " return order_list\n" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [], + "source": [ + "from qlib.data.dataset.loader import QlibDataLoader\n", + "from qlib.data.dataset.handler import DataHandler\n", + "from qlib.model.riskmodel import ShrinkCovEstimator\n", + "from qlib.portfolio.optimizer import PortfolioOptimizer" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [ + { + "output_type": "stream", + "name": "stderr", + "text": [ + "[20768:MainThread](2020-11-27 08:45:28,512) INFO - qlib.timer - [log.py:81] - Time cost: 14.502s | Loading data Done\n", + "[20768:MainThread](2020-11-27 08:45:28,513) INFO - qlib.timer - [log.py:81] - Time cost: 14.503s | Init data Done\n" + ] + } + ], + "source": [ + "data_loader = QlibDataLoader([\"$close\"])\n", + "data_handler = DataHandler(\"all\", \"2015-01-01\", \"2020-08-01\", data_loader)\n", + "cov_estimator = ShrinkCovEstimator(nan_option=\"mask\")\n", + "optimizer = PortfolioOptimizer(\"mvo\", lamb=1.0, delta=0.2)\n", + "strategy = OptBasedStrategy(data_handler, cov_estimator, optimizer)" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [ + { + "output_type": "stream", + "name": "stderr", + "text": [ + "[20768:MainThread](2020-11-27 08:45:28,543) INFO - qlib.workflow - [exp.py:180] - Experiment 2 starts running ...\n", + "[20768:MainThread](2020-11-27 08:45:28,581) INFO - qlib.workflow - [recorder.py:234] - Recorder d9bd45391cf5431bb339531baf5fb6f2 starts running under Experiment 2 ...\n", + "[20768:MainThread](2020-11-27 08:45:28,582) INFO - qlib.workflow - [expm.py:251] - No tracking URI is provided. The default tracking URI is set as `mlruns` under the working directory.\n", + "[20768:MainThread](2020-11-27 08:45:29,433) INFO - qlib.workflow - [record_temp.py:127] - Signal record 'pred.pkl' has been saved as the artifact of the Experiment 2\n", + "[20768:MainThread](2020-11-27 08:45:29,525) INFO - qlib.Evaluate - [evaluate.py:161] - Create new exchange\n", + "'The following are prediction results of the LGBModel model.'\n", + " score\n", + "datetime instrument \n", + "2017-01-03 SH600000 -0.033506\n", + " SH600008 0.002120\n", + " SH600009 0.032941\n", + " SH600010 -0.012371\n", + " SH600015 -0.140312\n", + "C:\\Users\\v-donzh\\AppData\\Local\\Continuum\\miniconda3\\envs\\qlib\\lib\\site-packages\\ipykernel_launcher.py:55: DeprecationWarning: The default dtype for empty Series will be 'object' instead of 'float64' in a future version. Specify a dtype explicitly to silence this warning.\n", + "instrument\n", + "SH600000 2.868027e-12\n", + "SH600008 1.341080e-12\n", + "SH600009 5.131225e-12\n", + "SH600010 3.890177e-12\n", + "SH600015 1.781055e-11\n", + " ... \n", + "SZ300146 2.841964e-12\n", + "SZ300168 4.603490e-12\n", + "SZ300182 9.855511e-12\n", + "SZ300251 1.495177e-12\n", + "SZ300315 4.054219e-12\n", + "Length: 290, dtype: float64\n", + "instrument\n", + "SH600000 5.253178e-12\n", + "SH600008 1.901077e-14\n", + "SH600009 5.573006e-12\n", + "SH600010 6.129089e-14\n", + "SH600015 7.236246e-14\n", + " ... \n", + "SZ300146 4.604396e-12\n", + "SZ300168 4.705359e-12\n", + "SZ300182 6.358140e-14\n", + "SZ300251 5.347927e-12\n", + "SZ300315 1.288077e-13\n", + "Length: 289, dtype: float64\n", + "instrument\n", + "SH600000 1.181534e-14\n", + "SH600008 3.480454e-14\n", + "SH600009 1.902741e-13\n", + "SH600010 8.388080e-12\n", + "SH600015 1.490974e-13\n", + " ... \n", + "SZ300146 9.838926e-13\n", + "SZ300168 1.790169e-11\n", + "SZ300182 1.002664e-11\n", + "SZ300251 2.097283e-12\n", + "SZ300315 1.180997e-11\n", + "Length: 288, dtype: float64\n", + "instrument\n", + "SH600000 4.027211e-14\n", + "SH600008 1.067081e-14\n", + "SH600009 1.010989e-13\n", + "SH600010 1.605190e-12\n", + "SH600015 6.075465e-14\n", + " ... \n", + "SZ300146 7.338274e-12\n", + "SZ300168 1.990990e-11\n", + "SZ300182 4.891503e-12\n", + "SZ300251 1.067670e-11\n", + "SZ300315 1.009767e-11\n", + "Length: 288, dtype: float64\n", + "instrument\n", + "SH600000 2.518140e-12\n", + "SH600008 5.504712e-13\n", + "SH600009 3.185232e-12\n", + "SH600010 3.641971e-13\n", + "SH600015 1.902283e-14\n", + " ... \n", + "SZ300146 5.427494e-14\n", + "SZ300168 7.620411e-13\n", + "SZ300182 2.484372e-14\n", + "SZ300251 5.261806e-13\n", + "SZ300315 1.412130e-13\n", + "Length: 288, dtype: float64\n", + "('SH600666', Timestamp('2017-01-10 00:00:00'))\n", + "instrument\n", + "SH600000 7.466386e-12\n", + "SH600008 1.348026e-15\n", + "SH600009 1.111042e-11\n", + "SH600010 6.740600e-14\n", + "SH600015 1.049665e-11\n", + " ... \n", + "SZ300146 6.842294e-14\n", + "SZ300168 1.750970e-13\n", + "SZ300182 6.916267e-14\n", + "SZ300251 9.068748e-14\n", + "SZ300315 1.193522e-13\n", + "Length: 289, dtype: float64\n" + ] + }, + { + "output_type": "error", + "ename": "ValueError", + "evalue": "only have -0.104491644538939 SZ002475, require 1448416.2584415162", + "traceback": [ + "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[1;31mValueError\u001b[0m Traceback (most recent call last)", + "\u001b[1;32m\u001b[0m in \u001b[0;36m\u001b[1;34m\u001b[0m\n\u001b[0;32m 30\u001b[0m \u001b[1;31m# backtest & analysis\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 31\u001b[0m \u001b[0mpar\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mPortAnaRecord\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mrecorder\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mport_analysis_config\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m---> 32\u001b[1;33m \u001b[0mpar\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mgenerate\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m", + "\u001b[1;32md:\\qlib\\qlib\\workflow\\record_temp.py\u001b[0m in \u001b[0;36mgenerate\u001b[1;34m(self, **kwargs)\u001b[0m\n\u001b[0;32m 230\u001b[0m \u001b[1;31m# custom strategy and get backtest\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 231\u001b[0m \u001b[0mpred_score\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0msuper\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mload\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m--> 232\u001b[1;33m \u001b[0mreport_normal\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mpositions_normal\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mnormal_backtest\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mpred_score\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mstrategy\u001b[0m\u001b[1;33m=\u001b[0m\u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mstrategy\u001b[0m\u001b[1;33m,\u001b[0m \u001b[1;33m**\u001b[0m\u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mbacktest_config\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m 233\u001b[0m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mrecorder\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0msave_objects\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;33m**\u001b[0m\u001b[1;33m{\u001b[0m\u001b[1;34m\"report_normal.pkl\"\u001b[0m\u001b[1;33m:\u001b[0m \u001b[0mreport_normal\u001b[0m\u001b[1;33m}\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0martifact_path\u001b[0m\u001b[1;33m=\u001b[0m\u001b[0mPortAnaRecord\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mget_path\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 234\u001b[0m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mrecorder\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0msave_objects\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;33m**\u001b[0m\u001b[1;33m{\u001b[0m\u001b[1;34m\"positions_normal.pkl\"\u001b[0m\u001b[1;33m:\u001b[0m \u001b[0mpositions_normal\u001b[0m\u001b[1;33m}\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0martifact_path\u001b[0m\u001b[1;33m=\u001b[0m\u001b[0mPortAnaRecord\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mget_path\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n", + "\u001b[1;32md:\\qlib\\qlib\\contrib\\evaluate.py\u001b[0m in \u001b[0;36mbacktest\u001b[1;34m(pred, account, shift, benchmark, verbose, **kwargs)\u001b[0m\n\u001b[0;32m 269\u001b[0m \u001b[0mverbose\u001b[0m\u001b[1;33m=\u001b[0m\u001b[0mverbose\u001b[0m\u001b[1;33m,\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 270\u001b[0m \u001b[0maccount\u001b[0m\u001b[1;33m=\u001b[0m\u001b[0maccount\u001b[0m\u001b[1;33m,\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m--> 271\u001b[1;33m \u001b[0mbenchmark\u001b[0m\u001b[1;33m=\u001b[0m\u001b[0mbenchmark\u001b[0m\u001b[1;33m,\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m 272\u001b[0m )\n\u001b[0;32m 273\u001b[0m \u001b[1;31m# for compatibility of the old API. return the dict positions\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n", + "\u001b[1;32md:\\qlib\\qlib\\contrib\\backtest\\backtest.py\u001b[0m in \u001b[0;36mbacktest\u001b[1;34m(pred, strategy, trade_exchange, shift, verbose, account, benchmark)\u001b[0m\n\u001b[0;32m 107\u001b[0m \u001b[1;31m# NOTE: The following operation will modify order.amount.\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 108\u001b[0m \u001b[1;31m# NOTE: If it is buy and the cash is insufficient, the tradable amount will be recalculated\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m--> 109\u001b[1;33m \u001b[0mtrade_info\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mexecutor\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mexecute\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mtrade_account\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0morder_list\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mtrade_date\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m 110\u001b[0m \u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 111\u001b[0m \u001b[1;31m# 5. Update account information according to transaction\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n", + "\u001b[1;32md:\\qlib\\qlib\\contrib\\online\\executor.py\u001b[0m in \u001b[0;36mexecute\u001b[1;34m(self, trade_account, order_list, trade_date)\u001b[0m\n\u001b[0;32m 145\u001b[0m \u001b[1;32mif\u001b[0m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mtrade_exchange\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mcheck_order\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0morder\u001b[0m\u001b[1;33m)\u001b[0m \u001b[1;32mis\u001b[0m \u001b[1;32mTrue\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 146\u001b[0m \u001b[1;31m# execute the order\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m--> 147\u001b[1;33m \u001b[0mtrade_val\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mtrade_cost\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mtrade_price\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mtrade_exchange\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mdeal_order\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0morder\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mtrade_account\u001b[0m\u001b[1;33m=\u001b[0m\u001b[0maccount\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m 148\u001b[0m \u001b[0mtrade_info\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mappend\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;33m[\u001b[0m\u001b[0morder\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mtrade_val\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mtrade_cost\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mtrade_price\u001b[0m\u001b[1;33m]\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 149\u001b[0m \u001b[1;32mif\u001b[0m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mverbose\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n", + "\u001b[1;32md:\\qlib\\qlib\\contrib\\backtest\\exchange.py\u001b[0m in \u001b[0;36mdeal_order\u001b[1;34m(self, order, trade_account, position)\u001b[0m\n\u001b[0;32m 209\u001b[0m \u001b[1;31m# Otherwise, it will result some stock with 0 amount in the position\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 210\u001b[0m \u001b[1;32mif\u001b[0m \u001b[0mtrade_account\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m--> 211\u001b[1;33m \u001b[0mtrade_account\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mupdate_order\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0morder\u001b[0m\u001b[1;33m=\u001b[0m\u001b[0morder\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mtrade_val\u001b[0m\u001b[1;33m=\u001b[0m\u001b[0mtrade_val\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mcost\u001b[0m\u001b[1;33m=\u001b[0m\u001b[0mtrade_cost\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mtrade_price\u001b[0m\u001b[1;33m=\u001b[0m\u001b[0mtrade_price\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m 212\u001b[0m \u001b[1;32melif\u001b[0m \u001b[0mposition\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 213\u001b[0m \u001b[0mposition\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mupdate_order\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0morder\u001b[0m\u001b[1;33m=\u001b[0m\u001b[0morder\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mtrade_val\u001b[0m\u001b[1;33m=\u001b[0m\u001b[0mtrade_val\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mcost\u001b[0m\u001b[1;33m=\u001b[0m\u001b[0mtrade_cost\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mtrade_price\u001b[0m\u001b[1;33m=\u001b[0m\u001b[0mtrade_price\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n", + "\u001b[1;32md:\\qlib\\qlib\\contrib\\backtest\\account.py\u001b[0m in \u001b[0;36mupdate_order\u001b[1;34m(self, order, trade_val, cost, trade_price)\u001b[0m\n\u001b[0;32m 77\u001b[0m \u001b[1;31m# update current position\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 78\u001b[0m \u001b[1;31m# for may sell all of stock_id\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m---> 79\u001b[1;33m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mcurrent\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mupdate_order\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0morder\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mtrade_val\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mcost\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mtrade_price\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m 80\u001b[0m \u001b[1;32melse\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 81\u001b[0m \u001b[1;31m# buy stock\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n", + "\u001b[1;32md:\\qlib\\qlib\\contrib\\backtest\\position.py\u001b[0m in \u001b[0;36mupdate_order\u001b[1;34m(self, order, trade_val, cost, trade_price)\u001b[0m\n\u001b[0;32m 81\u001b[0m \u001b[1;32melif\u001b[0m \u001b[0morder\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mdirection\u001b[0m \u001b[1;33m==\u001b[0m \u001b[0mOrder\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mSELL\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 82\u001b[0m \u001b[1;31m# SELL\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m---> 83\u001b[1;33m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0msell_stock\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0morder\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mstock_id\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mtrade_val\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mcost\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mtrade_price\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m 84\u001b[0m \u001b[1;32melse\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 85\u001b[0m \u001b[1;32mraise\u001b[0m \u001b[0mNotImplementedError\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;34m\"do not suppotr order direction {}\"\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mformat\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0morder\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mdirection\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n", + "\u001b[1;32md:\\qlib\\qlib\\contrib\\backtest\\position.py\u001b[0m in \u001b[0;36msell_stock\u001b[1;34m(self, stock_id, trade_val, cost, trade_price)\u001b[0m\n\u001b[0;32m 64\u001b[0m \u001b[1;32mif\u001b[0m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mposition\u001b[0m\u001b[1;33m[\u001b[0m\u001b[0mstock_id\u001b[0m\u001b[1;33m]\u001b[0m\u001b[1;33m[\u001b[0m\u001b[1;34m\"amount\"\u001b[0m\u001b[1;33m]\u001b[0m \u001b[1;33m<\u001b[0m \u001b[1;33m-\u001b[0m\u001b[1;36m1e-5\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 65\u001b[0m raise ValueError(\n\u001b[1;32m---> 66\u001b[1;33m \u001b[1;34m\"only have {} {}, require {}\"\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mformat\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mposition\u001b[0m\u001b[1;33m[\u001b[0m\u001b[0mstock_id\u001b[0m\u001b[1;33m]\u001b[0m\u001b[1;33m[\u001b[0m\u001b[1;34m\"amount\"\u001b[0m\u001b[1;33m]\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mstock_id\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mtrade_amount\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m 67\u001b[0m )\n\u001b[0;32m 68\u001b[0m \u001b[1;32melif\u001b[0m \u001b[0mabs\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mposition\u001b[0m\u001b[1;33m[\u001b[0m\u001b[0mstock_id\u001b[0m\u001b[1;33m]\u001b[0m\u001b[1;33m[\u001b[0m\u001b[1;34m\"amount\"\u001b[0m\u001b[1;33m]\u001b[0m\u001b[1;33m)\u001b[0m \u001b[1;33m<=\u001b[0m \u001b[1;36m1e-5\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n", + "\u001b[1;31mValueError\u001b[0m: only have -0.104491644538939 SZ002475, require 1448416.2584415162" + ] + } + ], + "source": [ + "###################################\n", + "# prediction, backtest & analysis\n", + "###################################\n", + "port_analysis_config = {\n", + " \"strategy\": strategy,\n", + " \"backtest\": {\n", + " \"verbose\": False,\n", + " \"limit_threshold\": 0.095,\n", + " \"account\": 100000000,\n", + " \"benchmark\": benchmark,\n", + " \"deal_price\": \"close\",\n", + " \"open_cost\": 0.0005,\n", + " \"close_cost\": 0.0015,\n", + " \"min_cost\": 5,\n", + " },\n", + "}\n", + "\n", + "\n", + "# backtest and analysis\n", + "with R.start(experiment_name=\"backtest_analysis\"):\n", + " recorder = R.get_recorder(rid, experiment_name=\"train_model\")\n", + " model = recorder.load_object(\"trained_model\")\n", + "\n", + " # prediction\n", + " recorder = R.get_recorder()\n", + " ba_rid = recorder.id\n", + " sr = SignalRecord(model, dataset, recorder)\n", + " sr.generate()\n", + "\n", + " # backtest & analysis\n", + " par = PortAnaRecord(recorder, port_analysis_config)\n", + " par.generate()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ] +} \ No newline at end of file From 13c2c41d238526c957ac86b56aade79efb36399b Mon Sep 17 00:00:00 2001 From: Haoyu Wang <48108414+javaThonc@users.noreply.github.com> Date: Fri, 27 Nov 2020 09:59:30 +0800 Subject: [PATCH 192/241] Update pytorch_sfm.py --- qlib/contrib/model/pytorch_sfm.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qlib/contrib/model/pytorch_sfm.py b/qlib/contrib/model/pytorch_sfm.py index e013238fa..4fbe12fb7 100644 --- a/qlib/contrib/model/pytorch_sfm.py +++ b/qlib/contrib/model/pytorch_sfm.py @@ -102,7 +102,7 @@ class SFM_Model(nn.Module): i = self.inner_activation( x_i + torch.matmul(h_tm1 * B_U[0], self.U_i) - ) # not sure whether I am doing in the right unsquuze + ) ste = self.inner_activation(x_ste + torch.matmul(h_tm1 * B_U[0], self.U_ste)) fre = self.inner_activation(x_fre + torch.matmul(h_tm1 * B_U[0], self.U_fre)) From 6fb19eb58d4f5dda1cdac47c3ace0a92bfc47de5 Mon Sep 17 00:00:00 2001 From: lwwang1995 Date: Fri, 27 Nov 2020 10:09:17 +0800 Subject: [PATCH 193/241] Update training setting. --- .../ALSTM/workflow_config_alstm.yaml | 18 ++++++++++++++++-- .../benchmarks/GRU/workflow_config_gru.yaml | 18 ++++++++++++++++-- .../benchmarks/HATS/worflow_config_hats.yaml | 18 ++++++++++++++++-- .../benchmarks/LSTM/workflow_config_lstm.yaml | 18 ++++++++++++++++-- .../benchmarks/SFM/workflow_config_sfm.yaml | 16 +++++++++++++++- qlib/contrib/model/pytorch_alstm.py | 9 ++------- qlib/contrib/model/pytorch_gats.py | 13 ++++--------- qlib/contrib/model/pytorch_gru.py | 1 + qlib/contrib/model/pytorch_hats.py | 11 +++-------- qlib/contrib/model/pytorch_lstm.py | 7 +------ qlib/contrib/model/pytorch_sfm.py | 12 +----------- 11 files changed, 91 insertions(+), 50 deletions(-) diff --git a/examples/benchmarks/ALSTM/workflow_config_alstm.yaml b/examples/benchmarks/ALSTM/workflow_config_alstm.yaml index bb35b6da5..dd57761f3 100644 --- a/examples/benchmarks/ALSTM/workflow_config_alstm.yaml +++ b/examples/benchmarks/ALSTM/workflow_config_alstm.yaml @@ -8,6 +8,20 @@ data_handler_config: &data_handler_config fit_start_time: 2008-01-01 fit_end_time: 2014-12-31 instruments: *market + infer_processors: + - class: RobustZScoreNorm + kwargs: + fields_group: feature + clip_outlier: true + - class: Fillna + kwargs: + fields_group: feature + learn_processors: + - class: DropnaLabel + - class: CSRankNorm + kwargs: + fields_group: label + label: ["Ref($close, -2) / Ref($close, -1) - 1"] port_analysis_config: &port_analysis_config strategy: class: TopkDropoutStrategy @@ -37,7 +51,7 @@ task: lr: 1e-3 early_stop: 20 batch_size: 800 - metric: IC + metric: loss loss: mse seed: 0 GPU: 0 @@ -47,7 +61,7 @@ task: module_path: qlib.data.dataset kwargs: handler: - class: ALPHA360_Denoise + class: ALPHA360 module_path: qlib.contrib.data.handler kwargs: *data_handler_config segments: diff --git a/examples/benchmarks/GRU/workflow_config_gru.yaml b/examples/benchmarks/GRU/workflow_config_gru.yaml index e9e6224e6..bdfcd4e55 100644 --- a/examples/benchmarks/GRU/workflow_config_gru.yaml +++ b/examples/benchmarks/GRU/workflow_config_gru.yaml @@ -8,6 +8,20 @@ data_handler_config: &data_handler_config fit_start_time: 2008-01-01 fit_end_time: 2014-12-31 instruments: *market + infer_processors: + - class: RobustZScoreNorm + kwargs: + fields_group: feature + clip_outlier: true + - class: Fillna + kwargs: + fields_group: feature + learn_processors: + - class: DropnaLabel + - class: CSRankNorm + kwargs: + fields_group: label + label: ["Ref($close, -2) / Ref($close, -1) - 1"] port_analysis_config: &port_analysis_config strategy: class: TopkDropoutStrategy @@ -37,7 +51,7 @@ task: lr: 1e-3 early_stop: 20 batch_size: 800 - metric: IC + metric: loss loss: mse seed: 0 GPU: 0 @@ -46,7 +60,7 @@ task: module_path: qlib.data.dataset kwargs: handler: - class: ALPHA360_Denoise + class: ALPHA360 module_path: qlib.contrib.data.handler kwargs: *data_handler_config segments: diff --git a/examples/benchmarks/HATS/worflow_config_hats.yaml b/examples/benchmarks/HATS/worflow_config_hats.yaml index 0abed6c62..b08df14e0 100644 --- a/examples/benchmarks/HATS/worflow_config_hats.yaml +++ b/examples/benchmarks/HATS/worflow_config_hats.yaml @@ -8,6 +8,20 @@ data_handler_config: &data_handler_config fit_start_time: 2008-01-01 fit_end_time: 2014-12-31 instruments: *market + infer_processors: + - class: RobustZScoreNorm + kwargs: + fields_group: feature + clip_outlier: true + - class: Fillna + kwargs: + fields_group: feature + learn_processors: + - class: DropnaLabel + - class: CSRankNorm + kwargs: + fields_group: label + label: ["Ref($close, -2) / Ref($close, -1) - 1"] port_analysis_config: &port_analysis_config strategy: class: TopkDropoutStrategy @@ -36,7 +50,7 @@ task: n_epochs: 200 lr: 1e-3 early_stop: 20 - metric: IC + metric: loss loss: mse base_model: GRU seed: 0 @@ -46,7 +60,7 @@ task: module_path: qlib.data.dataset kwargs: handler: - class: ALPHA360_Denoise + class: ALPHA360 module_path: qlib.contrib.data.handler kwargs: *data_handler_config segments: diff --git a/examples/benchmarks/LSTM/workflow_config_lstm.yaml b/examples/benchmarks/LSTM/workflow_config_lstm.yaml index 354149dae..6512a0df3 100644 --- a/examples/benchmarks/LSTM/workflow_config_lstm.yaml +++ b/examples/benchmarks/LSTM/workflow_config_lstm.yaml @@ -8,6 +8,20 @@ data_handler_config: &data_handler_config fit_start_time: 2008-01-01 fit_end_time: 2014-12-31 instruments: *market + infer_processors: + - class: RobustZScoreNorm + kwargs: + fields_group: feature + clip_outlier: true + - class: Fillna + kwargs: + fields_group: feature + learn_processors: + - class: DropnaLabel + - class: CSRankNorm + kwargs: + fields_group: label + label: ["Ref($close, -2) / Ref($close, -1) - 1"] port_analysis_config: &port_analysis_config strategy: class: TopkDropoutStrategy @@ -37,7 +51,7 @@ task: lr: 1e-3 early_stop: 20 batch_size: 800 - metric: IC + metric: loss loss: mse seed: 0 GPU: 0 @@ -46,7 +60,7 @@ task: module_path: qlib.data.dataset kwargs: handler: - class: ALPHA360_Denoise + class: ALPHA360 module_path: qlib.contrib.data.handler kwargs: *data_handler_config segments: diff --git a/examples/benchmarks/SFM/workflow_config_sfm.yaml b/examples/benchmarks/SFM/workflow_config_sfm.yaml index 04f796150..59596760f 100644 --- a/examples/benchmarks/SFM/workflow_config_sfm.yaml +++ b/examples/benchmarks/SFM/workflow_config_sfm.yaml @@ -8,6 +8,20 @@ data_handler_config: &data_handler_config fit_start_time: 2008-01-01 fit_end_time: 2014-12-31 instruments: *market + infer_processors: + - class: RobustZScoreNorm + kwargs: + fields_group: feature + clip_outlier: true + - class: Fillna + kwargs: + fields_group: feature + learn_processors: + - class: DropnaLabel + - class: CSRankNorm + kwargs: + fields_group: label + label: ["Ref($close, -2) / Ref($close, -1) - 1"] port_analysis_config: &port_analysis_config strategy: class: TopkDropoutStrategy @@ -51,7 +65,7 @@ task: module_path: qlib.data.dataset kwargs: handler: - class: ALPHA360_Denoise + class: ALPHA360 module_path: qlib.contrib.data.handler kwargs: *data_handler_config segments: diff --git a/qlib/contrib/model/pytorch_alstm.py b/qlib/contrib/model/pytorch_alstm.py index 065963e73..7b4943db2 100644 --- a/qlib/contrib/model/pytorch_alstm.py +++ b/qlib/contrib/model/pytorch_alstm.py @@ -44,7 +44,7 @@ class ALSTM(Model): dropout=0.0, n_epochs=200, lr=0.001, - metric="IC", + metric="", batch_size=2000, early_stop=20, loss="mse", @@ -142,21 +142,16 @@ class ALSTM(Model): def metric_fn(self, pred, label): mask = torch.isfinite(label) - if self.metric == "IC": - return self.cal_ic(pred[mask], label[mask]) if self.metric == "" or self.metric == "loss": # use loss return -self.loss_fn(pred[mask], label[mask]) raise ValueError("unknown metric `%s`" % self.metric) - def cal_ic(self, pred, label): - return torch.mean(pred * label) - def train_epoch(self, x_train, y_train): x_train_values = x_train.values - y_train_values = np.squeeze(y_train.values) * 100 + y_train_values = np.squeeze(y_train.values) self.alstm_model.train() diff --git a/qlib/contrib/model/pytorch_gats.py b/qlib/contrib/model/pytorch_gats.py index d951f1873..77a02a9b2 100755 --- a/qlib/contrib/model/pytorch_gats.py +++ b/qlib/contrib/model/pytorch_gats.py @@ -43,13 +43,13 @@ class GAT(Model): d_feat=6, hidden_size=64, num_layers=2, - dropout=0.7, + dropout=0.0, n_epochs=200, - lr=0.0001, - metric="loss", + lr=0.001, + metric="", early_stop=20, loss="mse", - base_model="LSTM", + base_model="GRU", with_pretrain=True, optimizer="adam", GPU="0", @@ -148,17 +148,12 @@ class GAT(Model): def metric_fn(self, pred, label): mask = torch.isfinite(label) - if self.metric == "IC": - return self.cal_ic(pred[mask], label[mask]) if self.metric == "" or self.metric == "loss": # use loss return -self.loss_fn(pred[mask], label[mask]) raise ValueError("unknown metric `%s`" % self.metric) - def cal_ic(self, pred, label): - return torch.mean(pred * label) - def get_daily_inter(self, df, shuffle=False): # organize the train data into daily inter as daily batches daily_count = df.groupby(level=0).size().values diff --git a/qlib/contrib/model/pytorch_gru.py b/qlib/contrib/model/pytorch_gru.py index 02664b6ac..282ce72dd 100755 --- a/qlib/contrib/model/pytorch_gru.py +++ b/qlib/contrib/model/pytorch_gru.py @@ -146,6 +146,7 @@ class GRU(Model): raise ValueError("unknown metric `%s`" % self.metric) + def train_epoch(self, x_train, y_train): x_train_values = x_train.values diff --git a/qlib/contrib/model/pytorch_hats.py b/qlib/contrib/model/pytorch_hats.py index a0da88dbf..7affea73c 100644 --- a/qlib/contrib/model/pytorch_hats.py +++ b/qlib/contrib/model/pytorch_hats.py @@ -52,11 +52,11 @@ class HATS(Model): num_layers=2, dropout=0.5, n_epochs=200, - lr=0.0001, - metric="loss", + lr=0.01, + metric="", early_stop=20, loss="mse", - base_model="LSTM", + base_model="GRU", with_pretrain=True, optimizer="adam", GPU="0", @@ -154,17 +154,12 @@ class HATS(Model): def metric_fn(self, pred, label): mask = torch.isfinite(label) - if self.metric == "IC": - return self.cal_ic(pred[mask], label[mask]) if self.metric == "" or self.metric == "loss": # use loss return -self.loss_fn(pred[mask], label[mask]) raise ValueError("unknown metric `%s`" % self.metric) - def cal_ic(self, pred, label): - return torch.mean(pred * label) - def get_daily_inter(self, df, shuffle=False): # organize the train data into daily inter as daily batches daily_count = df.groupby(level=0).size().values diff --git a/qlib/contrib/model/pytorch_lstm.py b/qlib/contrib/model/pytorch_lstm.py index 22cc21e7f..844a08a83 100755 --- a/qlib/contrib/model/pytorch_lstm.py +++ b/qlib/contrib/model/pytorch_lstm.py @@ -46,7 +46,7 @@ class LSTM(Model): dropout=0.0, n_epochs=200, lr=0.001, - metric="loss", + metric="", batch_size=2000, early_stop=20, loss="mse", @@ -140,16 +140,12 @@ class LSTM(Model): def metric_fn(self, pred, label): mask = torch.isfinite(label) - if self.metric == "IC": - return self.cal_ic(pred[mask], label[mask]) if self.metric == "" or self.metric == "loss": # use loss return -self.loss_fn(pred[mask], label[mask]) raise ValueError("unknown metric `%s`" % self.metric) - def cal_ic(self, pred, label): - return torch.mean(pred * label) def train_epoch(self, x_train, y_train): @@ -193,7 +189,6 @@ class LSTM(Model): losses = [] indices = np.arange(len(x_values)) - np.random.shuffle(indices) for i in range(len(indices))[:: self.batch_size]: diff --git a/qlib/contrib/model/pytorch_sfm.py b/qlib/contrib/model/pytorch_sfm.py index 4fbe12fb7..9e897a246 100644 --- a/qlib/contrib/model/pytorch_sfm.py +++ b/qlib/contrib/model/pytorch_sfm.py @@ -102,7 +102,7 @@ class SFM_Model(nn.Module): i = self.inner_activation( x_i + torch.matmul(h_tm1 * B_U[0], self.U_i) - ) + ) # not sure whether I am doing in the right unsquuze ste = self.inner_activation(x_ste + torch.matmul(h_tm1 * B_U[0], self.U_ste)) fre = self.inner_activation(x_fre + torch.matmul(h_tm1 * B_U[0], self.U_fre)) @@ -283,10 +283,6 @@ class SFM(Model): ) ) - if loss not in {"mse", "binary"}: - raise NotImplementedError("loss {} is not supported!".format(loss)) - self._scorer = mean_squared_error if loss == "mse" else roc_auc_score - self.sfm_model = SFM_Model( d_feat=self.d_feat, output_dim=self.output_dim, @@ -318,7 +314,6 @@ class SFM(Model): losses = [] indices = np.arange(len(x_values)) - np.random.shuffle(indices) for i in range(len(indices))[:: self.batch_size]: @@ -428,17 +423,12 @@ class SFM(Model): def metric_fn(self, pred, label): mask = torch.isfinite(label) - if self.metric == "IC": - return self.cal_ic(pred[mask], label[mask]) if self.metric == "" or self.metric == "loss": # use loss return -self.loss_fn(pred[mask], label[mask]) raise ValueError("unknown metric `%s`" % self.metric) - def cal_ic(self, pred, label): - return torch.mean(pred * label) - def predict(self, dataset): if not self._fitted: raise ValueError("model is not fitted yet!") From 11564f6457cd5bb975dee9b8caf2df31de62d325 Mon Sep 17 00:00:00 2001 From: Haoyu Wang <48108414+javaThonc@users.noreply.github.com> Date: Fri, 27 Nov 2020 10:11:59 +0800 Subject: [PATCH 194/241] Update pytorch_sfm.py --- qlib/contrib/model/pytorch_sfm.py | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/qlib/contrib/model/pytorch_sfm.py b/qlib/contrib/model/pytorch_sfm.py index 9e897a246..f8a96bc84 100644 --- a/qlib/contrib/model/pytorch_sfm.py +++ b/qlib/contrib/model/pytorch_sfm.py @@ -102,8 +102,7 @@ class SFM_Model(nn.Module): i = self.inner_activation( x_i + torch.matmul(h_tm1 * B_U[0], self.U_i) - ) # not sure whether I am doing in the right unsquuze - + ) ste = self.inner_activation(x_ste + torch.matmul(h_tm1 * B_U[0], self.U_ste)) fre = self.inner_activation(x_fre + torch.matmul(h_tm1 * B_U[0], self.U_fre)) @@ -183,10 +182,6 @@ class SFM(Model): output dimension lr : float learning rate - lr_decay : float - learning rate decay - lr_decay_steps : int - learning rate decay steps optimizer : str optimizer name GPU : str @@ -208,8 +203,6 @@ class SFM(Model): early_stop=20, eval_steps=5, loss="mse", - lr_decay=0.96, - lr_decay_steps=100, optimizer="gd", GPU="0", seed=0, @@ -232,8 +225,6 @@ class SFM(Model): self.batch_size = batch_size self.early_stop = early_stop self.eval_steps = eval_steps - self.lr_decay = lr_decay - self.lr_decay_steps = lr_decay_steps self.optimizer = optimizer.lower() self.loss = loss self.device = "cuda:%d" % (GPU) if torch.cuda.is_available() else "cpu" @@ -254,8 +245,6 @@ class SFM(Model): "\nbatch_size : {}" "\nearly_stop : {}" "\neval_steps : {}" - "\nlr_decay : {}" - "\nlr_decay_steps : {}" "\noptimizer : {}" "\nloss_type : {}" "\nvisible_GPU : {}" @@ -273,8 +262,6 @@ class SFM(Model): batch_size, early_stop, eval_steps, - lr_decay, - lr_decay_steps, optimizer.lower(), loss, GPU, From e27290c8a00739a1d9aafee26b5b47f79a13ece1 Mon Sep 17 00:00:00 2001 From: Haoyu Wang <48108414+javaThonc@users.noreply.github.com> Date: Fri, 27 Nov 2020 10:17:11 +0800 Subject: [PATCH 195/241] Update workflow_config_sfm.yaml --- examples/benchmarks/SFM/workflow_config_sfm.yaml | 2 -- 1 file changed, 2 deletions(-) diff --git a/examples/benchmarks/SFM/workflow_config_sfm.yaml b/examples/benchmarks/SFM/workflow_config_sfm.yaml index 59596760f..3fa3f932c 100644 --- a/examples/benchmarks/SFM/workflow_config_sfm.yaml +++ b/examples/benchmarks/SFM/workflow_config_sfm.yaml @@ -55,8 +55,6 @@ task: early_stop: 20 eval_steps: 5 loss: mse - lr_decay: 0.96 - lr_decay_steps: 100 optimizer: adam GPU: 1 seed: 710 From a4f76b3922c8b6a25450547fdbe10fe3fa6c95cb Mon Sep 17 00:00:00 2001 From: Dong Zhou Date: Fri, 27 Nov 2020 10:35:22 +0800 Subject: [PATCH 196/241] update portfolio strategy --- examples/portfolio_optimization_example.ipynb | 236 +++++++----------- 1 file changed, 95 insertions(+), 141 deletions(-) diff --git a/examples/portfolio_optimization_example.ipynb b/examples/portfolio_optimization_example.ipynb index d507f369c..4d6c2b3d2 100644 --- a/examples/portfolio_optimization_example.ipynb +++ b/examples/portfolio_optimization_example.ipynb @@ -57,10 +57,10 @@ "output_type": "stream", "name": "stderr", "text": [ - "[20768:MainThread](2020-11-27 08:15:01,096) INFO - qlib.Initialization - [__init__.py:41] - default_conf: client.\n", - "[20768:MainThread](2020-11-27 08:15:03,120) WARNING - qlib.Initialization - [__init__.py:57] - redis connection failed(host=127.0.0.1 port=6379), cache will not be used!\n", - "[20768:MainThread](2020-11-27 08:15:03,121) INFO - qlib.Initialization - [__init__.py:76] - qlib successfully initialized based on client settings.\n", - "[20768:MainThread](2020-11-27 08:15:03,122) INFO - qlib.Initialization - [__init__.py:79] - data_path=C:\\Users\\v-donzh\\.qlib\\qlib_data\\cn_data\n" + "[35366:MainThread](2020-11-27 10:31:09,528) INFO - qlib.Initialization - [__init__.py:41] - default_conf: client.\n", + "[35366:MainThread](2020-11-27 10:31:09,531) WARNING - qlib.Initialization - [__init__.py:57] - redis connection failed(host=127.0.0.1 port=6379), cache will not be used!\n", + "[35366:MainThread](2020-11-27 10:31:09,531) INFO - qlib.Initialization - [__init__.py:76] - qlib successfully initialized based on client settings.\n", + "[35366:MainThread](2020-11-27 10:31:09,532) INFO - qlib.Initialization - [__init__.py:79] - data_path=/home/dongzho/.qlib/qlib_data/cn_data\n" ] } ], @@ -102,25 +102,25 @@ "output_type": "stream", "name": "stderr", "text": [ - "[20768:MainThread](2020-11-27 08:15:55,319) INFO - qlib.timer - [log.py:81] - Time cost: 52.158s | Loading data Done\n", - "[20768:MainThread](2020-11-27 08:15:56,107) INFO - qlib.timer - [log.py:81] - Time cost: 0.669s | DropnaLabel Done\n", - "[20768:MainThread](2020-11-27 08:15:59,716) INFO - qlib.timer - [log.py:81] - Time cost: 3.608s | CSZScoreNorm Done\n", - "[20768:MainThread](2020-11-27 08:15:59,717) INFO - qlib.timer - [log.py:81] - Time cost: 4.397s | fit & process data Done\n", - "[20768:MainThread](2020-11-27 08:15:59,717) INFO - qlib.timer - [log.py:81] - Time cost: 56.556s | Init data Done\n", - "[20768:MainThread](2020-11-27 08:15:59,722) INFO - qlib.workflow - [exp.py:180] - Experiment 1 starts running ...\n", - "[20768:MainThread](2020-11-27 08:16:00,133) INFO - qlib.workflow - [recorder.py:234] - Recorder 46e50379b45a4a7684c683cd423535a9 starts running under Experiment 1 ...\n", - "[20768:MainThread](2020-11-27 08:16:00,134) INFO - qlib.workflow - [expm.py:251] - No tracking URI is provided. The default tracking URI is set as `mlruns` under the working directory.\n", + "[35366:MainThread](2020-11-27 10:31:29,731) INFO - qlib.timer - [log.py:81] - Time cost: 20.103s | Loading data Done\n", + "[35366:MainThread](2020-11-27 10:31:30,557) INFO - qlib.timer - [log.py:81] - Time cost: 0.241s | DropnaLabel Done\n", + "[35366:MainThread](2020-11-27 10:31:38,518) INFO - qlib.timer - [log.py:81] - Time cost: 7.960s | CSZScoreNorm Done\n", + "[35366:MainThread](2020-11-27 10:31:38,519) INFO - qlib.timer - [log.py:81] - Time cost: 8.786s | fit & process data Done\n", + "[35366:MainThread](2020-11-27 10:31:38,520) INFO - qlib.timer - [log.py:81] - Time cost: 28.891s | Init data Done\n", + "[35366:MainThread](2020-11-27 10:31:38,527) INFO - qlib.workflow - [exp.py:180] - Experiment 2 starts running ...\n", + "[35366:MainThread](2020-11-27 10:31:38,651) INFO - qlib.workflow - [recorder.py:234] - Recorder c81375e3b5474feb9c77711babd158c3 starts running under Experiment 2 ...\n", + "[35366:MainThread](2020-11-27 10:31:38,652) INFO - qlib.workflow - [expm.py:251] - No tracking URI is provided. The default tracking URI is set as `mlruns` under the working directory.\n", "Training until validation scores don't improve for 50 rounds\n", - "[20]\ttrain's l2: 0.990556\tvalid's l2: 0.994299\n", - "[40]\ttrain's l2: 0.986919\tvalid's l2: 0.993683\n", - "[60]\ttrain's l2: 0.984485\tvalid's l2: 0.993495\n", - "[80]\ttrain's l2: 0.982363\tvalid's l2: 0.993365\n", - "[100]\ttrain's l2: 0.980538\tvalid's l2: 0.993251\n", - "[120]\ttrain's l2: 0.978755\tvalid's l2: 0.993265\n", - "[140]\ttrain's l2: 0.977079\tvalid's l2: 0.993324\n", - "[160]\ttrain's l2: 0.97535\tvalid's l2: 0.99336\n", + "[20]\ttrain's l2: 0.990559\tvalid's l2: 0.994332\n", + "[40]\ttrain's l2: 0.98687\tvalid's l2: 0.993702\n", + "[60]\ttrain's l2: 0.984308\tvalid's l2: 0.993503\n", + "[80]\ttrain's l2: 0.982202\tvalid's l2: 0.993446\n", + "[100]\ttrain's l2: 0.980318\tvalid's l2: 0.993423\n", + "[120]\ttrain's l2: 0.97854\tvalid's l2: 0.993409\n", + "[140]\ttrain's l2: 0.97679\tvalid's l2: 0.993413\n", + "[160]\ttrain's l2: 0.975116\tvalid's l2: 0.993473\n", "Early stopping, best iteration is:\n", - "[118]\ttrain's l2: 0.978921\tvalid's l2: 0.993248\n" + "[127]\ttrain's l2: 0.977957\tvalid's l2: 0.993381\n" ] } ], @@ -191,7 +191,7 @@ }, { "cell_type": "code", - "execution_count": 17, + "execution_count": 5, "metadata": {}, "outputs": [], "source": [ @@ -248,7 +248,7 @@ "\n", " # calc remaining value\n", " current_value = pd.Series({\n", - " stock_id: trade_exchange.get_close(stock_id, pred_date) * amount\n", + " stock_id: current.get_stock_price(stock_id) * amount\n", " for stock_id, amount in current_position.items()\n", " })\n", " risk_total_value = self.get_risk_degree(trade_date) * current.calculate_value()\n", @@ -256,7 +256,9 @@ "\n", " # portfolio init weight\n", " init_weight = current_value.reindex(score_series.index, fill_value=0)\n", - " init_weight /= init_weight.sum() + 1e-12\n", + " init_weight_sum = init_weight.sum()\n", + " if init_weight_sum > 0:\n", + " init_weight /= init_weight_sum\n", "\n", " # covariance estimation\n", " selector = (self.data_handler.get_range_selector(pred_date, 252), score_series.index)\n", @@ -269,14 +271,22 @@ " )\n", "\n", " # optimize target portfolio\n", - " target_weight = self.optimizer(cov, score_series, init_weight)\n", + " if init_weight.sum() > 0:\n", + " target_weight = self.optimizer(cov, score_series, init_weight)\n", + " else:\n", + " target_weight = self.optimizer(cov, score_series)\n", + " target_weight = target_weight[target_weight > 1e-6]\n", " for stock_id, weight in target_weight.items():\n", " try:\n", - " target_position[stock_id] = traded_value * weight / trade_exchange.get_close(stock_id, pred_date)\n", + " target_position[stock_id] = int(traded_value * weight / trade_exchange.get_close(stock_id, pred_date))\n", " except Exception as e:\n", - " print(e)\n", - " target_position[stock_id] = 0\n", - " print(target_weight[target_weight>1e-4])\n", + " # TODO: unknown exception\n", + " print('Exception:', e)\n", + "\n", + " # for debug\n", + " print('trade date:', trade_date)\n", + " print('target weight:', target_weight.to_dict())\n", + " print('target position:', target_position)\n", "\n", " # generate order list\n", " order_list = trade_exchange.generate_order_for_target_amount_position(\n", @@ -285,12 +295,12 @@ " trade_date=trade_date,\n", " )\n", "\n", - " return order_list\n" + " return order_list" ] }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 6, "metadata": {}, "outputs": [], "source": [ @@ -302,15 +312,15 @@ }, { "cell_type": "code", - "execution_count": 15, + "execution_count": 7, "metadata": {}, "outputs": [ { "output_type": "stream", "name": "stderr", "text": [ - "[20768:MainThread](2020-11-27 08:45:28,512) INFO - qlib.timer - [log.py:81] - Time cost: 14.502s | Loading data Done\n", - "[20768:MainThread](2020-11-27 08:45:28,513) INFO - qlib.timer - [log.py:81] - Time cost: 14.503s | Init data Done\n" + "[35366:MainThread](2020-11-27 10:31:56,951) INFO - qlib.timer - [log.py:81] - Time cost: 6.763s | Loading data Done\n", + "[35366:MainThread](2020-11-27 10:31:56,953) INFO - qlib.timer - [log.py:81] - Time cost: 6.766s | Init data Done\n" ] } ], @@ -318,131 +328,75 @@ "data_loader = QlibDataLoader([\"$close\"])\n", "data_handler = DataHandler(\"all\", \"2015-01-01\", \"2020-08-01\", data_loader)\n", "cov_estimator = ShrinkCovEstimator(nan_option=\"mask\")\n", - "optimizer = PortfolioOptimizer(\"mvo\", lamb=1.0, delta=0.2)\n", + "optimizer = PortfolioOptimizer(\"mvo\", lamb=2, delta=0.2, tol=1e-5)\n", "strategy = OptBasedStrategy(data_handler, cov_estimator, optimizer)" ] }, { "cell_type": "code", - "execution_count": 16, - "metadata": {}, + "execution_count": 49, + "metadata": { + "tags": [] + }, "outputs": [ { "output_type": "stream", "name": "stderr", "text": [ - "[20768:MainThread](2020-11-27 08:45:28,543) INFO - qlib.workflow - [exp.py:180] - Experiment 2 starts running ...\n", - "[20768:MainThread](2020-11-27 08:45:28,581) INFO - qlib.workflow - [recorder.py:234] - Recorder d9bd45391cf5431bb339531baf5fb6f2 starts running under Experiment 2 ...\n", - "[20768:MainThread](2020-11-27 08:45:28,582) INFO - qlib.workflow - [expm.py:251] - No tracking URI is provided. The default tracking URI is set as `mlruns` under the working directory.\n", - "[20768:MainThread](2020-11-27 08:45:29,433) INFO - qlib.workflow - [record_temp.py:127] - Signal record 'pred.pkl' has been saved as the artifact of the Experiment 2\n", - "[20768:MainThread](2020-11-27 08:45:29,525) INFO - qlib.Evaluate - [evaluate.py:161] - Create new exchange\n", - "'The following are prediction results of the LGBModel model.'\n", - " score\n", - "datetime instrument \n", - "2017-01-03 SH600000 -0.033506\n", - " SH600008 0.002120\n", - " SH600009 0.032941\n", - " SH600010 -0.012371\n", - " SH600015 -0.140312\n", - "C:\\Users\\v-donzh\\AppData\\Local\\Continuum\\miniconda3\\envs\\qlib\\lib\\site-packages\\ipykernel_launcher.py:55: DeprecationWarning: The default dtype for empty Series will be 'object' instead of 'float64' in a future version. Specify a dtype explicitly to silence this warning.\n", - "instrument\n", - "SH600000 2.868027e-12\n", - "SH600008 1.341080e-12\n", - "SH600009 5.131225e-12\n", - "SH600010 3.890177e-12\n", - "SH600015 1.781055e-11\n", - " ... \n", - "SZ300146 2.841964e-12\n", - "SZ300168 4.603490e-12\n", - "SZ300182 9.855511e-12\n", - "SZ300251 1.495177e-12\n", - "SZ300315 4.054219e-12\n", - "Length: 290, dtype: float64\n", - "instrument\n", - "SH600000 5.253178e-12\n", - "SH600008 1.901077e-14\n", - "SH600009 5.573006e-12\n", - "SH600010 6.129089e-14\n", - "SH600015 7.236246e-14\n", - " ... \n", - "SZ300146 4.604396e-12\n", - "SZ300168 4.705359e-12\n", - "SZ300182 6.358140e-14\n", - "SZ300251 5.347927e-12\n", - "SZ300315 1.288077e-13\n", - "Length: 289, dtype: float64\n", - "instrument\n", - "SH600000 1.181534e-14\n", - "SH600008 3.480454e-14\n", - "SH600009 1.902741e-13\n", - "SH600010 8.388080e-12\n", - "SH600015 1.490974e-13\n", - " ... \n", - "SZ300146 9.838926e-13\n", - "SZ300168 1.790169e-11\n", - "SZ300182 1.002664e-11\n", - "SZ300251 2.097283e-12\n", - "SZ300315 1.180997e-11\n", - "Length: 288, dtype: float64\n", - "instrument\n", - "SH600000 4.027211e-14\n", - "SH600008 1.067081e-14\n", - "SH600009 1.010989e-13\n", - "SH600010 1.605190e-12\n", - "SH600015 6.075465e-14\n", - " ... \n", - "SZ300146 7.338274e-12\n", - "SZ300168 1.990990e-11\n", - "SZ300182 4.891503e-12\n", - "SZ300251 1.067670e-11\n", - "SZ300315 1.009767e-11\n", - "Length: 288, dtype: float64\n", - "instrument\n", - "SH600000 2.518140e-12\n", - "SH600008 5.504712e-13\n", - "SH600009 3.185232e-12\n", - "SH600010 3.641971e-13\n", - "SH600015 1.902283e-14\n", - " ... \n", - "SZ300146 5.427494e-14\n", - "SZ300168 7.620411e-13\n", - "SZ300182 2.484372e-14\n", - "SZ300251 5.261806e-13\n", - "SZ300315 1.412130e-13\n", - "Length: 288, dtype: float64\n", - "('SH600666', Timestamp('2017-01-10 00:00:00'))\n", - "instrument\n", - "SH600000 7.466386e-12\n", - "SH600008 1.348026e-15\n", - "SH600009 1.111042e-11\n", - "SH600010 6.740600e-14\n", - "SH600015 1.049665e-11\n", - " ... \n", - "SZ300146 6.842294e-14\n", - "SZ300168 1.750970e-13\n", - "SZ300182 6.916267e-14\n", - "SZ300251 9.068748e-14\n", - "SZ300315 1.193522e-13\n", - "Length: 289, dtype: float64\n" + "1': 0.08936553334387595, 'SH601800': 0.011014844457113308, 'SH601939': 0.013378001170219945, 'SH603993': 0.013820193926861863, 'SZ000338': 0.002455991798001457, 'SZ000423': 0.004893338273543826, 'SZ000538': 0.010686211189620477, 'SZ002065': 0.09095125419435357, 'SZ002074': 0.010299013738522475, 'SZ002085': 0.19844965949420615, 'SZ002236': 0.09210003831704765, 'SZ002310': 0.05664352912360013, 'SZ300017': 0.0197442255539771}\n", + "target position: {'SZ002299': 6184584.0980107365, 'SH600000': 272224, 'SH600009': 604839, 'SH600018': 3097398, 'SH600028': 335726, 'SH600196': 23243, 'SH600276': 71634, 'SH600519': 17354, 'SH600585': 269686, 'SH600900': 2501521, 'SH601111': 2400659, 'SH601800': 334062, 'SH601939': 1283164, 'SH603993': 742901, 'SZ000338': 95285, 'SZ000423': 21697, 'SZ000538': 14518, 'SZ002065': 498253, 'SZ002074': 111674, 'SZ002085': 591507, 'SZ002236': 394197, 'SZ002310': 2202674, 'SZ300017': 206128}\n", + "target weight: {'SH600000': 0.02310668460556249, 'SH600009': 0.06170206213753432, 'SH600018': 0.027608180837257277, 'SH600028': 0.00971532319525714, 'SH600196': 0.0036133308423111116, 'SH600276': 0.093195014492093, 'SH600519': 0.013476706174774766, 'SH600585': 0.036024919027310476, 'SH600660': 0.04512159672692613, 'SH600900': 0.12506534473579556, 'SH601939': 0.013494851810297546, 'SH603993': 0.07619418669734077, 'SZ000338': 0.0024673392047414363, 'SZ000423': 0.00485981529404862, 'SZ000538': 0.010602880875660015, 'SZ002065': 0.09064325205359221, 'SZ002074': 0.0011889996597580427, 'SZ002085': 0.1982091371262038, 'SZ002236': 0.09254320484936242, 'SZ002310': 0.05152917909181458, 'SZ002466': 0.00014732765084648903, 'SZ300017': 0.019490662910321074}\n", + "target position: {'SZ002299': 6184584.0980107365, 'SH600000': 272079, 'SH600009': 604359, 'SH600018': 3095205, 'SH600028': 335471, 'SH600196': 23407, 'SH600276': 71567, 'SH600519': 17345, 'SH600585': 269447, 'SH600660': 129265, 'SH600900': 2499305, 'SH601939': 1282317, 'SH603993': 4058172, 'SZ000338': 95223, 'SZ000423': 21703, 'SZ000538': 14509, 'SZ002065': 497821, 'SZ002074': 12787, 'SZ002085': 590955, 'SZ002236': 393895, 'SZ002310': 2190685, 'SZ002466': 4483, 'SZ300017': 205994}\n", + "target weight: {'SH600000': 0.0014042138463464568, 'SH600009': 0.11511740651805806, 'SH600018': 0.026968513725965638, 'SH600028': 0.009566603496832042, 'SH600150': 0.016339328084607228, 'SH600276': 0.09374974543357856, 'SH600489': 0.021876512936684123, 'SH600585': 0.035840818294258524, 'SH600900': 0.12414161958870683, 'SH601888': 0.005682635273269834, 'SH601939': 0.013289788356428228, 'SH603993': 0.07491407610535435, 'SZ000338': 0.002426716760042838, 'SZ000423': 0.00492071038737461, 'SZ000503': 0.005617017904986693, 'SZ000538': 0.010859006699485451, 'SZ002065': 0.08924691553942904, 'SZ002085': 0.19757848255238786, 'SZ002236': 0.09381012783787722, 'SZ002310': 0.03737359938389514, 'SZ300017': 0.01927616131502695}\n", + "target position: {'SZ002299': 6184584.0980107365, 'SH600000': 16809, 'SH600009': 1075516, 'SH600018': 3091248, 'SH600028': 335128, 'SH600150': 114804, 'SH600276': 71473, 'SH600489': 66586, 'SH600585': 268644, 'SH600900': 2496175, 'SH601888': 173824, 'SH601939': 1281108, 'SH603993': 4052802, 'SZ000338': 95107, 'SZ000423': 21684, 'SZ000503': 80461, 'SZ000538': 14507, 'SZ002065': 497197, 'SZ002085': 590211, 'SZ002236': 393412, 'SZ002310': 1573728, 'SZ300017': 205818}\n", + "target weight: {'SH600000': 0.0013962189421662084, 'SH600009': 0.09330267135244051, 'SH600018': 0.026443154116291615, 'SH600028': 0.009581412428525829, 'SH600150': 0.016443917649559808, 'SH600276': 0.09378402212481758, 'SH600703': 0.0005233118350013756, 'SH600741': 0.10117549074044105, 'SH600900': 0.12435147566444608, 'SH601888': 0.00560250787284307, 'SH601939': 0.013238798853730008, 'SH603993': 0.07455231781733267, 'SZ000423': 0.0048695925705555185, 'SZ000503': 0.006070996956328167, 'SZ000538': 0.010870567565742796, 'SZ002065': 0.08722983720892508, 'SZ002074': 0.00037126948590009574, 'SZ002085': 0.19840484837030906, 'SZ002236': 0.09365186287123867, 'SZ002310': 0.03806080531862309, 'SZ300017': 7.492025186876957e-05}\n", + "target position: {'SZ002299': 6184584.0980107365, 'SH600000': 16889, 'SH600009': 867443, 'SH600018': 3086467, 'SH600028': 334573, 'SH600150': 114383, 'SH600276': 71360, 'SH600703': 1760, 'SH600741': 665366, 'SH600900': 2491839, 'SH601888': 173465, 'SH601939': 1278590, 'SH603993': 4045939, 'SZ000423': 21674, 'SZ000503': 80212, 'SZ000538': 14499, 'SZ002065': 496361, 'SZ002074': 4086, 'SZ002085': 589224, 'SZ002236': 392766, 'SZ002310': 1571463, 'SZ300017': 805}\n", + "target weight: {'SH600000': 0.0014143911110003147, 'SH600018': 0.026834186435965166, 'SH600028': 0.00961324990522086, 'SH600150': 0.015905361405158292, 'SH600276': 0.09486308638260738, 'SH600685': 1.0253334545374858e-06, 'SH600703': 0.0005108576602907958, 'SH600741': 0.10252334336233063, 'SH600900': 0.1250632059809011, 'SH601888': 0.005830869532670813, 'SH601939': 0.01336945356138906, 'SH603993': 0.07101851124599835, 'SZ000423': 0.004899981502195361, 'SZ000503': 0.006113894785564276, 'SZ000538': 0.011081925761176491, 'SZ000709': 1.06442568357325e-06, 'SZ002065': 0.08812103684766726, 'SZ002074': 0.0003564773234700175, 'SZ002085': 0.19097427428977284, 'SZ002236': 0.09299395368630246, 'SZ002310': 0.03841630892378685, 'SZ002475': 0.10001934454071283, 'SZ300017': 7.322667303400442e-05}\n", + "target position: {'SZ002299': 6184584.0980107365, 'SH600000': 16886, 'SH600018': 3080789, 'SH600028': 334087, 'SH600150': 114360, 'SH600276': 71234, 'SH600685': 10, 'SH600703': 1709, 'SH600741': 663932, 'SH600900': 2486951, 'SH601888': 173417, 'SH601939': 1276335, 'SH603993': 3740672, 'SZ000423': 21667, 'SZ000503': 80191, 'SZ000538': 14495, 'SZ000709': 11, 'SZ002065': 495371, 'SZ002074': 3867, 'SZ002085': 588051, 'SZ002236': 392002, 'SZ002310': 1568834, 'SZ002475': 1264636, 'SZ300017': 809}\n", + "target weight: {'SH600000': 0.0013872765178790307, 'SH600018': 0.026321999857337998, 'SH600028': 0.009491029058787367, 'SH600150': 0.015749871987744815, 'SH600276': 0.09581999547114961, 'SH600703': 0.000518490273176083, 'SH600741': 0.1037547619508012, 'SH600900': 0.12396253436063161, 'SH601258': 0.02298494942988327, 'SH601888': 0.005915886046387033, 'SH601939': 0.013177336599075601, 'SH603993': 0.06888468621566025, 'SZ000423': 0.005102036718661418, 'SZ000503': 0.00602692511970311, 'SZ000538': 0.011127923667697532, 'SZ000709': 0.07688609680386178, 'SZ002065': 0.08693397271897534, 'SZ002074': 0.000347445594871718, 'SZ002085': 0.1905176824564206, 'SZ002236': 0.035835596544641496, 'SZ002475': 0.09918059167278087, 'SZ300017': 7.291118905149903e-05}\n", + "target position: {'SZ002299': 6184584.0980107365, 'SH600000': 16948, 'SH600018': 3086676, 'SH600028': 334750, 'SH600150': 114560, 'SH600276': 71372, 'SH600703': 1715, 'SH600741': 665129, 'SH600900': 2491433, 'SH601258': 4190669, 'SH601888': 174070, 'SH601939': 1278836, 'SH603993': 3747283, 'SZ000423': 21744, 'SZ000503': 80490, 'SZ000538': 14538, 'SZ000709': 871429, 'SZ002065': 496245, 'SZ002074': 3887, 'SZ002085': 589120, 'SZ002236': 145147, 'SZ002475': 1268582, 'SZ300017': 814}\n", + "target weight: {'SH600000': 0.001373124016867567, 'SH600018': 0.02646941123076474, 'SH600028': 0.009458335378810856, 'SH600150': 0.015442533996257352, 'SH600276': 0.09620341387657301, 'SH600649': 0.012613476480118908, 'SH600703': 0.0005280976985716832, 'SH600741': 0.06577156829314017, 'SH600900': 0.12455488881029539, 'SH601258': 0.02270943336842379, 'SH601939': 0.013066707696697587, 'SH603993': 0.0649427819283919, 'SZ000423': 0.0051167756388828005, 'SZ000503': 0.006076486564538039, 'SZ000709': 0.0770418453012855, 'SZ000778': 0.08738918304165759, 'SZ002065': 0.08804613990036694, 'SZ002074': 0.00034315924263262563, 'SZ002085': 0.18241434394629127, 'SZ002475': 0.10035998625624482, 'SZ300017': 7.809604376099223e-05}\n", + "target position: {'SZ002299': 6184584.0980107365, 'SH600000': 16935, 'SH600018': 3089469, 'SH600028': 334906, 'SH600150': 114496, 'SH600276': 71430, 'SH600649': 337388, 'SH600703': 1714, 'SH600741': 419916, 'SH600900': 2493978, 'SH601258': 4194599, 'SH601939': 1279661, 'SH603993': 3750968, 'SZ000423': 21734, 'SZ000503': 80440, 'SZ000709': 872293, 'SZ000778': 366855, 'SZ002065': 496756, 'SZ002074': 3880, 'SZ002085': 564610, 'SZ002475': 1269872, 'SZ300017': 812}\n", + "target weight: {'SH600000': 0.0013497287789570015, 'SH600018': 0.02647482761554837, 'SH600028': 0.00941080088689994, 'SH600150': 0.01556139303593115, 'SH600276': 0.09732218714743374, 'SH600649': 0.012606184789019243, 'SH600703': 0.0005334649726542859, 'SH600900': 0.12593267687041163, 'SH601258': 0.021199485570796834, 'SH601939': 0.013025993149697816, 'SH603993': 0.06446918682668012, 'SZ000423': 0.005311875734339093, 'SZ000503': 0.006125989728635501, 'SZ000709': 0.0707610058353687, 'SZ000778': 0.14004715956352495, 'SZ002065': 0.08746446321200681, 'SZ002074': 0.00033710686535540885, 'SZ002085': 0.15238971653801253, 'SZ002146': 0.042585776887618575, 'SZ002475': 0.10701429615740456, 'SZ300017': 7.667981013711115e-05}\n", + "target position: {'SZ002299': 6184584.0980107365, 'SH600000': 17031, 'SH600018': 3109084, 'SH600028': 336978, 'SH600150': 115126, 'SH600276': 71888, 'SH600649': 339316, 'SH600703': 1724, 'SH600900': 2510148, 'SH601258': 4237748, 'SH601939': 1287810, 'SH603993': 3775382, 'SZ000423': 21853, 'SZ000503': 80885, 'SZ000709': 878077, 'SZ000778': 625157, 'SZ002065': 499988, 'SZ002074': 3901, 'SZ002085': 469624, 'SZ002146': 2000993, 'SZ002475': 1278084, 'SZ300017': 814}\n", + "target weight: {'SH600000': 0.0013594926998639766, 'SH600009': 0.021101252574639438, 'SH600028': 0.009528554544265834, 'SH600150': 0.015013601602404225, 'SH600276': 0.09860402207319302, 'SH600649': 0.01292550325031454, 'SH600685': 0.00703471182662378, 'SH600703': 0.0005218767517596246, 'SH600900': 0.12786995199482584, 'SH601258': 0.04401496515184404, 'SH601398': 0.025932829520167643, 'SH601939': 0.0134408200189716, 'SH603993': 0.06319752369639879, 'SZ000423': 0.005221187626834546, 'SZ000503': 0.006085670359590286, 'SZ000568': 0.003081214755480397, 'SZ000709': 0.07061122716452324, 'SZ000778': 0.1379488795662632, 'SZ000839': 0.019142903464547063, 'SZ002065': 0.04714685528331623, 'SZ002074': 0.00033291622875151913, 'SZ002085': 0.11947661465752588, 'SZ002146': 0.043205942689553425, 'SZ002310': 0.0009243182551654129, 'SZ002475': 0.106199974013018, 'SZ300017': 7.709323254732814e-05}\n", + "target position: {'SZ002299': 6184584.0980107365, 'SH600000': 16933, 'SH600009': 196068, 'SH600028': 337025, 'SH600150': 115100, 'SH600276': 71926, 'SH600649': 339354, 'SH600685': 75328, 'SH600703': 1713, 'SH600900': 2511928, 'SH601258': 8791935, 'SH601398': 1146896, 'SH601939': 1288215, 'SH603993': 3777819, 'SZ000423': 21728, 'SZ000503': 80869, 'SZ000568': 10375, 'SZ000709': 878683, 'SZ000778': 625604, 'SZ000839': 312116, 'SZ002065': 268413, 'SZ002074': 3860, 'SZ002085': 369761, 'SZ002146': 2002072, 'SZ002310': 40341, 'SZ002475': 1278918, 'SZ300017': 811}\n", + "target weight: {'SH600000': 0.0013764694393366029, 'SH600009': 0.021541655860797534, 'SH600028': 0.009752609535237182, 'SH600276': 0.06514222178877259, 'SH600649': 0.01273168785031133, 'SH600685': 0.006989932070614982, 'SH600900': 0.12998548252109676, 'SH601258': 0.13157540821422453, 'SH601398': 0.02641881439805636, 'SH601939': 0.0136141957873422, 'SH603993': 0.0602411123337629, 'SZ000503': 0.006084251045333903, 'SZ000709': 0.06977363144499521, 'SZ000778': 0.1385461140272643, 'SZ000839': 0.018579865431307987, 'SZ002065': 0.046270476942690986, 'SZ002074': 0.00025974854597178115, 'SZ002085': 0.10060756172850334, 'SZ002146': 0.043204792194791966, 'SZ002310': 0.0009022784286642987, 'SZ002466': 0.011748866835406593, 'SZ002475': 0.08457581284822364, 'SZ300017': 7.701070501151889e-05}\n", + "target position: {'SZ002299': 6184584.0980107365, 'SH600000': 16938, 'SH600009': 196239, 'SH600028': 337355, 'SH600276': 46535, 'SH600649': 339479, 'SH600685': 75274, 'SH600900': 2514488, 'SH601258': 26730440, 'SH601398': 1148157, 'SH601939': 1289259, 'SH603993': 3781937, 'SZ000503': 80900, 'SZ000709': 879645, 'SZ000778': 626285, 'SZ000839': 312384, 'SZ002065': 268717, 'SZ002074': 3093, 'SZ002085': 309206, 'SZ002146': 2003901, 'SZ002310': 39782, 'SZ002466': 367691, 'SZ002475': 1026389, 'SZ300017': 812}\n", + "target weight: {'SH600000': 0.0013689894888766726, 'SH600009': 0.021087495457198752, 'SH600028': 0.009589419355091226, 'SH600276': 0.0644304399184473, 'SH600535': 0.016420787426513667, 'SH600649': 0.0267771761277641, 'SH600900': 0.12784455237901315, 'SH601169': 0.004374459372110214, 'SH601258': 0.13288651981531077, 'SH601398': 0.02615927477879055, 'SH601939': 0.013573361058977978, 'SH603993': 1.157895161672162e-06, 'SZ000503': 0.009069218941980683, 'SZ000709': 0.07014466816191627, 'SZ000778': 0.13956352821962528, 'SZ002065': 0.045206445945654664, 'SZ002085': 0.08649963592018277, 'SZ002146': 0.04234588186007612, 'SZ002310': 0.0008924777422846245, 'SZ002466': 0.07334842360184116, 'SZ002475': 0.08834296814868704, 'SZ300017': 7.311841306821287e-05}\n", + "Exception: ('SH601169', Timestamp('2017-04-25 00:00:00'))\n", + "target position: {'SZ002299': 6184584.0980107365, 'SH600000': 16929, 'SH600009': 196092, 'SH600028': 337333, 'SH600276': 46571, 'SH600535': 57649, 'SH600649': 731641, 'SH600900': 2515321, 'SH601258': 26740467, 'SH601398': 1148635, 'SH601939': 1289434, 'SH603993': 72, 'SZ000503': 122157, 'SZ000709': 879908, 'SZ000778': 626506, 'SZ002065': 268767, 'SZ002085': 267906, 'SZ002146': 2004576, 'SZ002310': 39745, 'SZ002466': 2332750, 'SZ002475': 1026858, 'SZ300017': 806}\n", + "target weight: {'SH600000': 0.0013439859873209908, 'SH600009': 0.02075652616964347, 'SH600028': 0.00939963933310415, 'SH600276': 0.06236017906066887, 'SH600535': 0.016369568294734148, 'SH600649': 0.025541724367766302, 'SH600900': 0.12768966131041845, 'SH601258': 0.1370446945486361, 'SH601398': 0.02601619218529119, 'SH601939': 0.013440958024818669, 'SH603993': 4.144559709761373e-06, 'SZ000503': 0.0084237188568659, 'SZ000568': 0.020576387679160105, 'SZ000709': 0.056783757531829446, 'SZ000778': 0.06920027928808208, 'SZ002008': 0.07943378393922318, 'SZ002065': 0.045339177613740886, 'SZ002085': 0.08505902525865962, 'SZ002146': 0.031624633954490035, 'SZ002310': 0.0008996156348854183, 'SZ002466': 0.0764983539831682, 'SZ002475': 0.086193992434369}\n", + "target position: {'SZ002299': 6184584.0980107365, 'SZ300017': 812.4573136217659, 'SH600000': 16923, 'SH600009': 196076, 'SH600028': 337279, 'SH600276': 46567, 'SH600535': 57624, 'SH600649': 731549, 'SH600900': 2515891, 'SH601258': 26747448, 'SH601398': 1148886, 'SH601939': 1289307, 'SH603993': 263, 'SZ000503': 122158, 'SZ000568': 69471, 'SZ000709': 700781, 'SZ000778': 302643, 'SZ002008': 746285, 'SZ002065': 268804, 'SZ002085': 267988, 'SZ002146': 1473970, 'SZ002310': 39739, 'SZ002466': 2333288, 'SZ002475': 1027134}\n", + "target weight: {'SH600000': 0.0014508867295425067, 'SH600009': 0.022137935734971876, 'SH600028': 0.01003980705499816, 'SH600276': 0.065554410760754, 'SH600535': 0.017337663954140436, 'SH600649': 0.026752732524884384, 'SH600900': 0.13610376526017787, 'SH601258': 0.14230666244775886, 'SH601398': 0.027847743092481312, 'SH601939': 0.014306563408357105, 'SH603993': 2.7770868647848817e-06, 'SZ000069': 0.10104502775773525, 'SZ000503': 0.009049444347506782, 'SZ000568': 0.005686495401232644, 'SZ000778': 0.0715782861850023, 'SZ002008': 0.08609584908472251, 'SZ002065': 0.04706561122827146, 'SZ002085': 0.09099179117275048, 'SZ002146': 0.03204301334262787, 'SZ002475': 0.09241758644387384, 'SZ300017': 0.00018594702102337797}\n", + "target position: {'SZ000709': 700825.0269758024, 'SZ002299': 6184584.0980107365, 'SH600000': 16845, 'SH600009': 195098, 'SH600028': 335689, 'SH600276': 46340, 'SH600535': 57343, 'SH600649': 728078, 'SH600900': 2504242, 'SH601258': 26624542, 'SH601398': 1143577, 'SH601939': 1283067, 'SH603993': 160, 'SZ000069': 367637, 'SZ000503': 121565, 'SZ000568': 17626, 'SZ000778': 301250, 'SZ002008': 742790, 'SZ002065': 267559, 'SZ002085': 266737, 'SZ002146': 1467579, 'SZ002475': 1022346, 'SZ300017': 1776}\n", + "target weight: {'SH600000': 0.0013484985106016394, 'SH600009': 0.020750773768622693, 'SH600028': 0.009285673867962157, 'SH600104': 2.9067007814076732e-05, 'SH600196': 0.10012804077099052, 'SH600276': 0.05943563439541343, 'SH600535': 0.015902136087846228, 'SH600649': 0.025189836387314323, 'SH600900': 0.12584805827140388, 'SH601111': 6.857382365314848e-06, 'SH601258': 0.03895938466363849, 'SH601398': 0.025753888553878806, 'SH601939': 0.013275755331575599, 'SH603993': 4.249178615404585e-06, 'SZ000069': 0.09445579375504781, 'SZ000503': 0.008532747266799033, 'SZ000568': 0.0052599046052527266, 'SZ000709': 0.06003418476540357, 'SZ000778': 0.06923031488245988, 'SZ002008': 0.07903025205993618, 'SZ002065': 0.04448484691775433, 'SZ002085': 0.08426354045447453, 'SZ002146': 0.031142767130486235, 'SZ002475': 0.08747938111190227, 'SZ300017': 0.00016841662419817417}\n", + "target position: {'SZ002299': 6184584.0980107365, 'SH600000': 16906, 'SH600009': 195107, 'SH600028': 335257, 'SH600104': 197, 'SH600196': 630404, 'SH600276': 46282, 'SH600535': 57311, 'SH600649': 727170, 'SH600900': 2500379, 'SH601111': 203, 'SH601258': 7443096, 'SH601398': 1142014, 'SH601939': 1281361, 'SH603993': 263, 'SZ000069': 366998, 'SZ000503': 121479, 'SZ000568': 17699, 'SZ000709': 699639, 'SZ000778': 300752, 'SZ002008': 741767, 'SZ002065': 267133, 'SZ002085': 266334, 'SZ002146': 1465489, 'SZ002475': 1020693, 'SZ300017': 1756}\n", + "target weight: {'SH600000': 0.0012976336004362882, 'SH600009': 0.0204756895024156, 'SH600028': 0.008883617000656601, 'SH600104': 2.592943319382378e-05, 'SH600196': 0.09617041827497698, 'SH600276': 0.05681162545715886, 'SH600535': 0.015294256733040745, 'SH600649': 0.02417676167926707, 'SH600900': 0.12233373885315162, 'SH601398': 0.024531954099214746, 'SH601628': 0.005044154324745466, 'SH601888': 0.09500034426651846, 'SH601939': 0.012657033879067425, 'SH603993': 4.079522960136806e-06, 'SZ000069': 0.09054142453059062, 'SZ000503': 0.008036587259744734, 'SZ000568': 0.0049533657881637655, 'SZ000778': 0.06904486736535222, 'SZ002008': 0.06688985213943154, 'SZ002065': 0.04278977877238287, 'SZ002085': 0.0820368284038888, 'SZ002299': 0.06899317887598991, 'SZ002475': 0.08384652594205952, 'SZ300017': 0.00016035416530955983}\n", + "target position: {'SH601258': 7443495.190430395, 'SH600000': 16952, 'SH600009': 195676, 'SH600028': 336044, 'SH600104': 183, 'SH600196': 631454, 'SH600276': 46372, 'SH600535': 57498, 'SH600649': 728582, 'SH600900': 2504660, 'SH601398': 1143938, 'SH601628': 695470, 'SH601888': 2951253, 'SH601939': 1283887, 'SH603993': 255, 'SZ000069': 367641, 'SZ000503': 121875, 'SZ000568': 17775, 'SZ000778': 301255, 'SZ002008': 638620, 'SZ002065': 267645, 'SZ002085': 266802, 'SZ002299': 6194843, 'SZ002475': 1022527, 'SZ300017': 1765}\n", + "target weight: {'SH600000': 0.0013469483722729403, 'SH600028': 0.009286467498269333, 'SH600104': 2.368500734977497e-05, 'SH600196': 0.10145424564201923, 'SH600276': 0.06002237364700993, 'SH600535': 0.01588332650422844, 'SH600649': 0.025440421851940002, 'SH600900': 0.1279028471227695, 'SH601258': 0.035917606048396986, 'SH601398': 0.02559318344055778, 'SH601628': 0.005221942888216608, 'SH601888': 0.14928498761757883, 'SH601939': 0.013161430940131148, 'SH603993': 4.350147095904942e-06, 'SZ000069': 0.14038473724819095, 'SZ000503': 0.008556251357999256, 'SZ000568': 0.005243511514392524, 'SZ002008': 0.06824325050397591, 'SZ002065': 0.04420632869308568, 'SZ002085': 0.074424247013131, 'SZ002299': 0.0010812901181988855, 'SZ002475': 0.0871460668952185, 'SZ300017': 0.00017049992832446128}\n", + "target position: {'SZ000778': 301254.84776855103, 'SH600000': 16873, 'SH600028': 335064, 'SH600104': 156, 'SH600196': 629613, 'SH600276': 46235, 'SH600535': 57245, 'SH600649': 726346, 'SH600900': 2497776, 'SH601258': 7423462, 'SH601398': 1140689, 'SH601628': 692346, 'SH601888': 4557826, 'SH601939': 1279908, 'SH603993': 261, 'SZ000069': 551887, 'SZ000503': 121344, 'SZ000568': 17697, 'SZ002008': 636943, 'SZ002065': 266904, 'SZ002085': 231781, 'SZ002299': 97527, 'SZ002475': 1019747, 'SZ300017': 1749}\n" ] }, { "output_type": "error", - "ename": "ValueError", - "evalue": "only have -0.104491644538939 SZ002475, require 1448416.2584415162", + "ename": "KeyboardInterrupt", + "evalue": "", "traceback": [ "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m", - "\u001b[1;31mValueError\u001b[0m Traceback (most recent call last)", - "\u001b[1;32m\u001b[0m in \u001b[0;36m\u001b[1;34m\u001b[0m\n\u001b[0;32m 30\u001b[0m \u001b[1;31m# backtest & analysis\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 31\u001b[0m \u001b[0mpar\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mPortAnaRecord\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mrecorder\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mport_analysis_config\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m---> 32\u001b[1;33m \u001b[0mpar\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mgenerate\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m", + "\u001b[1;31mKeyboardInterrupt\u001b[0m Traceback (most recent call last)", + "\u001b[1;32m\u001b[0m in \u001b[0;36m\u001b[1;34m\u001b[0m\n\u001b[0;32m 30\u001b[0m \u001b[1;31m# backtest & analysis\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 31\u001b[0m \u001b[0mpar\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mPortAnaRecord\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mrecorder\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mport_analysis_config\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m---> 32\u001b[1;33m \u001b[0mpar\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mgenerate\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m", "\u001b[1;32md:\\qlib\\qlib\\workflow\\record_temp.py\u001b[0m in \u001b[0;36mgenerate\u001b[1;34m(self, **kwargs)\u001b[0m\n\u001b[0;32m 230\u001b[0m \u001b[1;31m# custom strategy and get backtest\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 231\u001b[0m \u001b[0mpred_score\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0msuper\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mload\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m--> 232\u001b[1;33m \u001b[0mreport_normal\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mpositions_normal\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mnormal_backtest\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mpred_score\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mstrategy\u001b[0m\u001b[1;33m=\u001b[0m\u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mstrategy\u001b[0m\u001b[1;33m,\u001b[0m \u001b[1;33m**\u001b[0m\u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mbacktest_config\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m 233\u001b[0m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mrecorder\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0msave_objects\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;33m**\u001b[0m\u001b[1;33m{\u001b[0m\u001b[1;34m\"report_normal.pkl\"\u001b[0m\u001b[1;33m:\u001b[0m \u001b[0mreport_normal\u001b[0m\u001b[1;33m}\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0martifact_path\u001b[0m\u001b[1;33m=\u001b[0m\u001b[0mPortAnaRecord\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mget_path\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 234\u001b[0m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mrecorder\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0msave_objects\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;33m**\u001b[0m\u001b[1;33m{\u001b[0m\u001b[1;34m\"positions_normal.pkl\"\u001b[0m\u001b[1;33m:\u001b[0m \u001b[0mpositions_normal\u001b[0m\u001b[1;33m}\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0martifact_path\u001b[0m\u001b[1;33m=\u001b[0m\u001b[0mPortAnaRecord\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mget_path\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n", "\u001b[1;32md:\\qlib\\qlib\\contrib\\evaluate.py\u001b[0m in \u001b[0;36mbacktest\u001b[1;34m(pred, account, shift, benchmark, verbose, **kwargs)\u001b[0m\n\u001b[0;32m 269\u001b[0m \u001b[0mverbose\u001b[0m\u001b[1;33m=\u001b[0m\u001b[0mverbose\u001b[0m\u001b[1;33m,\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 270\u001b[0m \u001b[0maccount\u001b[0m\u001b[1;33m=\u001b[0m\u001b[0maccount\u001b[0m\u001b[1;33m,\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m--> 271\u001b[1;33m \u001b[0mbenchmark\u001b[0m\u001b[1;33m=\u001b[0m\u001b[0mbenchmark\u001b[0m\u001b[1;33m,\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m 272\u001b[0m )\n\u001b[0;32m 273\u001b[0m \u001b[1;31m# for compatibility of the old API. return the dict positions\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n", - "\u001b[1;32md:\\qlib\\qlib\\contrib\\backtest\\backtest.py\u001b[0m in \u001b[0;36mbacktest\u001b[1;34m(pred, strategy, trade_exchange, shift, verbose, account, benchmark)\u001b[0m\n\u001b[0;32m 107\u001b[0m \u001b[1;31m# NOTE: The following operation will modify order.amount.\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 108\u001b[0m \u001b[1;31m# NOTE: If it is buy and the cash is insufficient, the tradable amount will be recalculated\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m--> 109\u001b[1;33m \u001b[0mtrade_info\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mexecutor\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mexecute\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mtrade_account\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0morder_list\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mtrade_date\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m 110\u001b[0m \u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 111\u001b[0m \u001b[1;31m# 5. Update account information according to transaction\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n", - "\u001b[1;32md:\\qlib\\qlib\\contrib\\online\\executor.py\u001b[0m in \u001b[0;36mexecute\u001b[1;34m(self, trade_account, order_list, trade_date)\u001b[0m\n\u001b[0;32m 145\u001b[0m \u001b[1;32mif\u001b[0m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mtrade_exchange\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mcheck_order\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0morder\u001b[0m\u001b[1;33m)\u001b[0m \u001b[1;32mis\u001b[0m \u001b[1;32mTrue\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 146\u001b[0m \u001b[1;31m# execute the order\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m--> 147\u001b[1;33m \u001b[0mtrade_val\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mtrade_cost\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mtrade_price\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mtrade_exchange\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mdeal_order\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0morder\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mtrade_account\u001b[0m\u001b[1;33m=\u001b[0m\u001b[0maccount\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m 148\u001b[0m \u001b[0mtrade_info\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mappend\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;33m[\u001b[0m\u001b[0morder\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mtrade_val\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mtrade_cost\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mtrade_price\u001b[0m\u001b[1;33m]\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 149\u001b[0m \u001b[1;32mif\u001b[0m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mverbose\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n", - "\u001b[1;32md:\\qlib\\qlib\\contrib\\backtest\\exchange.py\u001b[0m in \u001b[0;36mdeal_order\u001b[1;34m(self, order, trade_account, position)\u001b[0m\n\u001b[0;32m 209\u001b[0m \u001b[1;31m# Otherwise, it will result some stock with 0 amount in the position\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 210\u001b[0m \u001b[1;32mif\u001b[0m \u001b[0mtrade_account\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m--> 211\u001b[1;33m \u001b[0mtrade_account\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mupdate_order\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0morder\u001b[0m\u001b[1;33m=\u001b[0m\u001b[0morder\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mtrade_val\u001b[0m\u001b[1;33m=\u001b[0m\u001b[0mtrade_val\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mcost\u001b[0m\u001b[1;33m=\u001b[0m\u001b[0mtrade_cost\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mtrade_price\u001b[0m\u001b[1;33m=\u001b[0m\u001b[0mtrade_price\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m 212\u001b[0m \u001b[1;32melif\u001b[0m \u001b[0mposition\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 213\u001b[0m \u001b[0mposition\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mupdate_order\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0morder\u001b[0m\u001b[1;33m=\u001b[0m\u001b[0morder\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mtrade_val\u001b[0m\u001b[1;33m=\u001b[0m\u001b[0mtrade_val\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mcost\u001b[0m\u001b[1;33m=\u001b[0m\u001b[0mtrade_cost\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mtrade_price\u001b[0m\u001b[1;33m=\u001b[0m\u001b[0mtrade_price\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n", - "\u001b[1;32md:\\qlib\\qlib\\contrib\\backtest\\account.py\u001b[0m in \u001b[0;36mupdate_order\u001b[1;34m(self, order, trade_val, cost, trade_price)\u001b[0m\n\u001b[0;32m 77\u001b[0m \u001b[1;31m# update current position\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 78\u001b[0m \u001b[1;31m# for may sell all of stock_id\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m---> 79\u001b[1;33m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mcurrent\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mupdate_order\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0morder\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mtrade_val\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mcost\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mtrade_price\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m 80\u001b[0m \u001b[1;32melse\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 81\u001b[0m \u001b[1;31m# buy stock\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n", - "\u001b[1;32md:\\qlib\\qlib\\contrib\\backtest\\position.py\u001b[0m in \u001b[0;36mupdate_order\u001b[1;34m(self, order, trade_val, cost, trade_price)\u001b[0m\n\u001b[0;32m 81\u001b[0m \u001b[1;32melif\u001b[0m \u001b[0morder\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mdirection\u001b[0m \u001b[1;33m==\u001b[0m \u001b[0mOrder\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mSELL\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 82\u001b[0m \u001b[1;31m# SELL\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m---> 83\u001b[1;33m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0msell_stock\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0morder\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mstock_id\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mtrade_val\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mcost\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mtrade_price\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m 84\u001b[0m \u001b[1;32melse\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 85\u001b[0m \u001b[1;32mraise\u001b[0m \u001b[0mNotImplementedError\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;34m\"do not suppotr order direction {}\"\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mformat\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0morder\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mdirection\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n", - "\u001b[1;32md:\\qlib\\qlib\\contrib\\backtest\\position.py\u001b[0m in \u001b[0;36msell_stock\u001b[1;34m(self, stock_id, trade_val, cost, trade_price)\u001b[0m\n\u001b[0;32m 64\u001b[0m \u001b[1;32mif\u001b[0m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mposition\u001b[0m\u001b[1;33m[\u001b[0m\u001b[0mstock_id\u001b[0m\u001b[1;33m]\u001b[0m\u001b[1;33m[\u001b[0m\u001b[1;34m\"amount\"\u001b[0m\u001b[1;33m]\u001b[0m \u001b[1;33m<\u001b[0m \u001b[1;33m-\u001b[0m\u001b[1;36m1e-5\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 65\u001b[0m raise ValueError(\n\u001b[1;32m---> 66\u001b[1;33m \u001b[1;34m\"only have {} {}, require {}\"\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mformat\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mposition\u001b[0m\u001b[1;33m[\u001b[0m\u001b[0mstock_id\u001b[0m\u001b[1;33m]\u001b[0m\u001b[1;33m[\u001b[0m\u001b[1;34m\"amount\"\u001b[0m\u001b[1;33m]\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mstock_id\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mtrade_amount\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m 67\u001b[0m )\n\u001b[0;32m 68\u001b[0m \u001b[1;32melif\u001b[0m \u001b[0mabs\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mposition\u001b[0m\u001b[1;33m[\u001b[0m\u001b[0mstock_id\u001b[0m\u001b[1;33m]\u001b[0m\u001b[1;33m[\u001b[0m\u001b[1;34m\"amount\"\u001b[0m\u001b[1;33m]\u001b[0m\u001b[1;33m)\u001b[0m \u001b[1;33m<=\u001b[0m \u001b[1;36m1e-5\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n", - "\u001b[1;31mValueError\u001b[0m: only have -0.104491644538939 SZ002475, require 1448416.2584415162" + "\u001b[1;32md:\\qlib\\qlib\\contrib\\backtest\\backtest.py\u001b[0m in \u001b[0;36mbacktest\u001b[1;34m(pred, strategy, trade_exchange, shift, verbose, account, benchmark)\u001b[0m\n\u001b[0;32m 100\u001b[0m \u001b[0mtrade_exchange\u001b[0m\u001b[1;33m=\u001b[0m\u001b[0mtrade_exchange\u001b[0m\u001b[1;33m,\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 101\u001b[0m \u001b[0mpred_date\u001b[0m\u001b[1;33m=\u001b[0m\u001b[0mpred_date\u001b[0m\u001b[1;33m,\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m--> 102\u001b[1;33m \u001b[0mtrade_date\u001b[0m\u001b[1;33m=\u001b[0m\u001b[0mtrade_date\u001b[0m\u001b[1;33m,\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m 103\u001b[0m )\n\u001b[0;32m 104\u001b[0m \u001b[1;32melse\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n", + "\u001b[1;32m\u001b[0m in \u001b[0;36mgenerate_order_list\u001b[1;34m(self, score_series, current, trade_exchange, pred_date, trade_date)\u001b[0m\n\u001b[0;32m 76\u001b[0m \u001b[1;31m# optimize target portfolio\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 77\u001b[0m \u001b[1;32mif\u001b[0m \u001b[0minit_weight\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0msum\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;33m)\u001b[0m \u001b[1;33m>\u001b[0m \u001b[1;36m0\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m---> 78\u001b[1;33m \u001b[0mtarget_weight\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0moptimizer\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mcov\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mscore_series\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0minit_weight\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m 79\u001b[0m \u001b[1;32melse\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 80\u001b[0m \u001b[0mtarget_weight\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0moptimizer\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mcov\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mscore_series\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n", + "\u001b[1;32md:\\qlib\\qlib\\portfolio\\optimizer.py\u001b[0m in \u001b[0;36m__call__\u001b[1;34m(self, S, u, w0)\u001b[0m\n\u001b[0;32m 100\u001b[0m \u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 101\u001b[0m \u001b[1;31m# optimize\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m--> 102\u001b[1;33m \u001b[0mw\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0m_optimize\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mS\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mu\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mw0\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m 103\u001b[0m \u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 104\u001b[0m \u001b[1;31m# restore index if needed\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n", + "\u001b[1;32md:\\qlib\\qlib\\portfolio\\optimizer.py\u001b[0m in \u001b[0;36m_optimize\u001b[1;34m(self, S, u, w0)\u001b[0m\n\u001b[0;32m 126\u001b[0m \u001b[1;31m# mean-variance\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 127\u001b[0m \u001b[1;32mif\u001b[0m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mmethod\u001b[0m \u001b[1;33m==\u001b[0m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mOPT_MVO\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m--> 128\u001b[1;33m \u001b[1;32mreturn\u001b[0m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0m_optimize_mvo\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mS\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mu\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mw0\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m 129\u001b[0m \u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 130\u001b[0m \u001b[1;31m# risk parity\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n", + "\u001b[1;32md:\\qlib\\qlib\\portfolio\\optimizer.py\u001b[0m in \u001b[0;36m_optimize_mvo\u001b[1;34m(self, S, u, w0)\u001b[0m\n\u001b[0;32m 162\u001b[0m \u001b[1;32mand\u001b[0m\u001b[0;31m \u001b[0m\u001b[0;31m`\u001b[0m\u001b[0mlamb\u001b[0m\u001b[0;31m`\u001b[0m \u001b[1;32mis\u001b[0m \u001b[0mthe\u001b[0m \u001b[0mrisk\u001b[0m \u001b[0maversion\u001b[0m \u001b[0mparameter\u001b[0m\u001b[1;33m.\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 163\u001b[0m \"\"\"\n\u001b[1;32m--> 164\u001b[1;33m \u001b[1;32mreturn\u001b[0m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0m_solve\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mlen\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mS\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0m_get_objective_mvo\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mS\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mu\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m,\u001b[0m \u001b[1;33m*\u001b[0m\u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0m_get_constrains\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mw0\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m 165\u001b[0m \u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 166\u001b[0m \u001b[1;32mdef\u001b[0m \u001b[0m_optimize_rp\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mself\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mS\u001b[0m\u001b[1;33m:\u001b[0m \u001b[0mnp\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mndarray\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mw0\u001b[0m\u001b[1;33m:\u001b[0m \u001b[0mOptional\u001b[0m\u001b[1;33m[\u001b[0m\u001b[0mnp\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mndarray\u001b[0m\u001b[1;33m]\u001b[0m \u001b[1;33m=\u001b[0m \u001b[1;32mNone\u001b[0m\u001b[1;33m)\u001b[0m \u001b[1;33m->\u001b[0m \u001b[0mnp\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mndarray\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n", + "\u001b[1;32md:\\qlib\\qlib\\portfolio\\optimizer.py\u001b[0m in \u001b[0;36m_solve\u001b[1;34m(self, n, obj, bounds, cons)\u001b[0m\n\u001b[0;32m 252\u001b[0m \u001b[1;31m# solve\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 253\u001b[0m \u001b[0mx0\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mnp\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mones\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mn\u001b[0m\u001b[1;33m)\u001b[0m \u001b[1;33m/\u001b[0m \u001b[0mn\u001b[0m \u001b[1;31m# init results\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m--> 254\u001b[1;33m \u001b[0msol\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mso\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mminimize\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mwrapped_obj\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mx0\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mbounds\u001b[0m\u001b[1;33m=\u001b[0m\u001b[0mbounds\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mconstraints\u001b[0m\u001b[1;33m=\u001b[0m\u001b[0mcons\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mtol\u001b[0m\u001b[1;33m=\u001b[0m\u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mtol\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m 255\u001b[0m \u001b[1;32mif\u001b[0m \u001b[1;32mnot\u001b[0m \u001b[0msol\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0msuccess\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 256\u001b[0m \u001b[0mwarnings\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mwarn\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;34mf\"optimization not success ({sol.status})\"\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n", + "\u001b[1;32m~\\AppData\\Local\\Continuum\\miniconda3\\envs\\qlib\\lib\\site-packages\\scipy\\optimize\\_minimize.py\u001b[0m in \u001b[0;36mminimize\u001b[1;34m(fun, x0, args, method, jac, hess, hessp, bounds, constraints, tol, callback, options)\u001b[0m\n\u001b[0;32m 624\u001b[0m \u001b[1;32melif\u001b[0m \u001b[0mmeth\u001b[0m \u001b[1;33m==\u001b[0m \u001b[1;34m'slsqp'\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 625\u001b[0m return _minimize_slsqp(fun, x0, args, jac, bounds,\n\u001b[1;32m--> 626\u001b[1;33m constraints, callback=callback, **options)\n\u001b[0m\u001b[0;32m 627\u001b[0m \u001b[1;32melif\u001b[0m \u001b[0mmeth\u001b[0m \u001b[1;33m==\u001b[0m \u001b[1;34m'trust-constr'\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 628\u001b[0m return _minimize_trustregion_constr(fun, x0, args, jac, hess, hessp,\n", + "\u001b[1;32m~\\AppData\\Local\\Continuum\\miniconda3\\envs\\qlib\\lib\\site-packages\\scipy\\optimize\\slsqp.py\u001b[0m in \u001b[0;36m_minimize_slsqp\u001b[1;34m(func, x0, args, jac, bounds, constraints, maxiter, ftol, iprint, disp, eps, callback, finite_diff_rel_step, **unknown_options)\u001b[0m\n\u001b[0;32m 419\u001b[0m n1, n2, n3)\n\u001b[0;32m 420\u001b[0m \u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m--> 421\u001b[1;33m \u001b[1;32mif\u001b[0m \u001b[0mmode\u001b[0m \u001b[1;33m==\u001b[0m \u001b[1;36m1\u001b[0m\u001b[1;33m:\u001b[0m \u001b[1;31m# objective and constraint evaluation required\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m 422\u001b[0m \u001b[0mfx\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0msf\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mfun\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mx\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 423\u001b[0m \u001b[0mc\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0m_eval_constraint\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mx\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mcons\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n", + "\u001b[1;31mKeyboardInterrupt\u001b[0m: " ] } ], From 55acac9fd51cdc8d68cdd548e9403599f389e194 Mon Sep 17 00:00:00 2001 From: Dong Zhou Date: Fri, 27 Nov 2020 10:42:20 +0800 Subject: [PATCH 197/241] black format --- qlib/contrib/model/pytorch_gru.py | 1 - qlib/contrib/model/pytorch_lstm.py | 1 - qlib/contrib/model/pytorch_sfm.py | 4 +--- 3 files changed, 1 insertion(+), 5 deletions(-) diff --git a/qlib/contrib/model/pytorch_gru.py b/qlib/contrib/model/pytorch_gru.py index 282ce72dd..02664b6ac 100755 --- a/qlib/contrib/model/pytorch_gru.py +++ b/qlib/contrib/model/pytorch_gru.py @@ -146,7 +146,6 @@ class GRU(Model): raise ValueError("unknown metric `%s`" % self.metric) - def train_epoch(self, x_train, y_train): x_train_values = x_train.values diff --git a/qlib/contrib/model/pytorch_lstm.py b/qlib/contrib/model/pytorch_lstm.py index 844a08a83..f8951509a 100755 --- a/qlib/contrib/model/pytorch_lstm.py +++ b/qlib/contrib/model/pytorch_lstm.py @@ -146,7 +146,6 @@ class LSTM(Model): raise ValueError("unknown metric `%s`" % self.metric) - def train_epoch(self, x_train, y_train): x_train_values = x_train.values diff --git a/qlib/contrib/model/pytorch_sfm.py b/qlib/contrib/model/pytorch_sfm.py index f8a96bc84..1d27f3927 100644 --- a/qlib/contrib/model/pytorch_sfm.py +++ b/qlib/contrib/model/pytorch_sfm.py @@ -100,9 +100,7 @@ class SFM_Model(nn.Module): x_c = torch.matmul(x * B_W[0], self.W_c) + self.b_c x_o = torch.matmul(x * B_W[0], self.W_o) + self.b_o - i = self.inner_activation( - x_i + torch.matmul(h_tm1 * B_U[0], self.U_i) - ) + i = self.inner_activation(x_i + torch.matmul(h_tm1 * B_U[0], self.U_i)) ste = self.inner_activation(x_ste + torch.matmul(h_tm1 * B_U[0], self.U_ste)) fre = self.inner_activation(x_fre + torch.matmul(h_tm1 * B_U[0], self.U_fre)) From c8355f9f184e670d98733210cfab7d7df1a0393c Mon Sep 17 00:00:00 2001 From: lwwang1995 Date: Fri, 27 Nov 2020 11:03:44 +0800 Subject: [PATCH 198/241] Fix alstm model. --- qlib/contrib/model/pytorch_alstm.py | 67 ++++++++++------------------- 1 file changed, 23 insertions(+), 44 deletions(-) diff --git a/qlib/contrib/model/pytorch_alstm.py b/qlib/contrib/model/pytorch_alstm.py index 7b4943db2..8f5ddc486 100644 --- a/qlib/contrib/model/pytorch_alstm.py +++ b/qlib/contrib/model/pytorch_alstm.py @@ -9,8 +9,10 @@ import os import numpy as np import pandas as pd import copy -from ...utils import create_save_path -from ...log import get_module_logger +from sklearn.metrics import roc_auc_score, mean_squared_error +import logging +from ...utils import unpack_archive_with_buffer, save_multiple_parts_file, create_save_path, drop_nan_by_y_index +from ...log import get_module_logger, TimeInspector import torch import torch.nn as nn @@ -51,7 +53,6 @@ class ALSTM(Model): optimizer="adam", GPU="0", seed=0, - rnn_type="GRU", **kwargs ): # Set logger. @@ -73,7 +74,6 @@ class ALSTM(Model): self.visible_GPU = GPU self.use_gpu = torch.cuda.is_available() self.seed = seed - self.rnn_type = rnn_type self.logger.info( "ALSTM parameters setting:" @@ -90,8 +90,7 @@ class ALSTM(Model): "\nloss_type : {}" "\nvisible_GPU : {}" "\nuse_GPU : {}" - "\nseed : {}" - "\nrnn_type : {}".format( + "\nseed : {}".format( d_feat, hidden_size, num_layers, @@ -106,24 +105,22 @@ class ALSTM(Model): GPU, self.use_gpu, seed, - self.rnn_type, ) ) - self.alstm_model = ALSTMModel( + self.ALSTM_model = ALSTMModel( d_feat=self.d_feat, hidden_size=self.hidden_size, num_layers=self.num_layers, dropout=self.dropout ) - if optimizer.lower() == "adam": - self.train_optimizer = optim.Adam(self.alstm_model.parameters(), lr=self.lr) + self.train_optimizer = optim.Adam(self.ALSTM_model.parameters(), lr=self.lr) elif optimizer.lower() == "gd": - self.train_optimizer = optim.SGD(self.alstm_model.parameters(), lr=self.lr) + self.train_optimizer = optim.SGD(self.ALSTM_model.parameters(), lr=self.lr) else: raise NotImplementedError("optimizer {} is not supported!".format(optimizer)) self._fitted = False if self.use_gpu: - self.alstm_model.cuda() + self.ALSTM_model.cuda() # set the visible GPU if self.visible_GPU: os.environ["CUDA_VISIBLE_DEVICES"] = self.visible_GPU @@ -141,6 +138,7 @@ class ALSTM(Model): raise ValueError("unknown loss `%s`" % self.loss) def metric_fn(self, pred, label): + mask = torch.isfinite(label) if self.metric == "" or self.metric == "loss": # use loss @@ -148,12 +146,13 @@ class ALSTM(Model): raise ValueError("unknown metric `%s`" % self.metric) + def train_epoch(self, x_train, y_train): x_train_values = x_train.values y_train_values = np.squeeze(y_train.values) - self.alstm_model.train() + self.ALSTM_model.train() indices = np.arange(len(x_train_values)) np.random.shuffle(indices) @@ -170,21 +169,21 @@ class ALSTM(Model): feature = feature.cuda() label = label.cuda() - pred = self.alstm_model(feature) + pred = self.ALSTM_model(feature) loss = self.loss_fn(pred, label) self.train_optimizer.zero_grad() loss.backward() - torch.nn.utils.clip_grad_value_(self.alstm_model.parameters(), 3.0) + torch.nn.utils.clip_grad_value_(self.ALSTM_model.parameters(), 3.0) self.train_optimizer.step() def test_epoch(self, data_x, data_y): - # prepare testing data + # prepare training data x_values = data_x.values y_values = np.squeeze(data_y.values) - self.alstm_model.eval() + self.ALSTM_model.eval() scores = [] losses = [] @@ -203,7 +202,7 @@ class ALSTM(Model): feature = feature.cuda() label = label.cuda() - pred = self.alstm_model(feature) + pred = self.ALSTM_model(feature) loss = self.loss_fn(pred, label) losses.append(loss.item()) @@ -230,6 +229,7 @@ class ALSTM(Model): if save_path == None: save_path = create_save_path(save_path) stop_steps = 0 + train_loss = 0 best_score = -np.inf best_epoch = 0 evals_result["train"] = [] @@ -254,7 +254,7 @@ class ALSTM(Model): best_score = val_score stop_steps = 0 best_epoch = step - best_param = copy.deepcopy(self.alstm_model.state_dict()) + best_param = copy.deepcopy(self.ALSTM_model.state_dict()) else: stop_steps += 1 if stop_steps >= self.early_stop: @@ -262,7 +262,7 @@ class ALSTM(Model): break self.logger.info("best score: %.6lf @ %d" % (best_score, best_epoch)) - self.alstm_model.load_state_dict(best_param) + self.ALSTM_model.load_state_dict(best_param) torch.save(best_param, save_path) if self.use_gpu: @@ -274,7 +274,7 @@ class ALSTM(Model): x_test = dataset.prepare("test", col_set="feature") index = x_test.index - self.alstm_model.eval() + self.ALSTM_model.eval() x_values = x_test.values sample_num = x_values.shape[0] preds = [] @@ -293,36 +293,15 @@ class ALSTM(Model): with torch.no_grad(): if self.use_gpu: - pred = self.alstm_model(x_batch).detach().cpu().numpy() + pred = self.ALSTM_model(x_batch).detach().cpu().numpy() else: - pred = self.alstm_model(x_batch).detach().numpy() + pred = self.ALSTM_model(x_batch).detach().numpy() preds.append(pred) return pd.Series(np.concatenate(preds), index=index) -class GRUModel(nn.Module): - def __init__(self, d_feat=6, hidden_size=64, num_layers=2, dropout=0.0): - super().__init__() - - self.rnn = nn.GRU( - input_size=d_feat, - hidden_size=hidden_size, - num_layers=num_layers, - batch_first=True, - dropout=dropout, - ) - self.fc_out = nn.Linear(hidden_size, 1) - self.d_feat = d_feat - - def forward(self, x): - x = x.reshape(len(x), self.d_feat, -1) # [N, F, T] - x = x.permute(0, 2, 1) # [N, T, F] - out, _ = self.rnn(x) - return self.fc_out(out[:, -1, :]).squeeze() - - class ALSTMModel(nn.Module): def __init__(self, d_feat=6, hidden_size=64, num_layers=2, dropout=0.0, rnn_type="GRU"): super().__init__() From a144a9c3c6b5c720ad49c214f6e9256f4b742fe1 Mon Sep 17 00:00:00 2001 From: bxdd Date: Fri, 27 Nov 2020 13:00:14 +0800 Subject: [PATCH 199/241] update dnn --- .../benchmarks/DNN/workflow_config_dnn.yaml | 27 ++++++++++++++----- qlib/contrib/data/handler.py | 2 ++ qlib/contrib/model/pytorch_nn.py | 16 +++-------- qlib/data/dataset/processor.py | 10 +++++++ 4 files changed, 36 insertions(+), 19 deletions(-) diff --git a/examples/benchmarks/DNN/workflow_config_dnn.yaml b/examples/benchmarks/DNN/workflow_config_dnn.yaml index bf5bd7c5f..023d1cd49 100644 --- a/examples/benchmarks/DNN/workflow_config_dnn.yaml +++ b/examples/benchmarks/DNN/workflow_config_dnn.yaml @@ -10,15 +10,30 @@ data_handler_config: &data_handler_config instruments: *market infer_processors: [ { - "class" : "CSZFillna", - "kwargs":{"fields_group": "feature"} + "class" : "DropCol", + "kwargs":{"col_list": ["VWAP0"]} }, { - "class" : "Fillna", - "kwargs":{"fields_group": "feature"} + "class" : "CSZFillna", + "kwargs":{"fields_group": "feature"} } ] - learn_processors: ["DropnaLabel", {"class": "CSZScoreNorm", "kwargs": {"fields_group": "label"}}] + learn_processors: [ + { + "class" : "DropCol", + "kwargs":{"col_list": ["VWAP0"]} + }, + { + "class" : "DropnaProcessor", + "kwargs":{"fields_group": "feature"} + }, + "DropnaLabel", + { + "class": "CSZScoreNorm", + "kwargs": {"fields_group": "label"} + } + ] + process_type: "independent" port_analysis_config: &port_analysis_config strategy: @@ -42,7 +57,7 @@ task: module_path: qlib.contrib.model.pytorch_nn kwargs: loss: mse - input_dim: 158 + input_dim: 157 output_dim: 1 lr: 0.002 lr_decay: 0.96 diff --git a/qlib/contrib/data/handler.py b/qlib/contrib/data/handler.py index 8cce92907..3668a0cc0 100644 --- a/qlib/contrib/data/handler.py +++ b/qlib/contrib/data/handler.py @@ -171,6 +171,7 @@ class Alpha158(DataHandlerLP): learn_processors=["DropnaLabel", {"class": "CSZScoreNorm", "kwargs": {"fields_group": "label"}}], fit_start_time=None, fit_end_time=None, + process_type=DataHandlerLP.PTYPE_A ): def check_transform_proc(proc_l): new_l = [] @@ -209,6 +210,7 @@ class Alpha158(DataHandlerLP): data_loader=data_loader, infer_processors=infer_processors, learn_processors=learn_processors, + process_type=process_type ) def get_feature_config(self): diff --git a/qlib/contrib/model/pytorch_nn.py b/qlib/contrib/model/pytorch_nn.py index e1b0736e2..47316ebf6 100644 --- a/qlib/contrib/model/pytorch_nn.py +++ b/qlib/contrib/model/pytorch_nn.py @@ -20,7 +20,7 @@ from ...data.dataset import DatasetH from ...data.dataset.handler import DataHandlerLP from ...utils import unpack_archive_with_buffer, save_multiple_parts_file, create_save_path, drop_nan_by_y_index from ...log import get_module_logger, TimeInspector - +from ...workflow import R class DNNModelPytorch(Model): """DNN Model @@ -151,7 +151,6 @@ class DNNModelPytorch(Model): verbose=True, save_path=None, ): - df_train, df_valid = dataset.prepare( ["train", "valid"], col_set=["feature", "label"], data_key=DataHandlerLP.DK_L ) @@ -170,7 +169,6 @@ class DNNModelPytorch(Model): best_loss = np.inf evals_result["train"] = [] evals_result["valid"] = [] - # train self.logger.info("training...") self._fitted = True @@ -184,9 +182,6 @@ class DNNModelPytorch(Model): x_val_auto = torch.from_numpy(x_valid.values).float() y_val_auto = torch.from_numpy(y_valid.values).float() w_val_auto = torch.from_numpy(w_valid.values).float() - #print('valiadationx:', x_val_auto) - #print('valiadationy:', y_val_auto) - #print('valiadationw:', w_val_auto) if self.use_GPU: x_val_auto = x_val_auto.cuda() y_val_auto = y_val_auto.cuda() @@ -200,7 +195,6 @@ class DNNModelPytorch(Model): loss = AverageMeter() self.dnn_model.train() self.train_optimizer.zero_grad() - choice = np.random.choice(train_num, self.batch_size) x_batch_auto = x_train_values[choice] y_batch_auto = y_train_values[choice] @@ -213,16 +207,14 @@ class DNNModelPytorch(Model): # forward preds = self.dnn_model(x_batch_auto) - #print('pred_train:', preds.detach().cpu().numpy()) - #print('label_train:', y_batch_auto.cpu().numpy()) cur_loss = self.get_loss(preds, w_batch_auto, y_batch_auto, self.loss_type) cur_loss.backward() self.train_optimizer.step() loss.update(cur_loss.item()) + R.log_metrics(train_loss=loss.avg, step=step) # validation train_loss += loss.val - # print(loss.val) if step and step % self.eval_steps == 0: stop_steps += 1 train_loss /= self.eval_steps @@ -232,10 +224,10 @@ class DNNModelPytorch(Model): loss_val = AverageMeter() # forward - preds = self.dnn_model(x_val_auto) cur_loss_val = self.get_loss(preds, w_val_auto, y_val_auto, self.loss_type) loss_val.update(cur_loss_val.item()) + R.log_metrics(val_loss=loss_val.val, step=step) if verbose: self.logger.info( "[Epoch {}]: train_loss {:.6f}, valid_loss {:.6f}".format(step, train_loss, loss_val.val) @@ -276,7 +268,6 @@ class DNNModelPytorch(Model): if not self._fitted: raise ValueError("model is not fitted yet!") x_test_pd = dataset.prepare("test", col_set="feature") - print(x_test_pd) x_test = torch.from_numpy(x_test_pd.values).float() if self.use_GPU: x_test = x_test.cuda() @@ -287,7 +278,6 @@ class DNNModelPytorch(Model): preds = self.dnn_model(x_test).detach().cpu().numpy() else: preds = self.dnn_model(x_test).detach().numpy() - print(preds) return pd.Series(np.squeeze(preds), index=x_test_pd.index) def save(self, filename, **kwargs): diff --git a/qlib/data/dataset/processor.py b/qlib/data/dataset/processor.py index 32b42462f..4a2d36e2f 100755 --- a/qlib/data/dataset/processor.py +++ b/qlib/data/dataset/processor.py @@ -90,7 +90,17 @@ class DropnaLabel(DropnaProcessor): return False +class DropCol(Processor): + def __init__(self, col_list=[]): + self.col_list = col_list + def __call__(self, df): + if isinstance(df.columns, pd.MultiIndex): + mask = df.columns.get_level_values(-1).isin(self.col_list) + else: + mask = df.columns.isin(self.col_list) + return df.loc[:, ~mask] + class TanhProcess(Processor): """ Use tanh to process noise data""" From 8b275a60063c9b72c19a24401f827cee866d1a0e Mon Sep 17 00:00:00 2001 From: bxdd Date: Fri, 27 Nov 2020 13:07:18 +0800 Subject: [PATCH 200/241] fix bug --- examples/benchmarks/DNN/workflow_config_dnn.yaml | 2 +- qlib/contrib/data/handler.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/benchmarks/DNN/workflow_config_dnn.yaml b/examples/benchmarks/DNN/workflow_config_dnn.yaml index 023d1cd49..e01c4eb3a 100644 --- a/examples/benchmarks/DNN/workflow_config_dnn.yaml +++ b/examples/benchmarks/DNN/workflow_config_dnn.yaml @@ -1,4 +1,4 @@ -provider_uri: "~/.qlib/qlib_data/cn_data_new" +provider_uri: "~/.qlib/qlib_data/cn_data" region: cn market: &market csi300 benchmark: &benchmark SH000300 diff --git a/qlib/contrib/data/handler.py b/qlib/contrib/data/handler.py index e61d26254..f74c2cebc 100644 --- a/qlib/contrib/data/handler.py +++ b/qlib/contrib/data/handler.py @@ -207,7 +207,7 @@ class Alpha158(DataHandlerLP): learn_processors=_DEFAULT_LEARN_PROCESSORS, fit_start_time=None, fit_end_time=None, - process_type=DataHandlerLP.PTYPE_A + process_type=DataHandlerLP.PTYPE_A, **kwargs, ): infer_processors = check_transform_proc(infer_processors, fit_start_time, fit_end_time) From ae757a4b5198a75c8650548acef52aac9a33f73e Mon Sep 17 00:00:00 2001 From: bxdd Date: Fri, 27 Nov 2020 13:09:40 +0800 Subject: [PATCH 201/241] black format --- qlib/contrib/data/handler.py | 2 +- qlib/contrib/model/pytorch_alstm.py | 1 - qlib/contrib/model/pytorch_nn.py | 3 ++- qlib/data/dataset/processor.py | 7 +++++-- 4 files changed, 8 insertions(+), 5 deletions(-) diff --git a/qlib/contrib/data/handler.py b/qlib/contrib/data/handler.py index f74c2cebc..e97b00c24 100644 --- a/qlib/contrib/data/handler.py +++ b/qlib/contrib/data/handler.py @@ -226,7 +226,7 @@ class Alpha158(DataHandlerLP): data_loader=data_loader, infer_processors=infer_processors, learn_processors=learn_processors, - process_type=process_type + process_type=process_type, ) def get_feature_config(self): diff --git a/qlib/contrib/model/pytorch_alstm.py b/qlib/contrib/model/pytorch_alstm.py index 8f5ddc486..1b23d2401 100644 --- a/qlib/contrib/model/pytorch_alstm.py +++ b/qlib/contrib/model/pytorch_alstm.py @@ -146,7 +146,6 @@ class ALSTM(Model): raise ValueError("unknown metric `%s`" % self.metric) - def train_epoch(self, x_train, y_train): x_train_values = x_train.values diff --git a/qlib/contrib/model/pytorch_nn.py b/qlib/contrib/model/pytorch_nn.py index 47316ebf6..d324e27aa 100644 --- a/qlib/contrib/model/pytorch_nn.py +++ b/qlib/contrib/model/pytorch_nn.py @@ -22,6 +22,7 @@ from ...utils import unpack_archive_with_buffer, save_multiple_parts_file, creat from ...log import get_module_logger, TimeInspector from ...workflow import R + class DNNModelPytorch(Model): """DNN Model @@ -349,7 +350,7 @@ class Net(nn.Module): def _weight_init(self): for m in self.modules(): if isinstance(m, nn.Linear): - nn.init.kaiming_normal_(m.weight, a=0.1, mode='fan_in', nonlinearity='leaky_relu') + nn.init.kaiming_normal_(m.weight, a=0.1, mode="fan_in", nonlinearity="leaky_relu") def forward(self, x): cur_output = x diff --git a/qlib/data/dataset/processor.py b/qlib/data/dataset/processor.py index fc85ccde9..76cf85c4a 100755 --- a/qlib/data/dataset/processor.py +++ b/qlib/data/dataset/processor.py @@ -100,7 +100,8 @@ class DropCol(Processor): else: mask = df.columns.isin(self.col_list) return df.loc[:, ~mask] - + + class TanhProcess(Processor): """ Use tanh to process noise data""" @@ -133,6 +134,7 @@ class ProcessInf(Processor): return replace_inf(df) + class Fillna(Processor): """Process NaN""" @@ -270,6 +272,7 @@ class CSRankNorm(Processor): df[cols] = t return df + class CSZFillna(Processor): """Cross Sectional Fill Nan""" @@ -279,4 +282,4 @@ class CSZFillna(Processor): def __call__(self, df): cols = get_group_columns(df, self.fields_group) df[cols] = df[cols].groupby("datetime").apply(lambda x: x.fillna(x.mean())) - return df \ No newline at end of file + return df From a9c1f8c2a0dd86adfc1683d82bfe6acb6d2e5497 Mon Sep 17 00:00:00 2001 From: Haoyu Wang <48108414+javaThonc@users.noreply.github.com> Date: Fri, 27 Nov 2020 17:16:35 +0800 Subject: [PATCH 202/241] Update workflow_config_xgboost.yaml --- .../XGBoost/workflow_config_xgboost.yaml | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/examples/benchmarks/XGBoost/workflow_config_xgboost.yaml b/examples/benchmarks/XGBoost/workflow_config_xgboost.yaml index 31eee8206..398f8bd9e 100644 --- a/examples/benchmarks/XGBoost/workflow_config_xgboost.yaml +++ b/examples/benchmarks/XGBoost/workflow_config_xgboost.yaml @@ -30,14 +30,12 @@ task: module_path: qlib.contrib.model.xgboost kwargs: eval_metric: rmse - colsample_bytree: 0.5 - eta: 0.2 - gamma: 0.55 - max_depth: 2 - min_child_weight: 1.0 - n_estimators: 647 - subsample: 0.8 - nthread: 4 + colsample_bytree: 0.8879 + eta: 0.0421 + max_depth: 8 + n_estimators: 650 + subsample: 0.8789 + nthread: 20 dataset: class: DatasetH module_path: qlib.data.dataset @@ -62,4 +60,4 @@ task: - class: PortAnaRecord module_path: qlib.workflow.record_temp kwargs: - config: *port_analysis_config \ No newline at end of file + config: *port_analysis_config From c62f3164a270813996e72640b345c70094cf8b8a Mon Sep 17 00:00:00 2001 From: Haoyu Wang <48108414+javaThonc@users.noreply.github.com> Date: Fri, 27 Nov 2020 17:17:57 +0800 Subject: [PATCH 203/241] update config --- examples/benchmarks/XGBoost/workflow_config_xgboost.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/benchmarks/XGBoost/workflow_config_xgboost.yaml b/examples/benchmarks/XGBoost/workflow_config_xgboost.yaml index 398f8bd9e..1352c496d 100644 --- a/examples/benchmarks/XGBoost/workflow_config_xgboost.yaml +++ b/examples/benchmarks/XGBoost/workflow_config_xgboost.yaml @@ -33,7 +33,7 @@ task: colsample_bytree: 0.8879 eta: 0.0421 max_depth: 8 - n_estimators: 650 + n_estimators: 647 subsample: 0.8789 nthread: 20 dataset: From e4e730bada1791cf9d228bc18b6f86af7effb95f Mon Sep 17 00:00:00 2001 From: Dong Zhou Date: Fri, 27 Nov 2020 17:46:41 +0800 Subject: [PATCH 204/241] update portfolio example --- examples/portfolio_optimization_example.ipynb | 161 +++++++++--------- 1 file changed, 76 insertions(+), 85 deletions(-) diff --git a/examples/portfolio_optimization_example.ipynb b/examples/portfolio_optimization_example.ipynb index 4d6c2b3d2..7ef593efa 100644 --- a/examples/portfolio_optimization_example.ipynb +++ b/examples/portfolio_optimization_example.ipynb @@ -57,10 +57,10 @@ "output_type": "stream", "name": "stderr", "text": [ - "[35366:MainThread](2020-11-27 10:31:09,528) INFO - qlib.Initialization - [__init__.py:41] - default_conf: client.\n", - "[35366:MainThread](2020-11-27 10:31:09,531) WARNING - qlib.Initialization - [__init__.py:57] - redis connection failed(host=127.0.0.1 port=6379), cache will not be used!\n", - "[35366:MainThread](2020-11-27 10:31:09,531) INFO - qlib.Initialization - [__init__.py:76] - qlib successfully initialized based on client settings.\n", - "[35366:MainThread](2020-11-27 10:31:09,532) INFO - qlib.Initialization - [__init__.py:79] - data_path=/home/dongzho/.qlib/qlib_data/cn_data\n" + "[36502:MainThread](2020-11-27 16:26:57,240) INFO - qlib.Initialization - [__init__.py:41] - default_conf: client.\n", + "[36502:MainThread](2020-11-27 16:26:57,242) WARNING - qlib.Initialization - [__init__.py:57] - redis connection failed(host=127.0.0.1 port=6379), cache will not be used!\n", + "[36502:MainThread](2020-11-27 16:26:57,243) INFO - qlib.Initialization - [__init__.py:76] - qlib successfully initialized based on client settings.\n", + "[36502:MainThread](2020-11-27 16:26:57,244) INFO - qlib.Initialization - [__init__.py:79] - data_path=/home/dongzho/.qlib/qlib_data/cn_data\n" ] } ], @@ -102,14 +102,14 @@ "output_type": "stream", "name": "stderr", "text": [ - "[35366:MainThread](2020-11-27 10:31:29,731) INFO - qlib.timer - [log.py:81] - Time cost: 20.103s | Loading data Done\n", - "[35366:MainThread](2020-11-27 10:31:30,557) INFO - qlib.timer - [log.py:81] - Time cost: 0.241s | DropnaLabel Done\n", - "[35366:MainThread](2020-11-27 10:31:38,518) INFO - qlib.timer - [log.py:81] - Time cost: 7.960s | CSZScoreNorm Done\n", - "[35366:MainThread](2020-11-27 10:31:38,519) INFO - qlib.timer - [log.py:81] - Time cost: 8.786s | fit & process data Done\n", - "[35366:MainThread](2020-11-27 10:31:38,520) INFO - qlib.timer - [log.py:81] - Time cost: 28.891s | Init data Done\n", - "[35366:MainThread](2020-11-27 10:31:38,527) INFO - qlib.workflow - [exp.py:180] - Experiment 2 starts running ...\n", - "[35366:MainThread](2020-11-27 10:31:38,651) INFO - qlib.workflow - [recorder.py:234] - Recorder c81375e3b5474feb9c77711babd158c3 starts running under Experiment 2 ...\n", - "[35366:MainThread](2020-11-27 10:31:38,652) INFO - qlib.workflow - [expm.py:251] - No tracking URI is provided. The default tracking URI is set as `mlruns` under the working directory.\n", + "[36502:MainThread](2020-11-27 16:27:17,338) INFO - qlib.timer - [log.py:81] - Time cost: 19.994s | Loading data Done\n", + "[36502:MainThread](2020-11-27 16:27:18,164) INFO - qlib.timer - [log.py:81] - Time cost: 0.245s | DropnaLabel Done\n", + "[36502:MainThread](2020-11-27 16:27:26,086) INFO - qlib.timer - [log.py:81] - Time cost: 7.921s | CSZScoreNorm Done\n", + "[36502:MainThread](2020-11-27 16:27:26,087) INFO - qlib.timer - [log.py:81] - Time cost: 8.747s | fit & process data Done\n", + "[36502:MainThread](2020-11-27 16:27:26,088) INFO - qlib.timer - [log.py:81] - Time cost: 28.744s | Init data Done\n", + "[36502:MainThread](2020-11-27 16:27:26,097) INFO - qlib.workflow - [exp.py:180] - Experiment 2 starts running ...\n", + "[36502:MainThread](2020-11-27 16:27:26,221) INFO - qlib.workflow - [recorder.py:234] - Recorder 3fa4def1f6694119a3d336a7a06c88cb starts running under Experiment 2 ...\n", + "[36502:MainThread](2020-11-27 16:27:26,223) INFO - qlib.workflow - [expm.py:251] - No tracking URI is provided. The default tracking URI is set as `mlruns` under the working directory.\n", "Training until validation scores don't improve for 50 rounds\n", "[20]\ttrain's l2: 0.990559\tvalid's l2: 0.994332\n", "[40]\ttrain's l2: 0.98687\tvalid's l2: 0.993702\n", @@ -164,7 +164,7 @@ " \"segments\": {\n", " \"train\": (\"2008-01-01\", \"2014-12-31\"),\n", " \"valid\": (\"2015-01-01\", \"2016-12-31\"),\n", - " \"test\": (\"2017-01-01\", \"2020-08-01\"),\n", + " \"test\": (\"2017-01-01\", \"2017-12-31\"), # NOTE: use a shorter time range\n", " },\n", " },\n", " },\n", @@ -271,22 +271,19 @@ " )\n", "\n", " # optimize target portfolio\n", - " if init_weight.sum() > 0:\n", - " target_weight = self.optimizer(cov, score_series, init_weight)\n", - " else:\n", - " target_weight = self.optimizer(cov, score_series)\n", - " target_weight = target_weight[target_weight > 1e-6]\n", - " for stock_id, weight in target_weight.items():\n", - " try:\n", + " try:\n", + " if init_weight.sum() > 0:\n", + " target_weight = self.optimizer(cov, score_series, init_weight)\n", + " else:\n", + " target_weight = self.optimizer(cov, score_series)\n", + " target_weight = target_weight[target_weight > 1e-6]\n", + " for stock_id, weight in target_weight.items():\n", " target_position[stock_id] = int(traded_value * weight / trade_exchange.get_close(stock_id, pred_date))\n", - " except Exception as e:\n", - " # TODO: unknown exception\n", - " print('Exception:', e)\n", - "\n", - " # for debug\n", - " print('trade date:', trade_date)\n", - " print('target weight:', target_weight.to_dict())\n", - " print('target position:', target_position)\n", + " except Exception as e:\n", + " print('Unknown exception:', trade_date, e)\n", + " for stock_id in score_series.index:\n", + " if stock_id in current_position:\n", + " target_position[stock_id] = current_position[stock_id]\n", "\n", " # generate order list\n", " order_list = trade_exchange.generate_order_for_target_amount_position(\n", @@ -319,8 +316,8 @@ "output_type": "stream", "name": "stderr", "text": [ - "[35366:MainThread](2020-11-27 10:31:56,951) INFO - qlib.timer - [log.py:81] - Time cost: 6.763s | Loading data Done\n", - "[35366:MainThread](2020-11-27 10:31:56,953) INFO - qlib.timer - [log.py:81] - Time cost: 6.766s | Init data Done\n" + "[36502:MainThread](2020-11-27 16:27:43,722) INFO - qlib.timer - [log.py:81] - Time cost: 6.369s | Loading data Done\n", + "[36502:MainThread](2020-11-27 16:27:43,724) INFO - qlib.timer - [log.py:81] - Time cost: 6.371s | Init data Done\n" ] } ], @@ -334,7 +331,7 @@ }, { "cell_type": "code", - "execution_count": 49, + "execution_count": 8, "metadata": { "tags": [] }, @@ -343,60 +340,54 @@ "output_type": "stream", "name": "stderr", "text": [ - "1': 0.08936553334387595, 'SH601800': 0.011014844457113308, 'SH601939': 0.013378001170219945, 'SH603993': 0.013820193926861863, 'SZ000338': 0.002455991798001457, 'SZ000423': 0.004893338273543826, 'SZ000538': 0.010686211189620477, 'SZ002065': 0.09095125419435357, 'SZ002074': 0.010299013738522475, 'SZ002085': 0.19844965949420615, 'SZ002236': 0.09210003831704765, 'SZ002310': 0.05664352912360013, 'SZ300017': 0.0197442255539771}\n", - "target position: {'SZ002299': 6184584.0980107365, 'SH600000': 272224, 'SH600009': 604839, 'SH600018': 3097398, 'SH600028': 335726, 'SH600196': 23243, 'SH600276': 71634, 'SH600519': 17354, 'SH600585': 269686, 'SH600900': 2501521, 'SH601111': 2400659, 'SH601800': 334062, 'SH601939': 1283164, 'SH603993': 742901, 'SZ000338': 95285, 'SZ000423': 21697, 'SZ000538': 14518, 'SZ002065': 498253, 'SZ002074': 111674, 'SZ002085': 591507, 'SZ002236': 394197, 'SZ002310': 2202674, 'SZ300017': 206128}\n", - "target weight: {'SH600000': 0.02310668460556249, 'SH600009': 0.06170206213753432, 'SH600018': 0.027608180837257277, 'SH600028': 0.00971532319525714, 'SH600196': 0.0036133308423111116, 'SH600276': 0.093195014492093, 'SH600519': 0.013476706174774766, 'SH600585': 0.036024919027310476, 'SH600660': 0.04512159672692613, 'SH600900': 0.12506534473579556, 'SH601939': 0.013494851810297546, 'SH603993': 0.07619418669734077, 'SZ000338': 0.0024673392047414363, 'SZ000423': 0.00485981529404862, 'SZ000538': 0.010602880875660015, 'SZ002065': 0.09064325205359221, 'SZ002074': 0.0011889996597580427, 'SZ002085': 0.1982091371262038, 'SZ002236': 0.09254320484936242, 'SZ002310': 0.05152917909181458, 'SZ002466': 0.00014732765084648903, 'SZ300017': 0.019490662910321074}\n", - "target position: {'SZ002299': 6184584.0980107365, 'SH600000': 272079, 'SH600009': 604359, 'SH600018': 3095205, 'SH600028': 335471, 'SH600196': 23407, 'SH600276': 71567, 'SH600519': 17345, 'SH600585': 269447, 'SH600660': 129265, 'SH600900': 2499305, 'SH601939': 1282317, 'SH603993': 4058172, 'SZ000338': 95223, 'SZ000423': 21703, 'SZ000538': 14509, 'SZ002065': 497821, 'SZ002074': 12787, 'SZ002085': 590955, 'SZ002236': 393895, 'SZ002310': 2190685, 'SZ002466': 4483, 'SZ300017': 205994}\n", - "target weight: {'SH600000': 0.0014042138463464568, 'SH600009': 0.11511740651805806, 'SH600018': 0.026968513725965638, 'SH600028': 0.009566603496832042, 'SH600150': 0.016339328084607228, 'SH600276': 0.09374974543357856, 'SH600489': 0.021876512936684123, 'SH600585': 0.035840818294258524, 'SH600900': 0.12414161958870683, 'SH601888': 0.005682635273269834, 'SH601939': 0.013289788356428228, 'SH603993': 0.07491407610535435, 'SZ000338': 0.002426716760042838, 'SZ000423': 0.00492071038737461, 'SZ000503': 0.005617017904986693, 'SZ000538': 0.010859006699485451, 'SZ002065': 0.08924691553942904, 'SZ002085': 0.19757848255238786, 'SZ002236': 0.09381012783787722, 'SZ002310': 0.03737359938389514, 'SZ300017': 0.01927616131502695}\n", - "target position: {'SZ002299': 6184584.0980107365, 'SH600000': 16809, 'SH600009': 1075516, 'SH600018': 3091248, 'SH600028': 335128, 'SH600150': 114804, 'SH600276': 71473, 'SH600489': 66586, 'SH600585': 268644, 'SH600900': 2496175, 'SH601888': 173824, 'SH601939': 1281108, 'SH603993': 4052802, 'SZ000338': 95107, 'SZ000423': 21684, 'SZ000503': 80461, 'SZ000538': 14507, 'SZ002065': 497197, 'SZ002085': 590211, 'SZ002236': 393412, 'SZ002310': 1573728, 'SZ300017': 205818}\n", - "target weight: {'SH600000': 0.0013962189421662084, 'SH600009': 0.09330267135244051, 'SH600018': 0.026443154116291615, 'SH600028': 0.009581412428525829, 'SH600150': 0.016443917649559808, 'SH600276': 0.09378402212481758, 'SH600703': 0.0005233118350013756, 'SH600741': 0.10117549074044105, 'SH600900': 0.12435147566444608, 'SH601888': 0.00560250787284307, 'SH601939': 0.013238798853730008, 'SH603993': 0.07455231781733267, 'SZ000423': 0.0048695925705555185, 'SZ000503': 0.006070996956328167, 'SZ000538': 0.010870567565742796, 'SZ002065': 0.08722983720892508, 'SZ002074': 0.00037126948590009574, 'SZ002085': 0.19840484837030906, 'SZ002236': 0.09365186287123867, 'SZ002310': 0.03806080531862309, 'SZ300017': 7.492025186876957e-05}\n", - "target position: {'SZ002299': 6184584.0980107365, 'SH600000': 16889, 'SH600009': 867443, 'SH600018': 3086467, 'SH600028': 334573, 'SH600150': 114383, 'SH600276': 71360, 'SH600703': 1760, 'SH600741': 665366, 'SH600900': 2491839, 'SH601888': 173465, 'SH601939': 1278590, 'SH603993': 4045939, 'SZ000423': 21674, 'SZ000503': 80212, 'SZ000538': 14499, 'SZ002065': 496361, 'SZ002074': 4086, 'SZ002085': 589224, 'SZ002236': 392766, 'SZ002310': 1571463, 'SZ300017': 805}\n", - "target weight: {'SH600000': 0.0014143911110003147, 'SH600018': 0.026834186435965166, 'SH600028': 0.00961324990522086, 'SH600150': 0.015905361405158292, 'SH600276': 0.09486308638260738, 'SH600685': 1.0253334545374858e-06, 'SH600703': 0.0005108576602907958, 'SH600741': 0.10252334336233063, 'SH600900': 0.1250632059809011, 'SH601888': 0.005830869532670813, 'SH601939': 0.01336945356138906, 'SH603993': 0.07101851124599835, 'SZ000423': 0.004899981502195361, 'SZ000503': 0.006113894785564276, 'SZ000538': 0.011081925761176491, 'SZ000709': 1.06442568357325e-06, 'SZ002065': 0.08812103684766726, 'SZ002074': 0.0003564773234700175, 'SZ002085': 0.19097427428977284, 'SZ002236': 0.09299395368630246, 'SZ002310': 0.03841630892378685, 'SZ002475': 0.10001934454071283, 'SZ300017': 7.322667303400442e-05}\n", - "target position: {'SZ002299': 6184584.0980107365, 'SH600000': 16886, 'SH600018': 3080789, 'SH600028': 334087, 'SH600150': 114360, 'SH600276': 71234, 'SH600685': 10, 'SH600703': 1709, 'SH600741': 663932, 'SH600900': 2486951, 'SH601888': 173417, 'SH601939': 1276335, 'SH603993': 3740672, 'SZ000423': 21667, 'SZ000503': 80191, 'SZ000538': 14495, 'SZ000709': 11, 'SZ002065': 495371, 'SZ002074': 3867, 'SZ002085': 588051, 'SZ002236': 392002, 'SZ002310': 1568834, 'SZ002475': 1264636, 'SZ300017': 809}\n", - "target weight: {'SH600000': 0.0013872765178790307, 'SH600018': 0.026321999857337998, 'SH600028': 0.009491029058787367, 'SH600150': 0.015749871987744815, 'SH600276': 0.09581999547114961, 'SH600703': 0.000518490273176083, 'SH600741': 0.1037547619508012, 'SH600900': 0.12396253436063161, 'SH601258': 0.02298494942988327, 'SH601888': 0.005915886046387033, 'SH601939': 0.013177336599075601, 'SH603993': 0.06888468621566025, 'SZ000423': 0.005102036718661418, 'SZ000503': 0.00602692511970311, 'SZ000538': 0.011127923667697532, 'SZ000709': 0.07688609680386178, 'SZ002065': 0.08693397271897534, 'SZ002074': 0.000347445594871718, 'SZ002085': 0.1905176824564206, 'SZ002236': 0.035835596544641496, 'SZ002475': 0.09918059167278087, 'SZ300017': 7.291118905149903e-05}\n", - "target position: {'SZ002299': 6184584.0980107365, 'SH600000': 16948, 'SH600018': 3086676, 'SH600028': 334750, 'SH600150': 114560, 'SH600276': 71372, 'SH600703': 1715, 'SH600741': 665129, 'SH600900': 2491433, 'SH601258': 4190669, 'SH601888': 174070, 'SH601939': 1278836, 'SH603993': 3747283, 'SZ000423': 21744, 'SZ000503': 80490, 'SZ000538': 14538, 'SZ000709': 871429, 'SZ002065': 496245, 'SZ002074': 3887, 'SZ002085': 589120, 'SZ002236': 145147, 'SZ002475': 1268582, 'SZ300017': 814}\n", - "target weight: {'SH600000': 0.001373124016867567, 'SH600018': 0.02646941123076474, 'SH600028': 0.009458335378810856, 'SH600150': 0.015442533996257352, 'SH600276': 0.09620341387657301, 'SH600649': 0.012613476480118908, 'SH600703': 0.0005280976985716832, 'SH600741': 0.06577156829314017, 'SH600900': 0.12455488881029539, 'SH601258': 0.02270943336842379, 'SH601939': 0.013066707696697587, 'SH603993': 0.0649427819283919, 'SZ000423': 0.0051167756388828005, 'SZ000503': 0.006076486564538039, 'SZ000709': 0.0770418453012855, 'SZ000778': 0.08738918304165759, 'SZ002065': 0.08804613990036694, 'SZ002074': 0.00034315924263262563, 'SZ002085': 0.18241434394629127, 'SZ002475': 0.10035998625624482, 'SZ300017': 7.809604376099223e-05}\n", - "target position: {'SZ002299': 6184584.0980107365, 'SH600000': 16935, 'SH600018': 3089469, 'SH600028': 334906, 'SH600150': 114496, 'SH600276': 71430, 'SH600649': 337388, 'SH600703': 1714, 'SH600741': 419916, 'SH600900': 2493978, 'SH601258': 4194599, 'SH601939': 1279661, 'SH603993': 3750968, 'SZ000423': 21734, 'SZ000503': 80440, 'SZ000709': 872293, 'SZ000778': 366855, 'SZ002065': 496756, 'SZ002074': 3880, 'SZ002085': 564610, 'SZ002475': 1269872, 'SZ300017': 812}\n", - "target weight: {'SH600000': 0.0013497287789570015, 'SH600018': 0.02647482761554837, 'SH600028': 0.00941080088689994, 'SH600150': 0.01556139303593115, 'SH600276': 0.09732218714743374, 'SH600649': 0.012606184789019243, 'SH600703': 0.0005334649726542859, 'SH600900': 0.12593267687041163, 'SH601258': 0.021199485570796834, 'SH601939': 0.013025993149697816, 'SH603993': 0.06446918682668012, 'SZ000423': 0.005311875734339093, 'SZ000503': 0.006125989728635501, 'SZ000709': 0.0707610058353687, 'SZ000778': 0.14004715956352495, 'SZ002065': 0.08746446321200681, 'SZ002074': 0.00033710686535540885, 'SZ002085': 0.15238971653801253, 'SZ002146': 0.042585776887618575, 'SZ002475': 0.10701429615740456, 'SZ300017': 7.667981013711115e-05}\n", - "target position: {'SZ002299': 6184584.0980107365, 'SH600000': 17031, 'SH600018': 3109084, 'SH600028': 336978, 'SH600150': 115126, 'SH600276': 71888, 'SH600649': 339316, 'SH600703': 1724, 'SH600900': 2510148, 'SH601258': 4237748, 'SH601939': 1287810, 'SH603993': 3775382, 'SZ000423': 21853, 'SZ000503': 80885, 'SZ000709': 878077, 'SZ000778': 625157, 'SZ002065': 499988, 'SZ002074': 3901, 'SZ002085': 469624, 'SZ002146': 2000993, 'SZ002475': 1278084, 'SZ300017': 814}\n", - "target weight: {'SH600000': 0.0013594926998639766, 'SH600009': 0.021101252574639438, 'SH600028': 0.009528554544265834, 'SH600150': 0.015013601602404225, 'SH600276': 0.09860402207319302, 'SH600649': 0.01292550325031454, 'SH600685': 0.00703471182662378, 'SH600703': 0.0005218767517596246, 'SH600900': 0.12786995199482584, 'SH601258': 0.04401496515184404, 'SH601398': 0.025932829520167643, 'SH601939': 0.0134408200189716, 'SH603993': 0.06319752369639879, 'SZ000423': 0.005221187626834546, 'SZ000503': 0.006085670359590286, 'SZ000568': 0.003081214755480397, 'SZ000709': 0.07061122716452324, 'SZ000778': 0.1379488795662632, 'SZ000839': 0.019142903464547063, 'SZ002065': 0.04714685528331623, 'SZ002074': 0.00033291622875151913, 'SZ002085': 0.11947661465752588, 'SZ002146': 0.043205942689553425, 'SZ002310': 0.0009243182551654129, 'SZ002475': 0.106199974013018, 'SZ300017': 7.709323254732814e-05}\n", - "target position: {'SZ002299': 6184584.0980107365, 'SH600000': 16933, 'SH600009': 196068, 'SH600028': 337025, 'SH600150': 115100, 'SH600276': 71926, 'SH600649': 339354, 'SH600685': 75328, 'SH600703': 1713, 'SH600900': 2511928, 'SH601258': 8791935, 'SH601398': 1146896, 'SH601939': 1288215, 'SH603993': 3777819, 'SZ000423': 21728, 'SZ000503': 80869, 'SZ000568': 10375, 'SZ000709': 878683, 'SZ000778': 625604, 'SZ000839': 312116, 'SZ002065': 268413, 'SZ002074': 3860, 'SZ002085': 369761, 'SZ002146': 2002072, 'SZ002310': 40341, 'SZ002475': 1278918, 'SZ300017': 811}\n", - "target weight: {'SH600000': 0.0013764694393366029, 'SH600009': 0.021541655860797534, 'SH600028': 0.009752609535237182, 'SH600276': 0.06514222178877259, 'SH600649': 0.01273168785031133, 'SH600685': 0.006989932070614982, 'SH600900': 0.12998548252109676, 'SH601258': 0.13157540821422453, 'SH601398': 0.02641881439805636, 'SH601939': 0.0136141957873422, 'SH603993': 0.0602411123337629, 'SZ000503': 0.006084251045333903, 'SZ000709': 0.06977363144499521, 'SZ000778': 0.1385461140272643, 'SZ000839': 0.018579865431307987, 'SZ002065': 0.046270476942690986, 'SZ002074': 0.00025974854597178115, 'SZ002085': 0.10060756172850334, 'SZ002146': 0.043204792194791966, 'SZ002310': 0.0009022784286642987, 'SZ002466': 0.011748866835406593, 'SZ002475': 0.08457581284822364, 'SZ300017': 7.701070501151889e-05}\n", - "target position: {'SZ002299': 6184584.0980107365, 'SH600000': 16938, 'SH600009': 196239, 'SH600028': 337355, 'SH600276': 46535, 'SH600649': 339479, 'SH600685': 75274, 'SH600900': 2514488, 'SH601258': 26730440, 'SH601398': 1148157, 'SH601939': 1289259, 'SH603993': 3781937, 'SZ000503': 80900, 'SZ000709': 879645, 'SZ000778': 626285, 'SZ000839': 312384, 'SZ002065': 268717, 'SZ002074': 3093, 'SZ002085': 309206, 'SZ002146': 2003901, 'SZ002310': 39782, 'SZ002466': 367691, 'SZ002475': 1026389, 'SZ300017': 812}\n", - "target weight: {'SH600000': 0.0013689894888766726, 'SH600009': 0.021087495457198752, 'SH600028': 0.009589419355091226, 'SH600276': 0.0644304399184473, 'SH600535': 0.016420787426513667, 'SH600649': 0.0267771761277641, 'SH600900': 0.12784455237901315, 'SH601169': 0.004374459372110214, 'SH601258': 0.13288651981531077, 'SH601398': 0.02615927477879055, 'SH601939': 0.013573361058977978, 'SH603993': 1.157895161672162e-06, 'SZ000503': 0.009069218941980683, 'SZ000709': 0.07014466816191627, 'SZ000778': 0.13956352821962528, 'SZ002065': 0.045206445945654664, 'SZ002085': 0.08649963592018277, 'SZ002146': 0.04234588186007612, 'SZ002310': 0.0008924777422846245, 'SZ002466': 0.07334842360184116, 'SZ002475': 0.08834296814868704, 'SZ300017': 7.311841306821287e-05}\n", - "Exception: ('SH601169', Timestamp('2017-04-25 00:00:00'))\n", - "target position: {'SZ002299': 6184584.0980107365, 'SH600000': 16929, 'SH600009': 196092, 'SH600028': 337333, 'SH600276': 46571, 'SH600535': 57649, 'SH600649': 731641, 'SH600900': 2515321, 'SH601258': 26740467, 'SH601398': 1148635, 'SH601939': 1289434, 'SH603993': 72, 'SZ000503': 122157, 'SZ000709': 879908, 'SZ000778': 626506, 'SZ002065': 268767, 'SZ002085': 267906, 'SZ002146': 2004576, 'SZ002310': 39745, 'SZ002466': 2332750, 'SZ002475': 1026858, 'SZ300017': 806}\n", - "target weight: {'SH600000': 0.0013439859873209908, 'SH600009': 0.02075652616964347, 'SH600028': 0.00939963933310415, 'SH600276': 0.06236017906066887, 'SH600535': 0.016369568294734148, 'SH600649': 0.025541724367766302, 'SH600900': 0.12768966131041845, 'SH601258': 0.1370446945486361, 'SH601398': 0.02601619218529119, 'SH601939': 0.013440958024818669, 'SH603993': 4.144559709761373e-06, 'SZ000503': 0.0084237188568659, 'SZ000568': 0.020576387679160105, 'SZ000709': 0.056783757531829446, 'SZ000778': 0.06920027928808208, 'SZ002008': 0.07943378393922318, 'SZ002065': 0.045339177613740886, 'SZ002085': 0.08505902525865962, 'SZ002146': 0.031624633954490035, 'SZ002310': 0.0008996156348854183, 'SZ002466': 0.0764983539831682, 'SZ002475': 0.086193992434369}\n", - "target position: {'SZ002299': 6184584.0980107365, 'SZ300017': 812.4573136217659, 'SH600000': 16923, 'SH600009': 196076, 'SH600028': 337279, 'SH600276': 46567, 'SH600535': 57624, 'SH600649': 731549, 'SH600900': 2515891, 'SH601258': 26747448, 'SH601398': 1148886, 'SH601939': 1289307, 'SH603993': 263, 'SZ000503': 122158, 'SZ000568': 69471, 'SZ000709': 700781, 'SZ000778': 302643, 'SZ002008': 746285, 'SZ002065': 268804, 'SZ002085': 267988, 'SZ002146': 1473970, 'SZ002310': 39739, 'SZ002466': 2333288, 'SZ002475': 1027134}\n", - "target weight: {'SH600000': 0.0014508867295425067, 'SH600009': 0.022137935734971876, 'SH600028': 0.01003980705499816, 'SH600276': 0.065554410760754, 'SH600535': 0.017337663954140436, 'SH600649': 0.026752732524884384, 'SH600900': 0.13610376526017787, 'SH601258': 0.14230666244775886, 'SH601398': 0.027847743092481312, 'SH601939': 0.014306563408357105, 'SH603993': 2.7770868647848817e-06, 'SZ000069': 0.10104502775773525, 'SZ000503': 0.009049444347506782, 'SZ000568': 0.005686495401232644, 'SZ000778': 0.0715782861850023, 'SZ002008': 0.08609584908472251, 'SZ002065': 0.04706561122827146, 'SZ002085': 0.09099179117275048, 'SZ002146': 0.03204301334262787, 'SZ002475': 0.09241758644387384, 'SZ300017': 0.00018594702102337797}\n", - "target position: {'SZ000709': 700825.0269758024, 'SZ002299': 6184584.0980107365, 'SH600000': 16845, 'SH600009': 195098, 'SH600028': 335689, 'SH600276': 46340, 'SH600535': 57343, 'SH600649': 728078, 'SH600900': 2504242, 'SH601258': 26624542, 'SH601398': 1143577, 'SH601939': 1283067, 'SH603993': 160, 'SZ000069': 367637, 'SZ000503': 121565, 'SZ000568': 17626, 'SZ000778': 301250, 'SZ002008': 742790, 'SZ002065': 267559, 'SZ002085': 266737, 'SZ002146': 1467579, 'SZ002475': 1022346, 'SZ300017': 1776}\n", - "target weight: {'SH600000': 0.0013484985106016394, 'SH600009': 0.020750773768622693, 'SH600028': 0.009285673867962157, 'SH600104': 2.9067007814076732e-05, 'SH600196': 0.10012804077099052, 'SH600276': 0.05943563439541343, 'SH600535': 0.015902136087846228, 'SH600649': 0.025189836387314323, 'SH600900': 0.12584805827140388, 'SH601111': 6.857382365314848e-06, 'SH601258': 0.03895938466363849, 'SH601398': 0.025753888553878806, 'SH601939': 0.013275755331575599, 'SH603993': 4.249178615404585e-06, 'SZ000069': 0.09445579375504781, 'SZ000503': 0.008532747266799033, 'SZ000568': 0.0052599046052527266, 'SZ000709': 0.06003418476540357, 'SZ000778': 0.06923031488245988, 'SZ002008': 0.07903025205993618, 'SZ002065': 0.04448484691775433, 'SZ002085': 0.08426354045447453, 'SZ002146': 0.031142767130486235, 'SZ002475': 0.08747938111190227, 'SZ300017': 0.00016841662419817417}\n", - "target position: {'SZ002299': 6184584.0980107365, 'SH600000': 16906, 'SH600009': 195107, 'SH600028': 335257, 'SH600104': 197, 'SH600196': 630404, 'SH600276': 46282, 'SH600535': 57311, 'SH600649': 727170, 'SH600900': 2500379, 'SH601111': 203, 'SH601258': 7443096, 'SH601398': 1142014, 'SH601939': 1281361, 'SH603993': 263, 'SZ000069': 366998, 'SZ000503': 121479, 'SZ000568': 17699, 'SZ000709': 699639, 'SZ000778': 300752, 'SZ002008': 741767, 'SZ002065': 267133, 'SZ002085': 266334, 'SZ002146': 1465489, 'SZ002475': 1020693, 'SZ300017': 1756}\n", - "target weight: {'SH600000': 0.0012976336004362882, 'SH600009': 0.0204756895024156, 'SH600028': 0.008883617000656601, 'SH600104': 2.592943319382378e-05, 'SH600196': 0.09617041827497698, 'SH600276': 0.05681162545715886, 'SH600535': 0.015294256733040745, 'SH600649': 0.02417676167926707, 'SH600900': 0.12233373885315162, 'SH601398': 0.024531954099214746, 'SH601628': 0.005044154324745466, 'SH601888': 0.09500034426651846, 'SH601939': 0.012657033879067425, 'SH603993': 4.079522960136806e-06, 'SZ000069': 0.09054142453059062, 'SZ000503': 0.008036587259744734, 'SZ000568': 0.0049533657881637655, 'SZ000778': 0.06904486736535222, 'SZ002008': 0.06688985213943154, 'SZ002065': 0.04278977877238287, 'SZ002085': 0.0820368284038888, 'SZ002299': 0.06899317887598991, 'SZ002475': 0.08384652594205952, 'SZ300017': 0.00016035416530955983}\n", - "target position: {'SH601258': 7443495.190430395, 'SH600000': 16952, 'SH600009': 195676, 'SH600028': 336044, 'SH600104': 183, 'SH600196': 631454, 'SH600276': 46372, 'SH600535': 57498, 'SH600649': 728582, 'SH600900': 2504660, 'SH601398': 1143938, 'SH601628': 695470, 'SH601888': 2951253, 'SH601939': 1283887, 'SH603993': 255, 'SZ000069': 367641, 'SZ000503': 121875, 'SZ000568': 17775, 'SZ000778': 301255, 'SZ002008': 638620, 'SZ002065': 267645, 'SZ002085': 266802, 'SZ002299': 6194843, 'SZ002475': 1022527, 'SZ300017': 1765}\n", - "target weight: {'SH600000': 0.0013469483722729403, 'SH600028': 0.009286467498269333, 'SH600104': 2.368500734977497e-05, 'SH600196': 0.10145424564201923, 'SH600276': 0.06002237364700993, 'SH600535': 0.01588332650422844, 'SH600649': 0.025440421851940002, 'SH600900': 0.1279028471227695, 'SH601258': 0.035917606048396986, 'SH601398': 0.02559318344055778, 'SH601628': 0.005221942888216608, 'SH601888': 0.14928498761757883, 'SH601939': 0.013161430940131148, 'SH603993': 4.350147095904942e-06, 'SZ000069': 0.14038473724819095, 'SZ000503': 0.008556251357999256, 'SZ000568': 0.005243511514392524, 'SZ002008': 0.06824325050397591, 'SZ002065': 0.04420632869308568, 'SZ002085': 0.074424247013131, 'SZ002299': 0.0010812901181988855, 'SZ002475': 0.0871460668952185, 'SZ300017': 0.00017049992832446128}\n", - "target position: {'SZ000778': 301254.84776855103, 'SH600000': 16873, 'SH600028': 335064, 'SH600104': 156, 'SH600196': 629613, 'SH600276': 46235, 'SH600535': 57245, 'SH600649': 726346, 'SH600900': 2497776, 'SH601258': 7423462, 'SH601398': 1140689, 'SH601628': 692346, 'SH601888': 4557826, 'SH601939': 1279908, 'SH603993': 261, 'SZ000069': 551887, 'SZ000503': 121344, 'SZ000568': 17697, 'SZ002008': 636943, 'SZ002065': 266904, 'SZ002085': 231781, 'SZ002299': 97527, 'SZ002475': 1019747, 'SZ300017': 1749}\n" - ] - }, - { - "output_type": "error", - "ename": "KeyboardInterrupt", - "evalue": "", - "traceback": [ - "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m", - "\u001b[1;31mKeyboardInterrupt\u001b[0m Traceback (most recent call last)", - "\u001b[1;32m\u001b[0m in \u001b[0;36m\u001b[1;34m\u001b[0m\n\u001b[0;32m 30\u001b[0m \u001b[1;31m# backtest & analysis\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 31\u001b[0m \u001b[0mpar\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mPortAnaRecord\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mrecorder\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mport_analysis_config\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m---> 32\u001b[1;33m \u001b[0mpar\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mgenerate\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m", - "\u001b[1;32md:\\qlib\\qlib\\workflow\\record_temp.py\u001b[0m in \u001b[0;36mgenerate\u001b[1;34m(self, **kwargs)\u001b[0m\n\u001b[0;32m 230\u001b[0m \u001b[1;31m# custom strategy and get backtest\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 231\u001b[0m \u001b[0mpred_score\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0msuper\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mload\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m--> 232\u001b[1;33m \u001b[0mreport_normal\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mpositions_normal\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mnormal_backtest\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mpred_score\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mstrategy\u001b[0m\u001b[1;33m=\u001b[0m\u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mstrategy\u001b[0m\u001b[1;33m,\u001b[0m \u001b[1;33m**\u001b[0m\u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mbacktest_config\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m 233\u001b[0m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mrecorder\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0msave_objects\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;33m**\u001b[0m\u001b[1;33m{\u001b[0m\u001b[1;34m\"report_normal.pkl\"\u001b[0m\u001b[1;33m:\u001b[0m \u001b[0mreport_normal\u001b[0m\u001b[1;33m}\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0martifact_path\u001b[0m\u001b[1;33m=\u001b[0m\u001b[0mPortAnaRecord\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mget_path\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 234\u001b[0m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mrecorder\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0msave_objects\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;33m**\u001b[0m\u001b[1;33m{\u001b[0m\u001b[1;34m\"positions_normal.pkl\"\u001b[0m\u001b[1;33m:\u001b[0m \u001b[0mpositions_normal\u001b[0m\u001b[1;33m}\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0martifact_path\u001b[0m\u001b[1;33m=\u001b[0m\u001b[0mPortAnaRecord\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mget_path\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n", - "\u001b[1;32md:\\qlib\\qlib\\contrib\\evaluate.py\u001b[0m in \u001b[0;36mbacktest\u001b[1;34m(pred, account, shift, benchmark, verbose, **kwargs)\u001b[0m\n\u001b[0;32m 269\u001b[0m \u001b[0mverbose\u001b[0m\u001b[1;33m=\u001b[0m\u001b[0mverbose\u001b[0m\u001b[1;33m,\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 270\u001b[0m \u001b[0maccount\u001b[0m\u001b[1;33m=\u001b[0m\u001b[0maccount\u001b[0m\u001b[1;33m,\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m--> 271\u001b[1;33m \u001b[0mbenchmark\u001b[0m\u001b[1;33m=\u001b[0m\u001b[0mbenchmark\u001b[0m\u001b[1;33m,\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m 272\u001b[0m )\n\u001b[0;32m 273\u001b[0m \u001b[1;31m# for compatibility of the old API. return the dict positions\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n", - "\u001b[1;32md:\\qlib\\qlib\\contrib\\backtest\\backtest.py\u001b[0m in \u001b[0;36mbacktest\u001b[1;34m(pred, strategy, trade_exchange, shift, verbose, account, benchmark)\u001b[0m\n\u001b[0;32m 100\u001b[0m \u001b[0mtrade_exchange\u001b[0m\u001b[1;33m=\u001b[0m\u001b[0mtrade_exchange\u001b[0m\u001b[1;33m,\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 101\u001b[0m \u001b[0mpred_date\u001b[0m\u001b[1;33m=\u001b[0m\u001b[0mpred_date\u001b[0m\u001b[1;33m,\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m--> 102\u001b[1;33m \u001b[0mtrade_date\u001b[0m\u001b[1;33m=\u001b[0m\u001b[0mtrade_date\u001b[0m\u001b[1;33m,\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m 103\u001b[0m )\n\u001b[0;32m 104\u001b[0m \u001b[1;32melse\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n", - "\u001b[1;32m\u001b[0m in \u001b[0;36mgenerate_order_list\u001b[1;34m(self, score_series, current, trade_exchange, pred_date, trade_date)\u001b[0m\n\u001b[0;32m 76\u001b[0m \u001b[1;31m# optimize target portfolio\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 77\u001b[0m \u001b[1;32mif\u001b[0m \u001b[0minit_weight\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0msum\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;33m)\u001b[0m \u001b[1;33m>\u001b[0m \u001b[1;36m0\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m---> 78\u001b[1;33m \u001b[0mtarget_weight\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0moptimizer\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mcov\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mscore_series\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0minit_weight\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m 79\u001b[0m \u001b[1;32melse\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 80\u001b[0m \u001b[0mtarget_weight\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0moptimizer\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mcov\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mscore_series\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n", - "\u001b[1;32md:\\qlib\\qlib\\portfolio\\optimizer.py\u001b[0m in \u001b[0;36m__call__\u001b[1;34m(self, S, u, w0)\u001b[0m\n\u001b[0;32m 100\u001b[0m \u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 101\u001b[0m \u001b[1;31m# optimize\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m--> 102\u001b[1;33m \u001b[0mw\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0m_optimize\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mS\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mu\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mw0\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m 103\u001b[0m \u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 104\u001b[0m \u001b[1;31m# restore index if needed\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n", - "\u001b[1;32md:\\qlib\\qlib\\portfolio\\optimizer.py\u001b[0m in \u001b[0;36m_optimize\u001b[1;34m(self, S, u, w0)\u001b[0m\n\u001b[0;32m 126\u001b[0m \u001b[1;31m# mean-variance\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 127\u001b[0m \u001b[1;32mif\u001b[0m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mmethod\u001b[0m \u001b[1;33m==\u001b[0m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mOPT_MVO\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m--> 128\u001b[1;33m \u001b[1;32mreturn\u001b[0m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0m_optimize_mvo\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mS\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mu\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mw0\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m 129\u001b[0m \u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 130\u001b[0m \u001b[1;31m# risk parity\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n", - "\u001b[1;32md:\\qlib\\qlib\\portfolio\\optimizer.py\u001b[0m in \u001b[0;36m_optimize_mvo\u001b[1;34m(self, S, u, w0)\u001b[0m\n\u001b[0;32m 162\u001b[0m \u001b[1;32mand\u001b[0m\u001b[0;31m \u001b[0m\u001b[0;31m`\u001b[0m\u001b[0mlamb\u001b[0m\u001b[0;31m`\u001b[0m \u001b[1;32mis\u001b[0m \u001b[0mthe\u001b[0m \u001b[0mrisk\u001b[0m \u001b[0maversion\u001b[0m \u001b[0mparameter\u001b[0m\u001b[1;33m.\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 163\u001b[0m \"\"\"\n\u001b[1;32m--> 164\u001b[1;33m \u001b[1;32mreturn\u001b[0m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0m_solve\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mlen\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mS\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0m_get_objective_mvo\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mS\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mu\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m,\u001b[0m \u001b[1;33m*\u001b[0m\u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0m_get_constrains\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mw0\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m 165\u001b[0m \u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 166\u001b[0m \u001b[1;32mdef\u001b[0m \u001b[0m_optimize_rp\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mself\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mS\u001b[0m\u001b[1;33m:\u001b[0m \u001b[0mnp\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mndarray\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mw0\u001b[0m\u001b[1;33m:\u001b[0m \u001b[0mOptional\u001b[0m\u001b[1;33m[\u001b[0m\u001b[0mnp\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mndarray\u001b[0m\u001b[1;33m]\u001b[0m \u001b[1;33m=\u001b[0m \u001b[1;32mNone\u001b[0m\u001b[1;33m)\u001b[0m \u001b[1;33m->\u001b[0m \u001b[0mnp\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mndarray\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n", - "\u001b[1;32md:\\qlib\\qlib\\portfolio\\optimizer.py\u001b[0m in \u001b[0;36m_solve\u001b[1;34m(self, n, obj, bounds, cons)\u001b[0m\n\u001b[0;32m 252\u001b[0m \u001b[1;31m# solve\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 253\u001b[0m \u001b[0mx0\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mnp\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mones\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mn\u001b[0m\u001b[1;33m)\u001b[0m \u001b[1;33m/\u001b[0m \u001b[0mn\u001b[0m \u001b[1;31m# init results\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m--> 254\u001b[1;33m \u001b[0msol\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mso\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mminimize\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mwrapped_obj\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mx0\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mbounds\u001b[0m\u001b[1;33m=\u001b[0m\u001b[0mbounds\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mconstraints\u001b[0m\u001b[1;33m=\u001b[0m\u001b[0mcons\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mtol\u001b[0m\u001b[1;33m=\u001b[0m\u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mtol\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m 255\u001b[0m \u001b[1;32mif\u001b[0m \u001b[1;32mnot\u001b[0m \u001b[0msol\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0msuccess\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 256\u001b[0m \u001b[0mwarnings\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mwarn\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;34mf\"optimization not success ({sol.status})\"\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n", - "\u001b[1;32m~\\AppData\\Local\\Continuum\\miniconda3\\envs\\qlib\\lib\\site-packages\\scipy\\optimize\\_minimize.py\u001b[0m in \u001b[0;36mminimize\u001b[1;34m(fun, x0, args, method, jac, hess, hessp, bounds, constraints, tol, callback, options)\u001b[0m\n\u001b[0;32m 624\u001b[0m \u001b[1;32melif\u001b[0m \u001b[0mmeth\u001b[0m \u001b[1;33m==\u001b[0m \u001b[1;34m'slsqp'\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 625\u001b[0m return _minimize_slsqp(fun, x0, args, jac, bounds,\n\u001b[1;32m--> 626\u001b[1;33m constraints, callback=callback, **options)\n\u001b[0m\u001b[0;32m 627\u001b[0m \u001b[1;32melif\u001b[0m \u001b[0mmeth\u001b[0m \u001b[1;33m==\u001b[0m \u001b[1;34m'trust-constr'\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 628\u001b[0m return _minimize_trustregion_constr(fun, x0, args, jac, hess, hessp,\n", - "\u001b[1;32m~\\AppData\\Local\\Continuum\\miniconda3\\envs\\qlib\\lib\\site-packages\\scipy\\optimize\\slsqp.py\u001b[0m in \u001b[0;36m_minimize_slsqp\u001b[1;34m(func, x0, args, jac, bounds, constraints, maxiter, ftol, iprint, disp, eps, callback, finite_diff_rel_step, **unknown_options)\u001b[0m\n\u001b[0;32m 419\u001b[0m n1, n2, n3)\n\u001b[0;32m 420\u001b[0m \u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m--> 421\u001b[1;33m \u001b[1;32mif\u001b[0m \u001b[0mmode\u001b[0m \u001b[1;33m==\u001b[0m \u001b[1;36m1\u001b[0m\u001b[1;33m:\u001b[0m \u001b[1;31m# objective and constraint evaluation required\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m 422\u001b[0m \u001b[0mfx\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0msf\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mfun\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mx\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 423\u001b[0m \u001b[0mc\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0m_eval_constraint\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mx\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mcons\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n", - "\u001b[1;31mKeyboardInterrupt\u001b[0m: " + "[36502:MainThread](2020-11-27 16:27:43,761) INFO - qlib.workflow - [exp.py:180] - Experiment 3 starts running ...\n", + "[36502:MainThread](2020-11-27 16:27:43,779) INFO - qlib.workflow - [recorder.py:234] - Recorder 67d105113f424259889fc0b6b0b94973 starts running under Experiment 3 ...\n", + "[36502:MainThread](2020-11-27 16:27:43,780) INFO - qlib.workflow - [expm.py:251] - No tracking URI is provided. The default tracking URI is set as `mlruns` under the working directory.\n", + "[36502:MainThread](2020-11-27 16:27:43,991) INFO - qlib.workflow - [record_temp.py:127] - Signal record 'pred.pkl' has been saved as the artifact of the Experiment 3\n", + "[36502:MainThread](2020-11-27 16:27:44,050) INFO - qlib.Evaluate - [evaluate.py:161] - Create new exchange\n", + "'The following are prediction results of the LGBModel model.'\n", + " score\n", + "datetime instrument \n", + "2017-01-03 SH600000 -0.053414\n", + " SH600008 0.001820\n", + " SH600009 0.023472\n", + " SH600010 -0.005625\n", + " SH600015 -0.137476\n", + "/home/dongzho/miniconda3/lib/python3.7/site-packages/ipykernel_launcher.py:55: DeprecationWarning: The default dtype for empty Series will be 'object' instead of 'float64' in a future version. Specify a dtype explicitly to silence this warning.\n", + "/home/dongzho/qlib/qlib/portfolio/optimizer.py:256: UserWarning: optimization not success (9)\n", + " warnings.warn(f\"optimization not success ({sol.status})\")\n", + "Unknown exception: 2017-01-16 00:00:00 ('SZ300104', Timestamp('2017-01-13 00:00:00'))\n", + "Unknown exception: 2017-01-23 00:00:00 ('SZ000671', Timestamp('2017-01-20 00:00:00'))\n", + "Unknown exception: 2017-03-03 00:00:00 ('SZ002465', Timestamp('2017-03-02 00:00:00'))\n", + "Unknown exception: 2017-03-07 00:00:00 ('SH601127', Timestamp('2017-03-06 00:00:00'))\n", + "/home/dongzho/qlib/qlib/portfolio/optimizer.py:256: UserWarning: optimization not success (4)\n", + " warnings.warn(f\"optimization not success ({sol.status})\")\n", + "Unknown exception: 2017-05-08 00:00:00 ('SH601727', Timestamp('2017-05-05 00:00:00'))\n", + "Unknown exception: 2017-06-20 00:00:00 ('SH600036', Timestamp('2017-06-19 00:00:00'))\n", + "Unknown exception: 2017-06-21 00:00:00 ('SH600739', Timestamp('2017-06-20 00:00:00'))\n", + "Unknown exception: 2017-06-29 00:00:00 ('SZ300168', Timestamp('2017-06-28 00:00:00'))\n", + "Unknown exception: 2017-09-01 00:00:00 ('SH601088', Timestamp('2017-08-31 00:00:00'))\n", + "Unknown exception: 2017-09-12 00:00:00 ('SH601872', Timestamp('2017-09-11 00:00:00'))\n", + "Unknown exception: 2017-09-21 00:00:00 ('SH600100', Timestamp('2017-09-20 00:00:00'))\n", + "Unknown exception: 2017-09-22 00:00:00 ('SH600021', Timestamp('2017-09-21 00:00:00'))\n", + "Unknown exception: 2017-10-11 00:00:00 ('SH600959', Timestamp('2017-10-10 00:00:00'))\n", + "Unknown exception: 2017-10-25 00:00:00 ('SZ000792', Timestamp('2017-10-24 00:00:00'))\n", + "Unknown exception: 2017-12-26 00:00:00 ('SH600682', Timestamp('2017-12-25 00:00:00'))\n", + "[36502:MainThread](2020-11-27 17:28:14,269) INFO - qlib.workflow - [record_temp.py:249] - Portfolio analysis record 'port_analysis.pkl' has been saved as the artifact of the Experiment 3\n", + "'The following are analysis results of the excess return without cost.'\n", + " risk\n", + "mean 0.001247\n", + "std 0.005437\n", + "annualized_return 0.314237\n", + "information_ratio 3.640637\n", + "max_drawdown -0.033416\n", + "'The following are analysis results of the excess return with cost.'\n", + " risk\n", + "mean 0.001028\n", + "std 0.005432\n", + "annualized_return 0.259041\n", + "information_ratio 3.003970\n", + "max_drawdown -0.041455\n" ] } ], From 2311af5e47b44d17f46326c170e1a8b71bbe6214 Mon Sep 17 00:00:00 2001 From: Jactus Date: Fri, 27 Nov 2020 19:46:52 +0800 Subject: [PATCH 205/241] Update script --- README.md | 37 ++--- docs/start/initialization.rst | 2 +- examples/run_all_model.py | 280 +++++++++++++--------------------- requirements.txt | 3 +- 4 files changed, 127 insertions(+), 195 deletions(-) diff --git a/README.md b/README.md index c890afaca..dc9df109b 100644 --- a/README.md +++ b/README.md @@ -192,24 +192,6 @@ The automatic workflow may not suite the research workflow of all Quant research # [Quant Model Zoo](examples/benchmarks) -## Run a single model -`Qlib` provides three different ways to run a single model, users can pick the one that fits their cases best: -- User can use the tool `qrun` mentioned above to run a model's workflow based from a config file. -- User can create a `workflow_by_code` python script based on the [one](examples/workflow_by_code.py) listed in the `examples` folder. -- User can use the script [`run_all_model.py`](examples/run_all_model.py) listed in the `examples` folder to run a model. Here is an example of the specific shell command to be used: `python run_all_model.py --models=lightgbm`. For more use cases, please refer to the file's [docstrings](examples/run_all_model.py). - -## Run multiple models -`Qlib` also provides a script [`run_all_model.py`](examples/run_all_model.py) which can run multiple models for several iterations. (**Note**: the script only supprots *Linux* now. Other OS will be supported in the future.) - -The script will create a unique virtual environment for each model, and delete the environments after training. Thus, only experiment results such as `IC` and `backtest` results will be generated and stored. - -Here is an example of running all the models for 10 iterations: -```python -python run_all_model.py 10 -``` - -It also provides the API to run specific models at once. For more use cases, please refer to the file's [docstrings](examples/run_all_model.py). - Here is a list of models built on `Qlib`. - [GBDT based on LightGBM](qlib/contrib/model/gbdt.py) - [GBDT based on Catboost](qlib/contrib/model/catboost_model.py) @@ -226,6 +208,25 @@ Here is a list of models built on `Qlib`. Your PR of new Quant models is highly welcomed. +## Run a single model +`Qlib` provides three different ways to run a single model, users can pick the one that fits their cases best: +- User can use the tool `qrun` mentioned above to run a model's workflow based from a config file. +- User can create a `workflow_by_code` python script based on the [one](examples/workflow_by_code.py) listed in the `examples` folder. +- User can use the script [`run_all_model.py`](examples/run_all_model.py) listed in the `examples` folder to run a model. Here is an example of the specific shell command to be used: `python run_all_model.py --models=lightgbm`. For more use cases, please refer to the file's [docstrings](examples/run_all_model.py). + +## Run multiple models +`Qlib` also provides a script [`run_all_model.py`](examples/run_all_model.py) which can run multiple models for several iterations. (**Note**: the script only supprots *Linux* now. Other OS will be supported in the future.) + +The script will create a unique virtual environment for each model, and delete the environments after training. Thus, only experiment results such as `IC` and `backtest` results will be generated and stored. (**Note**: the script will erase your previous experiment records created by running itself.) + +Here is an example of running all the models for 10 iterations: +```python +python run_all_model.py 10 +``` + +It also provides the API to run specific models at once. For more use cases, please refer to the file's [docstrings](examples/run_all_model.py). + + # Quant Dataset Zoo Dataset plays a very important role in Quant. Here is a list of the datasets built on `Qlib`. - [Alpha360](./qlib/contrib/data/handler.py) diff --git a/docs/start/initialization.rst b/docs/start/initialization.rst index 423d7edf8..5615556b6 100644 --- a/docs/start/initialization.rst +++ b/docs/start/initialization.rst @@ -69,7 +69,7 @@ Besides `provider_uri` and `region`, `qlib.init` has other parameters. The follo "class": "MLflowExpManager", "module_path": "qlib.workflow.expm", "kwargs": { - "uri": "python_execution_path/mlruns"), + "uri": "python_execution_path/mlruns", "default_exp_name": "Experiment", } } \ No newline at end of file diff --git a/examples/run_all_model.py b/examples/run_all_model.py index 2f6c4299e..c02077b32 100644 --- a/examples/run_all_model.py +++ b/examples/run_all_model.py @@ -4,18 +4,20 @@ import os import sys import fire +import time import venv import glob import shutil +import signal +import inspect import tempfile +import traceback +import functools import statistics +import subprocess from pathlib import Path from operator import xor -from subprocess import Popen, PIPE -from threading import Thread from pprint import pprint -from urllib.parse import urlparse -from urllib.request import urlretrieve import qlib from qlib.config import REG_CN @@ -23,144 +25,50 @@ from qlib.workflow import R from qlib.workflow.cli import workflow from qlib.utils import exists_qlib_data + # init qlib provider_uri = "~/.qlib/qlib_data/cn_data" +exp_manager = { + "class": "MLflowExpManager", + "module_path": "qlib.workflow.expm", + "kwargs": { + "uri": "file:" + str(Path(os.getcwd()).resolve() / "run_all_model_records"), + "default_exp_name": "Experiment", + }, +} 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) +qlib.init(provider_uri=provider_uri, region=REG_CN, exp_manager=exp_manager) +shutil.rmtree(str(Path(os.getcwd()).resolve() / "run_all_model_records")) + +# decorator to check the arguments +def only_allow_defined_args(function_to_decorate): + @functools.wraps(function_to_decorate) + def _return_wrapped(*args, **kwargs): + """Internal wrapper function.""" + argspec = inspect.getfullargspec(function_to_decorate) + valid_names = set(argspec.args + argspec.kwonlyargs) + if "self" in valid_names: + valid_names.remove("self") + for arg_name in kwargs: + if arg_name not in valid_names: + raise ValueError("Unknown argument seen '%s', expected: [%s]" % (arg_name, ", ".join(valid_names))) + return function_to_decorate(*args, **kwargs) + + return _return_wrapped -class ExtendedEnvBuilder(venv.EnvBuilder): - """ - Thie class is modified based on https://docs.python.org/3/library/venv.html. - This builder installs setuptools and pip so that you can pip or - easy_install other packages into the created virtual environment. +# function to handle ctrl z and ctrl c +def handler(signum, frame): + os.system("kill -9 %d" % os.getpid()) - :param nodist: If true, setuptools and pip are not installed into the - created virtual environment. - :param nopip: If true, pip is not installed into the created - virtual environment. - :param progress: If setuptools or pip are installed, the progress of the - installation can be monitored by passing a progress - callable. If specified, it is called with two - arguments: a string indicating some progress, and a - context indicating where the string is coming from. - The context argument can have one of three values: - 'main', indicating that it is called from virtualize() - itself, and 'stdout' and 'stderr', which are obtained - by reading lines from the output streams of a subprocess - which is used to install the app. - - If a callable is not specified, default progress - information is output to sys.stderr. - """ - - def __init__(self, *args, **kwargs): - self.nodist = kwargs.pop("nodist", False) - self.nopip = kwargs.pop("nopip", False) - self.progress = kwargs.pop("progress", None) - self.verbose = kwargs.pop("verbose", False) - super().__init__(*args, **kwargs) - - def post_setup(self, context): - """ - Set up any packages which need to be pre-installed into the - virtual environment being created. - - :param context: The information for the virtual environment - creation request being processed. - """ - os.environ["VIRTUAL_ENV"] = context.env_dir - if not self.nodist: - self.install_setuptools(context) - # Can't install pip without setuptools - if not self.nopip and not self.nodist: - self.install_pip(context) - - def reader(self, stream, context): - """ - Read lines from a subprocess' output stream and either pass to a progress - callable (if specified) or write progress information to sys.stderr. - """ - progress = self.progress - while True: - s = stream.readline() - if not s: - break - if progress is not None: - progress(s, context) - else: - if not self.verbose: - sys.stderr.write(".") - else: - sys.stderr.write(s.decode("utf-8")) - sys.stderr.flush() - stream.close() - - def install_script(self, context, name, url): - _, _, path, _, _, _ = urlparse(url) - fn = os.path.split(path)[-1] - binpath = context.bin_path - distpath = os.path.join(binpath, fn) - # Download script into the virtual environment's binaries folder - urlretrieve(url, distpath) - progress = self.progress - if self.verbose: - term = "\n" - else: - term = "" - if progress is not None: - progress("Installing %s ...%s" % (name, term), "main") - else: - sys.stderr.write("Installing %s ...%s" % (name, term)) - sys.stderr.flush() - # Install in the virtual environment - args = [context.env_exe, fn] - p = Popen(args, stdout=PIPE, stderr=PIPE, cwd=binpath) - t1 = Thread(target=self.reader, args=(p.stdout, "stdout")) - t1.start() - t2 = Thread(target=self.reader, args=(p.stderr, "stderr")) - t2.start() - p.wait() - t1.join() - t2.join() - if progress is not None: - progress("done.", "main") - else: - sys.stderr.write("done.\n") - # Clean up - no longer needed - os.unlink(distpath) - - def install_setuptools(self, context): - """ - Install setuptools in the virtual environment. - - :param context: The information for the virtual environment - creation request being processed. - """ - url = "https://bootstrap.pypa.io/ez_setup.py" - self.install_script(context, "setuptools", url) - # clear up the setuptools archive which gets downloaded - pred = lambda o: o.startswith("setuptools-") and o.endswith(".tar.gz") - files = filter(pred, os.listdir(context.bin_path)) - for f in files: - f = os.path.join(context.bin_path, f) - os.unlink(f) - - def install_pip(self, context): - """ - Install pip in the virtual environment. - - :param context: The information for the virtual environment - creation request being processed. - """ - url = "https://bootstrap.pypa.io/get-pip.py" - self.install_script(context, "pip", url) +signal.signal(signal.SIGTSTP, handler) +signal.signal(signal.SIGINT, handler) # function to calculate the mean and std of a list in the results dictionary def cal_mean_std(results) -> dict: @@ -174,6 +82,36 @@ def cal_mean_std(results) -> dict: return mean_std +# function to create the environment ofr an anaconda environment +def create_env(): + # create env + temp_dir = tempfile.mkdtemp() + env_path = Path(temp_dir).absolute() + sys.stderr.write(f"Creating Virtual Environment with path: {env_path}...\n") + execute(f"conda create --prefix {env_path} python=3.7 -y") + python_path = env_path / "bin" / "python" # TODO: FIX ME! + sys.stderr.write("\n") + # get anaconda activate path + conda_activate = Path(os.environ["CONDA_PREFIX"]) / "bin" / "activate" # TODO: FIX ME! + return env_path, python_path, conda_activate + + +# function to execute the cmd +def execute(cmd): + with subprocess.Popen(cmd, stdout=subprocess.PIPE, bufsize=1, universal_newlines=True, shell=True) as p: + for line in p.stdout: + sys.stdout.write(line.split("\b")[0]) + if "\b" in line: + sys.stdout.flush() + time.sleep(0.1) + sys.stdout.write("\b" * 10 + "\b".join(line.split("\b")[1:-1])) + + if p.returncode != 0: + return p.stderr + else: + return None + + # function to get all the folders benchmark folder def get_all_folders(models, exclude) -> dict: folders = dict() @@ -212,11 +150,12 @@ def get_all_results(folders) -> dict: result["information_ratio_with_cost"] = list() result["max_drawdown_with_cost"] = list() for recorder_id in recorders: - recorder = R.get_recorder(recorder_id=recorder_id, experiment_name=fn) - metrics = recorder.list_metrics() - result["annualized_return_with_cost"].append(metrics["excess_return_with_cost.annualized_return"]) - result["information_ratio_with_cost"].append(metrics["excess_return_with_cost.information_ratio"]) - result["max_drawdown_with_cost"].append(metrics["excess_return_with_cost.max_drawdown"]) + if recorders[recorder_id]["status"] == "FINISHED": + recorder = R.get_recorder(recorder_id=recorder_id, experiment_name=fn) + metrics = recorder.list_metrics() + result["annualized_return_with_cost"].append(metrics["excess_return_with_cost.annualized_return"]) + result["information_ratio_with_cost"].append(metrics["excess_return_with_cost.information_ratio"]) + result["max_drawdown_with_cost"].append(metrics["excess_return_with_cost.max_drawdown"]) results[fn] = result return results @@ -237,6 +176,7 @@ def gen_and_save_md_table(metrics): # function to run the all the models +@only_allow_defined_args def run(times=1, models=None, exclude=False): """ Please be aware that this function can only work under Linux. MacOS and Windows will be supported in the future. @@ -275,53 +215,46 @@ def run(times=1, models=None, exclude=False): """ # get all folders folders = get_all_folders(models, exclude) - # set up - compatible = True - if sys.version_info < (3, 3): - compatible = False - elif not hasattr(sys, "base_prefix"): - compatible = False - if not compatible: - raise ValueError("This script is only for use with " "Python 3.3 or later") - if os.name == "nt": - use_symlinks = False - else: - use_symlinks = True - builder = ExtendedEnvBuilder( - system_site_packages=False, - clear=False, - symlinks=use_symlinks, - upgrade=False, - nodist=False, - nopip=False, - verbose=False, - ) + # init error messages: + errors = dict() # run all the model for iterations for fn in folders: - # create env - temp_dir = tempfile.mkdtemp() - env_path = Path(temp_dir).absolute() - sys.stderr.write(f"Creating Virtual Environment with path: {env_path}...\n") - builder.create(str(env_path)) - python_path = env_path / "bin" / "python" # TODO: FIX ME! - sys.stderr.write("\n") + # create env by anaconda + env_path, python_path, conda_activate = create_env() # get all files sys.stderr.write("Retrieving files...\n") yaml_path, req_path = get_all_files(folders[fn]) sys.stderr.write("\n") # install requirements.txt sys.stderr.write("Installing requirements.txt...\n") - os.system(f"{python_path} -m pip install -r {req_path}") + execute(f"{python_path} -m pip install -r {req_path}") sys.stderr.write("\n") + # setup gpu for tft + if fn == "TFT": + execute( + f"conda install -y --prefix {env_path} anaconda cudatoolkit=10.0 && conda install -y --prefix {env_path} cudnn" + ) + sys.stderr.write("\n") # install qlib sys.stderr.write("Installing qlib...\n") - os.system(f"{python_path} -m pip install --upgrade cython") # TODO: FIX ME! - os.system(f"{python_path} -m pip install -e git+https://github.com/you-n-g/qlib#egg=pyqlib") # TODO: FIX ME! + execute(f"{python_path} -m pip install --upgrade cython") # TODO: FIX ME! + if fn == "TFT": + execute( + f"cd {env_path} && {python_path} -m pip install --upgrade --force-reinstall --ignore-installed PyYAML -e git+https://github.com/you-n-g/qlib#egg=pyqlib" + ) # TODO: FIX ME! + else: + execute( + f"cd {env_path} && {python_path} -m pip install --upgrade --force-reinstall -e git+https://github.com/you-n-g/qlib#egg=pyqlib" + ) # TODO: FIX ME! sys.stderr.write("\n") # run workflow_by_config for multiple times for i in range(times): sys.stderr.write(f"Running the model: {fn} for iteration {i+1}...\n") - os.system(f"{python_path} {env_path / 'src/pyqlib/qlib/workflow/cli.py'} {yaml_path} {fn}") + errs = execute(f"{python_path} {env_path / 'src/pyqlib/qlib/workflow/cli.py'} {yaml_path} {fn}") + if errs is not None: + _errs = errors.get(fn, {}) + _errs.update({i: errs}) + errors[fn] = _errs sys.stderr.write("\n") # remove env sys.stderr.write(f"Deleting the environment: {env_path}...\n") @@ -335,13 +268,12 @@ def run(times=1, models=None, exclude=False): # generating md table sys.stderr.write(f"Generating markdown table...\n") gen_and_save_md_table(results) + sys.stderr.write("\n") + # print erros + sys.stderr.write(f"Here are some of the errors of the models...\n") + pprint(errors) + sys.stderr.write("\n") if __name__ == "__main__": - rc = 1 - try: - fire.Fire(run) # run all the model - rc = 0 - except Exception as e: - print("Error: %s" % e, file=sys.stderr) - sys.exit(rc) + fire.Fire(run) # run all the model diff --git a/requirements.txt b/requirements.txt index d3511d780..638ce22f4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -22,5 +22,4 @@ scikit_learn==0.23.2 torch==1.6.0 tqdm==4.49.0 yahooquery==2.2.7 -mlflow==1.12.1 -pytorch-tabnet==2.0.1 \ No newline at end of file +mlflow==1.12.1 \ No newline at end of file From f8101214ebdc3ed953e74ab25433f9376734bf39 Mon Sep 17 00:00:00 2001 From: zhupr Date: Fri, 27 Nov 2020 20:00:05 +0800 Subject: [PATCH 206/241] Fix report, support google colab --- README.md | 2 +- examples/workflow_by_code.ipynb | 43 ++++++++++++++++++++++++++++----- qlib/contrib/report/graph.py | 14 ++++++++++- 3 files changed, 51 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index dc9df109b..884bbb5c0 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ For more details, please refer to our paper ["Qlib: An AI-oriented Quantitative # Framework of Qlib

- +
diff --git a/examples/workflow_by_code.ipynb b/examples/workflow_by_code.ipynb index 692e52078..4860bbf9e 100644 --- a/examples/workflow_by_code.ipynb +++ b/examples/workflow_by_code.ipynb @@ -10,14 +10,42 @@ "# Licensed under the MIT License." ] }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "import sys, site\n", + "from pathlib import Path\n", + "\n", + "TEMP_CODE_DIR = str(Path(\"~/tmp/qlib_code\").expanduser().resolve())\n", + "\n", + "try:\n", + " import qlib\n", + " scripts_dir = Path.cwd().parent.joinpath(\"scripts\")\n", + "except ImportError:\n", + " # install qlib\n", + " ! pip install pyqlib\n", + " # reload\n", + " site.main()\n", + " # download get_data.py script\n", + " scripts_dir = Path(\"~/tmp/qlib_code/scripts\").expanduser().resolve()\n", + " scripts_dir.mkdir(parents=True, exist_ok=True)\n", + " import requests\n", + " with requests.get(\"https://github.com/microsoft/qlib/blob/main/scripts/get_data.py\") as resp:\n", + " with open(scripts_dir.joinpath(\"get_data.py\"), \"wb\") as fp:\n", + " fp.write(resp.content)" + ] + }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ - "import sys\n", - "from pathlib import Path\n", "\n", "import qlib\n", "import pandas as pd\n", @@ -32,7 +60,7 @@ "from qlib.utils import exists_qlib_data, init_instance_by_config\n", "from qlib.workflow import R\n", "from qlib.workflow.record_temp import SignalRecord, PortAnaRecord\n", - "from qlib.utils import flatten_dict" + "from qlib.utils import flatten_dict\n" ] }, { @@ -48,7 +76,7 @@ "provider_uri = \"~/.qlib/qlib_data/cn_data\" # target_dir\n", "if not exists_qlib_data(provider_uri):\n", " print(f\"Qlib data is not found in {provider_uri}\")\n", - " sys.path.append(str(Path.cwd().parent.joinpath(\"scripts\")))\n", + " sys.path.append(str(scripts_dir))\n", " from get_data import GetData\n", " GetData().qlib_data(target_dir=provider_uri, region=REG_CN)\n", "qlib.init(provider_uri=provider_uri, region=REG_CN)" @@ -202,7 +230,9 @@ { "cell_type": "code", "execution_count": null, - "metadata": {}, + "metadata": { + "scrolled": false + }, "outputs": [], "source": [ "from qlib.contrib.report import analysis_model, analysis_position\n", @@ -320,7 +350,8 @@ "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", - "pygments_lexer": "ipython3" + "pygments_lexer": "ipython3", + "version": "3.7.9" }, "toc": { "base_numbering": 1, diff --git a/qlib/contrib/report/graph.py b/qlib/contrib/report/graph.py index 15cc5fd0e..0ac0ffbc9 100644 --- a/qlib/contrib/report/graph.py +++ b/qlib/contrib/report/graph.py @@ -96,7 +96,19 @@ class BaseGraph(object): """ py.init_notebook_mode() for _fig in figure_list: - py.iplot(_fig) + # NOTE: displays figures: https://plotly.com/python/renderers/ + # default: plotly_mimetype+notebook + # support renderers: import plotly.io as pio; print(pio.renderers) + renderer = None + try: + # in notebook + _ipykernel = str(type(get_ipython())) + if 'google.colab' in _ipykernel: + renderer = 'colab' + except NameError: + pass + + _fig.show(renderer=renderer) def _get_layout(self) -> go.Layout: """ From f05df04320e2db30831167efe602182038c1fda8 Mon Sep 17 00:00:00 2001 From: Jactus Date: Fri, 27 Nov 2020 20:00:58 +0800 Subject: [PATCH 207/241] Fix --- examples/run_all_model.py | 4 +++- qlib/contrib/report/graph.py | 4 ++-- qlib/workflow/cli.py | 7 +++++-- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/examples/run_all_model.py b/examples/run_all_model.py index c02077b32..620632588 100644 --- a/examples/run_all_model.py +++ b/examples/run_all_model.py @@ -250,7 +250,9 @@ def run(times=1, models=None, exclude=False): # run workflow_by_config for multiple times for i in range(times): sys.stderr.write(f"Running the model: {fn} for iteration {i+1}...\n") - errs = execute(f"{python_path} {env_path / 'src/pyqlib/qlib/workflow/cli.py'} {yaml_path} {fn}") + errs = execute( + f"{python_path} {env_path / 'src/pyqlib/qlib/workflow/cli.py'} {yaml_path} {fn} {exp_manager}" + ) if errs is not None: _errs = errors.get(fn, {}) _errs.update({i: errs}) diff --git a/qlib/contrib/report/graph.py b/qlib/contrib/report/graph.py index 0ac0ffbc9..3fa688d36 100644 --- a/qlib/contrib/report/graph.py +++ b/qlib/contrib/report/graph.py @@ -103,8 +103,8 @@ class BaseGraph(object): try: # in notebook _ipykernel = str(type(get_ipython())) - if 'google.colab' in _ipykernel: - renderer = 'colab' + if "google.colab" in _ipykernel: + renderer = "colab" except NameError: pass diff --git a/qlib/workflow/cli.py b/qlib/workflow/cli.py index 08c13de2a..451337343 100644 --- a/qlib/workflow/cli.py +++ b/qlib/workflow/cli.py @@ -41,7 +41,7 @@ def sys_config(config, config_path): # worflow handler function -def workflow(config_path, experiment_name="workflow"): +def workflow(config_path, experiment_name="workflow", exp_manager=None): with open(config_path) as fp: config = yaml.load(fp, Loader=yaml.Loader) @@ -50,7 +50,10 @@ def workflow(config_path, experiment_name="workflow"): provider_uri = config.get("provider_uri") region = config.get("region") - qlib.init(provider_uri=provider_uri, region=region) + if exp_manager: + qlib.init(provider_uri=provider_uri, region=region, exp_manager=exp_manager) + else: + qlib.init(provider_uri=provider_uri, region=region) task_train(config, experiment_name=experiment_name) From b3afcc67d4cc83e6fdaf56b6b3053e9cf9fbe1aa Mon Sep 17 00:00:00 2001 From: Jactus Date: Fri, 27 Nov 2020 20:03:55 +0800 Subject: [PATCH 208/241] Add path --- examples/run_all_model.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/examples/run_all_model.py b/examples/run_all_model.py index 620632588..93d7ac822 100644 --- a/examples/run_all_model.py +++ b/examples/run_all_model.py @@ -28,11 +28,12 @@ from qlib.utils import exists_qlib_data # init qlib provider_uri = "~/.qlib/qlib_data/cn_data" +exp_path = str(Path(os.getcwd()).resolve() / "run_all_model_records") exp_manager = { "class": "MLflowExpManager", "module_path": "qlib.workflow.expm", "kwargs": { - "uri": "file:" + str(Path(os.getcwd()).resolve() / "run_all_model_records"), + "uri": "file:" + exp_path, "default_exp_name": "Experiment", }, } @@ -43,7 +44,8 @@ if not exists_qlib_data(provider_uri): GetData().qlib_data(target_dir=provider_uri, region=REG_CN) qlib.init(provider_uri=provider_uri, region=REG_CN, exp_manager=exp_manager) -shutil.rmtree(str(Path(os.getcwd()).resolve() / "run_all_model_records")) +if os.path.isdir(exp_path): + shutil.rmtree(exp_path) # decorator to check the arguments def only_allow_defined_args(function_to_decorate): From 8b281957b0f7444493565857f073eb446c314645 Mon Sep 17 00:00:00 2001 From: Jactus Date: Fri, 27 Nov 2020 20:21:23 +0800 Subject: [PATCH 209/241] Fix fire --- examples/run_all_model.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/run_all_model.py b/examples/run_all_model.py index 93d7ac822..ee98177c2 100644 --- a/examples/run_all_model.py +++ b/examples/run_all_model.py @@ -253,7 +253,7 @@ def run(times=1, models=None, exclude=False): for i in range(times): sys.stderr.write(f"Running the model: {fn} for iteration {i+1}...\n") errs = execute( - f"{python_path} {env_path / 'src/pyqlib/qlib/workflow/cli.py'} {yaml_path} {fn} {exp_manager}" + f"{python_path} {env_path / 'src/pyqlib/qlib/workflow/cli.py'} {yaml_path} {fn} '{exp_manager}'" ) if errs is not None: _errs = errors.get(fn, {}) From 10747a3219cc59474613184cb4bafdd5d202ed7d Mon Sep 17 00:00:00 2001 From: Jactus Date: Fri, 27 Nov 2020 21:19:27 +0800 Subject: [PATCH 210/241] Fix --- examples/run_all_model.py | 5 +++-- qlib/workflow/cli.py | 12 ++++++------ 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/examples/run_all_model.py b/examples/run_all_model.py index ee98177c2..f40f11444 100644 --- a/examples/run_all_model.py +++ b/examples/run_all_model.py @@ -28,7 +28,8 @@ from qlib.utils import exists_qlib_data # init qlib provider_uri = "~/.qlib/qlib_data/cn_data" -exp_path = str(Path(os.getcwd()).resolve() / "run_all_model_records") +exp_folder_name = "run_all_model_records" +exp_path = str(Path(os.getcwd()).resolve() / exp_folder_name) exp_manager = { "class": "MLflowExpManager", "module_path": "qlib.workflow.expm", @@ -253,7 +254,7 @@ def run(times=1, models=None, exclude=False): for i in range(times): sys.stderr.write(f"Running the model: {fn} for iteration {i+1}...\n") errs = execute( - f"{python_path} {env_path / 'src/pyqlib/qlib/workflow/cli.py'} {yaml_path} {fn} '{exp_manager}'" + f"{python_path} {env_path / 'src/pyqlib/qlib/workflow/cli.py'} {yaml_path} {fn} {exp_folder_name}" ) if errs is not None: _errs = errors.get(fn, {}) diff --git a/qlib/workflow/cli.py b/qlib/workflow/cli.py index 451337343..e0c957f60 100644 --- a/qlib/workflow/cli.py +++ b/qlib/workflow/cli.py @@ -1,13 +1,14 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. -import sys +import sys, os from pathlib import Path import qlib import fire import pandas as pd import ruamel.yaml as yaml +from qlib.config import C from qlib.model.trainer import task_train @@ -41,7 +42,7 @@ def sys_config(config, config_path): # worflow handler function -def workflow(config_path, experiment_name="workflow", exp_manager=None): +def workflow(config_path, experiment_name="workflow", uri_folder="mlruns"): with open(config_path) as fp: config = yaml.load(fp, Loader=yaml.Loader) @@ -50,10 +51,9 @@ def workflow(config_path, experiment_name="workflow", exp_manager=None): provider_uri = config.get("provider_uri") region = config.get("region") - if exp_manager: - qlib.init(provider_uri=provider_uri, region=region, exp_manager=exp_manager) - else: - qlib.init(provider_uri=provider_uri, region=region) + exp_manager = C["exp_manager"] + exp_manager["kwargs"]["uri"] = "file:" + str(Path(os.getcwd()).resolve() / uri_folder + qlib.init(provider_uri=provider_uri, region=region, exp_manager=exp_manager) task_train(config, experiment_name=experiment_name) From 1b781527155e3515feb17b3865977dce93825446 Mon Sep 17 00:00:00 2001 From: Jactus Date: Fri, 27 Nov 2020 21:20:04 +0800 Subject: [PATCH 211/241] Fix --- qlib/workflow/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qlib/workflow/cli.py b/qlib/workflow/cli.py index e0c957f60..65d9a14b4 100644 --- a/qlib/workflow/cli.py +++ b/qlib/workflow/cli.py @@ -52,7 +52,7 @@ def workflow(config_path, experiment_name="workflow", uri_folder="mlruns"): provider_uri = config.get("provider_uri") region = config.get("region") exp_manager = C["exp_manager"] - exp_manager["kwargs"]["uri"] = "file:" + str(Path(os.getcwd()).resolve() / uri_folder + exp_manager["kwargs"]["uri"] = "file:" + str(Path(os.getcwd()).resolve() / uri_folder) qlib.init(provider_uri=provider_uri, region=region, exp_manager=exp_manager) task_train(config, experiment_name=experiment_name) From be2173d8392cebbe3a2385429a4ff96f1bf20b5c Mon Sep 17 00:00:00 2001 From: zhupr Date: Fri, 27 Nov 2020 22:08:35 +0800 Subject: [PATCH 212/241] Fix --- examples/workflow_by_code.ipynb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/workflow_by_code.ipynb b/examples/workflow_by_code.ipynb index 4860bbf9e..f8370789b 100644 --- a/examples/workflow_by_code.ipynb +++ b/examples/workflow_by_code.ipynb @@ -35,7 +35,7 @@ " scripts_dir = Path(\"~/tmp/qlib_code/scripts\").expanduser().resolve()\n", " scripts_dir.mkdir(parents=True, exist_ok=True)\n", " import requests\n", - " with requests.get(\"https://github.com/microsoft/qlib/blob/main/scripts/get_data.py\") as resp:\n", + " with requests.get(\"https://raw.githubusercontent.com/you-n-g/qlib/main/scripts/get_data.py\") as resp:\n", " with open(scripts_dir.joinpath(\"get_data.py\"), \"wb\") as fp:\n", " fp.write(resp.content)" ] From f0454667f3f1767c98506f3b3f3886a7eaa5e059 Mon Sep 17 00:00:00 2001 From: Jactus Date: Fri, 27 Nov 2020 22:16:10 +0800 Subject: [PATCH 213/241] Fix workflow --- qlib/model/trainer.py | 1 + qlib/workflow/expm.py | 13 +++++-------- qlib/workflow/recorder.py | 2 ++ 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/qlib/model/trainer.py b/qlib/model/trainer.py index e4fc8eef9..0ef062021 100644 --- a/qlib/model/trainer.py +++ b/qlib/model/trainer.py @@ -1,5 +1,6 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. + from qlib.utils import init_instance_by_config, flatten_dict from qlib.workflow import R from qlib.workflow.record_temp import SignalRecord diff --git a/qlib/workflow/expm.py b/qlib/workflow/expm.py index 156beb690..80d471845 100644 --- a/qlib/workflow/expm.py +++ b/qlib/workflow/expm.py @@ -239,20 +239,17 @@ class MLflowExpManager(ExpManager): return self._client def start_exp(self, experiment_name=None, recorder_name=None, uri=None): + # set the tracking uri + if uri is None: + logger.info("No tracking URI is provided. Use the default tracking URI.") + else: + self.uri = uri # create experiment experiment, _ = self._get_or_create_exp(experiment_name=experiment_name) # set up active experiment self.active_experiment = experiment # start the experiment self.active_experiment.start(recorder_name) - # set the tracking uri - if uri is None: - logger.info( - "No tracking URI is provided. The default tracking URI is set as `mlruns` under the working directory." - ) - else: - self.uri = uri - mlflow.set_tracking_uri(self.uri) return self.active_experiment diff --git a/qlib/workflow/recorder.py b/qlib/workflow/recorder.py index b3069b9ac..b381a914a 100644 --- a/qlib/workflow/recorder.py +++ b/qlib/workflow/recorder.py @@ -224,6 +224,8 @@ class MLflowRecorder(Recorder): ) def start_run(self): + # set the tracking uri + mlflow.set_tracking_uri(self.uri) # start the run run = mlflow.start_run(self.id, self.experiment_id, self.name) # save the run id and artifact_uri From 0824e0e65cb7d838fb6c61ad42068555e35ae11a Mon Sep 17 00:00:00 2001 From: Jactus Date: Fri, 27 Nov 2020 22:21:03 +0800 Subject: [PATCH 214/241] Fix recorder --- qlib/workflow/recorder.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qlib/workflow/recorder.py b/qlib/workflow/recorder.py index b381a914a..4c1ddfdfe 100644 --- a/qlib/workflow/recorder.py +++ b/qlib/workflow/recorder.py @@ -225,7 +225,7 @@ class MLflowRecorder(Recorder): def start_run(self): # set the tracking uri - mlflow.set_tracking_uri(self.uri) + mlflow.set_tracking_uri(self._uri) # start the run run = mlflow.start_run(self.id, self.experiment_id, self.name) # save the run id and artifact_uri From 7952d7993209978094417ffa2b4f536d2abd6dfa Mon Sep 17 00:00:00 2001 From: Jactus Date: Fri, 27 Nov 2020 22:26:53 +0800 Subject: [PATCH 215/241] Fix script --- examples/run_all_model.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/run_all_model.py b/examples/run_all_model.py index f40f11444..05839a125 100644 --- a/examples/run_all_model.py +++ b/examples/run_all_model.py @@ -153,7 +153,7 @@ def get_all_results(folders) -> dict: result["information_ratio_with_cost"] = list() result["max_drawdown_with_cost"] = list() for recorder_id in recorders: - if recorders[recorder_id]["status"] == "FINISHED": + if recorders[recorder_id].status == "FINISHED": recorder = R.get_recorder(recorder_id=recorder_id, experiment_name=fn) metrics = recorder.list_metrics() result["annualized_return_with_cost"].append(metrics["excess_return_with_cost.annualized_return"]) From bebce24a7c35c81d2ad4ed492d9e9e56d8f6662b Mon Sep 17 00:00:00 2001 From: lwwang1995 Date: Fri, 27 Nov 2020 22:30:05 +0800 Subject: [PATCH 216/241] Update all baseline models. --- .../benchmarks/GATs/workflow_config_gats.yaml | 28 +- examples/benchmarks/HATS/README.md | 15 - examples/benchmarks/HATS/requirements.txt | 4 - .../benchmarks/HATS/worflow_config_hats.yaml | 77 --- .../benchmarks/LSTM/model_lstm_csi300.pkl | Bin 209290 -> 209290 bytes examples/benchmarks/TabNet/README.md | 4 - examples/benchmarks/TabNet/requirements.txt | 5 - .../TabNet/workflow_config_tabnet.yaml | 66 --- qlib/contrib/model/catboost_model.py | 8 +- qlib/contrib/model/pytorch_alstm.py | 60 ++- qlib/contrib/model/pytorch_gats.py | 100 ++-- qlib/contrib/model/pytorch_gru.py | 34 +- qlib/contrib/model/pytorch_hats.py | 491 ------------------ qlib/contrib/model/pytorch_lstm.py | 34 +- qlib/contrib/model/pytorch_sfm.py | 115 +++- qlib/contrib/model/tabnet.py | 85 --- qlib/contrib/model/xgboost.py | 12 +- 17 files changed, 282 insertions(+), 856 deletions(-) delete mode 100644 examples/benchmarks/HATS/README.md delete mode 100644 examples/benchmarks/HATS/requirements.txt delete mode 100644 examples/benchmarks/HATS/worflow_config_hats.yaml delete mode 100644 examples/benchmarks/TabNet/README.md delete mode 100644 examples/benchmarks/TabNet/requirements.txt delete mode 100644 examples/benchmarks/TabNet/workflow_config_tabnet.yaml mode change 100755 => 100644 qlib/contrib/model/pytorch_gats.py delete mode 100644 qlib/contrib/model/pytorch_hats.py delete mode 100644 qlib/contrib/model/tabnet.py diff --git a/examples/benchmarks/GATs/workflow_config_gats.yaml b/examples/benchmarks/GATs/workflow_config_gats.yaml index 33aa0fe8d..7212e0ee2 100644 --- a/examples/benchmarks/GATs/workflow_config_gats.yaml +++ b/examples/benchmarks/GATs/workflow_config_gats.yaml @@ -8,6 +8,20 @@ data_handler_config: &data_handler_config fit_start_time: 2008-01-01 fit_end_time: 2014-12-31 instruments: *market + infer_processors: + - class: RobustZScoreNorm + kwargs: + fields_group: feature + clip_outlier: true + - class: Fillna + kwargs: + fields_group: feature + learn_processors: + - class: DropnaLabel + - class: CSRankNorm + kwargs: + fields_group: label + label: ["Ref($close, -2) / Ref($close, -1) - 1"] port_analysis_config: &port_analysis_config strategy: class: TopkDropoutStrategy @@ -26,8 +40,8 @@ port_analysis_config: &port_analysis_config min_cost: 5 task: model: - class: GAT - module_path: qlib.contrib.model.pytorch_gats + class: GAT_Classic + module_path: qlib.contrib.model.pytorch_gats_classic kwargs: d_feat: 6 hidden_size: 64 @@ -38,8 +52,7 @@ task: early_stop: 20 metric: loss loss: mse - base_model: LSTM - with_pretrain: True + base_model: GRU seed: 0 GPU: 0 dataset: @@ -47,7 +60,7 @@ task: module_path: qlib.data.dataset kwargs: handler: - class: ALPHA360_Denoise + class: ALPHA360 module_path: qlib.contrib.data.handler kwargs: *data_handler_config segments: @@ -58,11 +71,6 @@ task: - class: SignalRecord module_path: qlib.workflow.record_temp kwargs: {} - - class: SigAnaRecord - module_path: qlib.workflow.record_temp - kwargs: - ana_long_short: False - ann_scaler: 252 - class: PortAnaRecord module_path: qlib.workflow.record_temp kwargs: diff --git a/examples/benchmarks/HATS/README.md b/examples/benchmarks/HATS/README.md deleted file mode 100644 index b70dbff25..000000000 --- a/examples/benchmarks/HATS/README.md +++ /dev/null @@ -1,15 +0,0 @@ -## Requirement - -* pandas==1.1.2 -* numpy==1.17.4 -* scikit_learn==0.23.2 -* torch==1.7.0 - -## HATS - -* HATS is a a hierarchical attention network for stock prediction which uses relational data for stock market prediction. HATS selectively aggregates information -on different relation types and adds the information to the representations of each company. HATS is used as a relational modeling module with initialized node representations.Furthermore, HATS -can predict not only individual stock prices but also market index movements, which is similar to the graph classification task. - -* HATS uses pretrained model of GRU and LSTM. The code of GRU and LSTM used in Qlib is a pyTorch implemention of GRU and LSTM. -* Paper address:HATS: A Hierarchical Graph Attention Network for Stock Movement Prediction https://arxiv.org/pdf/1908.07999.pdf \ No newline at end of file diff --git a/examples/benchmarks/HATS/requirements.txt b/examples/benchmarks/HATS/requirements.txt deleted file mode 100644 index 16de0a438..000000000 --- a/examples/benchmarks/HATS/requirements.txt +++ /dev/null @@ -1,4 +0,0 @@ -pandas==1.1.2 -numpy==1.17.4 -scikit_learn==0.23.2 -torch==1.7.0 diff --git a/examples/benchmarks/HATS/worflow_config_hats.yaml b/examples/benchmarks/HATS/worflow_config_hats.yaml deleted file mode 100644 index b08df14e0..000000000 --- a/examples/benchmarks/HATS/worflow_config_hats.yaml +++ /dev/null @@ -1,77 +0,0 @@ -provider_uri: "~/.qlib/qlib_data/cn_data" -region: cn -market: &market csi300 -benchmark: &benchmark SH000300 -data_handler_config: &data_handler_config - start_time: 2008-01-01 - end_time: 2020-08-01 - fit_start_time: 2008-01-01 - fit_end_time: 2014-12-31 - instruments: *market - infer_processors: - - class: RobustZScoreNorm - kwargs: - fields_group: feature - clip_outlier: true - - class: Fillna - kwargs: - fields_group: feature - learn_processors: - - class: DropnaLabel - - class: CSRankNorm - kwargs: - fields_group: label - label: ["Ref($close, -2) / Ref($close, -1) - 1"] -port_analysis_config: &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 -task: - model: - class: HATS - module_path: qlib.contrib.model.pytorch_hats - kwargs: - d_feat: 6 - hidden_size: 64 - num_layers: 2 - dropout: 0.6 - n_epochs: 200 - lr: 1e-3 - early_stop: 20 - metric: loss - loss: mse - base_model: GRU - seed: 0 - GPU: 0 - dataset: - class: DatasetH - module_path: qlib.data.dataset - kwargs: - handler: - class: ALPHA360 - module_path: qlib.contrib.data.handler - kwargs: *data_handler_config - segments: - train: [2008-01-01, 2014-12-31] - valid: [2015-01-01, 2016-12-31] - test: [2017-01-01, 2020-08-01] - record: - - class: SignalRecord - module_path: qlib.workflow.record_temp - kwargs: {} - - class: PortAnaRecord - module_path: qlib.workflow.record_temp - kwargs: - config: *port_analysis_config \ No newline at end of file diff --git a/examples/benchmarks/LSTM/model_lstm_csi300.pkl b/examples/benchmarks/LSTM/model_lstm_csi300.pkl index ff7fee4506e7f74de8b105ab960f83338ff9a62e..84d6419da2801bbe484bb71cfd40188225fe7951 100644 GIT binary patch literal 209290 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

8jr?!> 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 [batch, seq_len, input_size] - rnn_out, _ = self.rnn(self.net(inputs)) # [batch, seq_len, num_directions * hidden_size] + inputs = inputs.permute( + 0, 2, 1 + ) # [batch, input_size, seq_len] -> [batch, seq_len, input_size] + rnn_out, _ = self.rnn( + self.net(inputs) + ) # [batch, seq_len, num_directions * hidden_size] attention_score = self.att_net(rnn_out) # [batch, seq_len, 1] out_att = torch.mul(rnn_out, attention_score) out_att = torch.sum(out_att, dim=1) diff --git a/qlib/contrib/model/pytorch_gats.py b/qlib/contrib/model/pytorch_gats.py old mode 100755 new mode 100644 index 77a02a9b2..226204fe7 --- a/qlib/contrib/model/pytorch_gats.py +++ b/qlib/contrib/model/pytorch_gats.py @@ -19,10 +19,12 @@ import torch.optim as optim from ...model.base import Model from ...data.dataset import DatasetH from ...data.dataset.handler import DataHandlerLP +from ...contrib.model.pytorch_lstm import LSTMModel +from ...contrib.model.pytorch_gru import GRUModel -class GAT(Model): - """GAT Model +class GATs(Model): + """GATs Model Parameters ---------- @@ -57,8 +59,8 @@ class GAT(Model): **kwargs ): # Set logger. - self.logger = get_module_logger("GAT") - self.logger.info("GAT pytorch version...") + self.logger = get_module_logger("GATs") + self.logger.info("GATs pytorch version...") # set hyper-parameters. self.d_feat = d_feat @@ -78,7 +80,7 @@ class GAT(Model): self.seed = seed self.logger.info( - "GAT parameters setting:" + "GATs parameters setting:" "\nd_feat : {}" "\nhidden_size : {}" "\nnum_layers : {}" @@ -124,7 +126,9 @@ class GAT(Model): elif optimizer.lower() == "gd": self.train_optimizer = optim.SGD(self.GAT_model.parameters(), lr=self.lr) else: - raise NotImplementedError("optimizer {} is not supported!".format(optimizer)) + raise NotImplementedError( + "optimizer {} is not supported!".format(optimizer) + ) self._fitted = False if self.use_gpu: @@ -149,18 +153,18 @@ class GAT(Model): mask = torch.isfinite(label) - if self.metric == "" or self.metric == "loss": # use loss + if self.metric == "" or self.metric == "loss": return -self.loss_fn(pred[mask], label[mask]) raise ValueError("unknown metric `%s`" % self.metric) def get_daily_inter(self, df, shuffle=False): - # organize the train data into daily inter as daily batches + # organize the train data into daily batches daily_count = df.groupby(level=0).size().values daily_index = np.roll(np.cumsum(daily_count), 1) daily_index[0] = 0 if shuffle: - # shuffle the daily inter data + # shuffle data daily_shuffle = list(zip(daily_index, daily_count)) np.random.shuffle(daily_shuffle) daily_index, daily_count = zip(*daily_shuffle) @@ -172,7 +176,7 @@ class GAT(Model): y_train_values = np.squeeze(y_train.values) self.GAT_model.train() - # organize the train data into daily inter as daily batches + # organize the train data into daily batches daily_index, daily_count = self.get_daily_inter(x_train, shuffle=True) for idx, count in zip(daily_index, daily_count): @@ -203,7 +207,7 @@ class GAT(Model): scores = [] losses = [] - # organize the test data into daily inter as daily batches + # organize the test data into daily batches daily_index, daily_count = self.get_daily_inter(data_x, shuffle=False) for idx, count in zip(daily_index, daily_count): @@ -233,7 +237,9 @@ class GAT(Model): ): df_train, df_valid, df_test = dataset.prepare( - ["train", "valid", "test"], col_set=["feature", "label"], data_key=DataHandlerLP.DK_L + ["train", "valid", "test"], + col_set=["feature", "label"], + data_key=DataHandlerLP.DK_L, ) x_train, y_train = df_train["feature"], df_train["label"] @@ -251,17 +257,23 @@ class GAT(Model): if self.with_pretrain: self.logger.info("Loading pretrained model...") if self.base_model == "LSTM": - from ...contrib.model.pytorch_lstm import LSTMModel - pretrained_model = LSTMModel() - pretrained_model.load_state_dict(torch.load("benchmarks/LSTM/model_lstm_csi300.pkl")) - elif self.base_model == "GRU": - from ...contrib.model.pytorch_gru import GRUModel + pretrained_model.load_state_dict( + torch.load("benchmarks/LSTM/model_lstm_csi300.pkl") + ) + elif self.base_model == "GRU": pretrained_model = GRUModel() - pretrained_model.load_state_dict(torch.load("benchmarks/GRU/model_gru_csi300.pkl")) + pretrained_model.load_state_dict( + torch.load("benchmarks/GRU/model_gru_csi300.pkl") + ) + model_dict = self.GAT_model.state_dict() - pretrained_dict = {k: v for k, v in pretrained_model.state_dict().items() if k in model_dict} + pretrained_dict = { + k: v + for k, v in pretrained_model.state_dict().items() + if k in model_dict + } model_dict.update(pretrained_dict) self.GAT_model.load_state_dict(model_dict) self.logger.info("Loading pretrained model Done...") @@ -269,7 +281,6 @@ class GAT(Model): # train self.logger.info("training...") self._fitted = True - # return for step in range(self.n_epochs): self.logger.info("Epoch%d:", step) @@ -310,7 +321,7 @@ class GAT(Model): x_values = x_test.values preds = [] - # organize the data into daily inter as daily batches + # organize the data into daily batches daily_index, daily_count = self.get_daily_inter(x_test, shuffle=False) for idx, count in zip(daily_index, daily_count): @@ -332,7 +343,9 @@ class GAT(Model): class GATModel(nn.Module): - def __init__(self, d_feat=6, hidden_size=64, num_layers=2, dropout=0.0, base_model="GRU"): + def __init__( + self, d_feat=6, hidden_size=64, num_layers=2, dropout=0.0, base_model="GRU" + ): super().__init__() if base_model == "GRU": @@ -355,22 +368,29 @@ class GATModel(nn.Module): raise ValueError("unknown base model name `%s`" % base_model) self.hidden_size = hidden_size - self.bn1 = nn.BatchNorm1d(num_features=hidden_size, track_running_stats=False) - self.fc = nn.Linear(hidden_size, hidden_size) - self.bn2 = nn.BatchNorm1d(num_features=hidden_size, track_running_stats=False) + self.d_feat = d_feat + self.transformation = nn.Linear(self.hidden_size, self.hidden_size) + self.a = nn.Parameter(torch.randn(self.hidden_size * 2, 1)) + self.a.requires_grad = True + self.fc = nn.Linear(self.hidden_size, self.hidden_size) self.fc_out = nn.Linear(hidden_size, 1) self.leaky_relu = nn.LeakyReLU() self.softmax = nn.Softmax(dim=1) - self.d_feat = d_feat - def cal_convariance(self, x, y): # the 2nd dimension of x and y are the same - e_x = torch.mean(x, dim=1).reshape(-1, 1) - e_y = torch.mean(y, dim=1).reshape(-1, 1) - e_x_e_y = e_x.mm(torch.t(e_y)) - x_extend = x.reshape(x.shape[0], 1, x.shape[1]).repeat(1, y.shape[0], 1) - y_extend = y.reshape(1, y.shape[0], y.shape[1]).repeat(x.shape[0], 1, 1) - e_xy = torch.mean(x_extend * y_extend, dim=2) - return e_xy - e_x_e_y + def cal_attention(self, x, y): + x = self.transformation(x) + y = self.transformation(y) + + sample_num = x.shape[0] + dim = x.shape[1] + e_x = x.expand(sample_num, sample_num, dim) + e_y = torch.transpose(e_x, 0, 1) + attention_in = torch.cat((e_x, e_y), 2).view(-1, dim * 2) + self.a_t = torch.t(self.a) + attention_out = self.a_t.mm(torch.t(attention_in)).view(sample_num, sample_num) + attention_out = self.leaky_relu(attention_out) + att_weight = self.softmax(attention_out) + return att_weight def forward(self, x): # x: [N, F*T] @@ -378,10 +398,8 @@ class GATModel(nn.Module): x = x.permute(0, 2, 1) # [N, T, F] out, _ = self.rnn(x) hidden = out[:, -1, :] - hidden = self.bn1(hidden) - gamma = self.cal_convariance(hidden, hidden) - output = gamma.mm(hidden) - output = self.fc(output) - output = self.bn2(output) - output = self.leaky_relu(output) - return self.fc_out(output).squeeze() + att_weight = self.cal_attention(hidden, hidden) + hidden = att_weight.mm(hidden) + hidden + hidden = self.fc(hidden) + hidden = self.leaky_relu(hidden) + return self.fc_out(hidden).squeeze() diff --git a/qlib/contrib/model/pytorch_gru.py b/qlib/contrib/model/pytorch_gru.py index 02664b6ac..935716bcc 100755 --- a/qlib/contrib/model/pytorch_gru.py +++ b/qlib/contrib/model/pytorch_gru.py @@ -11,7 +11,12 @@ import pandas as pd import copy from sklearn.metrics import roc_auc_score, mean_squared_error import logging -from ...utils import unpack_archive_with_buffer, save_multiple_parts_file, create_save_path, drop_nan_by_y_index +from ...utils import ( + unpack_archive_with_buffer, + save_multiple_parts_file, + create_save_path, + drop_nan_by_y_index, +) from ...log import get_module_logger, TimeInspector import torch @@ -109,14 +114,19 @@ class GRU(Model): ) self.gru_model = GRUModel( - d_feat=self.d_feat, hidden_size=self.hidden_size, num_layers=self.num_layers, dropout=self.dropout + d_feat=self.d_feat, + hidden_size=self.hidden_size, + num_layers=self.num_layers, + dropout=self.dropout, ) if optimizer.lower() == "adam": self.train_optimizer = optim.Adam(self.gru_model.parameters(), lr=self.lr) elif optimizer.lower() == "gd": self.train_optimizer = optim.SGD(self.gru_model.parameters(), lr=self.lr) else: - raise NotImplementedError("optimizer {} is not supported!".format(optimizer)) + raise NotImplementedError( + "optimizer {} is not supported!".format(optimizer) + ) self._fitted = False if self.use_gpu: @@ -141,7 +151,7 @@ class GRU(Model): mask = torch.isfinite(label) - if self.metric == "" or self.metric == "loss": # use loss + if self.metric == "" or self.metric == "loss": return -self.loss_fn(pred[mask], label[mask]) raise ValueError("unknown metric `%s`" % self.metric) @@ -161,8 +171,12 @@ class GRU(Model): if len(indices) - i < self.batch_size: break - feature = torch.from_numpy(x_train_values[indices[i : i + self.batch_size]]).float() - label = torch.from_numpy(y_train_values[indices[i : i + self.batch_size]]).float() + feature = torch.from_numpy( + x_train_values[indices[i : i + self.batch_size]] + ).float() + label = torch.from_numpy( + y_train_values[indices[i : i + self.batch_size]] + ).float() if self.use_gpu: feature = feature.cuda() @@ -194,7 +208,9 @@ class GRU(Model): if len(indices) - i < self.batch_size: break - feature = torch.from_numpy(x_values[indices[i : i + self.batch_size]]).float() + feature = torch.from_numpy( + x_values[indices[i : i + self.batch_size]] + ).float() label = torch.from_numpy(y_values[indices[i : i + self.batch_size]]).float() if self.use_gpu: @@ -219,7 +235,9 @@ class GRU(Model): ): df_train, df_valid, df_test = dataset.prepare( - ["train", "valid", "test"], col_set=["feature", "label"], data_key=DataHandlerLP.DK_L + ["train", "valid", "test"], + col_set=["feature", "label"], + data_key=DataHandlerLP.DK_L, ) x_train, y_train = df_train["feature"], df_train["label"] diff --git a/qlib/contrib/model/pytorch_hats.py b/qlib/contrib/model/pytorch_hats.py deleted file mode 100644 index 7affea73c..000000000 --- a/qlib/contrib/model/pytorch_hats.py +++ /dev/null @@ -1,491 +0,0 @@ -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - - -from __future__ import division -from __future__ import print_function - -import os -import numpy as np -import pandas as pd -import copy -from ...utils import create_save_path -from ...log import get_module_logger - -import torch -import torch.nn as nn -import torch.optim as optim - -from ...model.base import Model -from ...data.dataset import DatasetH -from ...data.dataset.handler import DataHandlerLP - - -class HATS(Model): - """HATS Model - - Parameters - ---------- - d_feat : int - input dimension for each time step - metric: str - the evaluate metric used in early stop - optimizer : str - optimizer name - GPU : str - the GPU ID(s) used for training - """ - - def __init__( - self, - d_feat=6, - hidden_size=64, - num_layers=2, - dropout=0.5, - n_epochs=200, - lr=0.01, - metric="", - early_stop=20, - loss="mse", - base_model="GRU", - with_pretrain=True, - optimizer="adam", - GPU="0", - seed=0, - **kwargs - ): - # Set logger. - self.logger = get_module_logger("HATS") - self.logger.info("HATS pytorch version...") - - # set hyper-parameters. - self.d_feat = d_feat - self.hidden_size = hidden_size - self.num_layers = num_layers - self.dropout = dropout - self.n_epochs = n_epochs - self.lr = lr - self.metric = metric - self.early_stop = early_stop - self.optimizer = optimizer.lower() - self.loss = loss - self.base_model = base_model - self.with_pretrain = with_pretrain - self.visible_GPU = GPU - self.use_gpu = torch.cuda.is_available() - self.seed = seed - - self.logger.info( - "HATS parameters setting:" - "\nd_feat : {}" - "\nhidden_size : {}" - "\nnum_layers : {}" - "\ndropout : {}" - "\nn_epochs : {}" - "\nlr : {}" - "\nmetric : {}" - "\nearly_stop : {}" - "\noptimizer : {}" - "\nloss_type : {}" - "\nbase_model : {}" - "\nwith_pretrain : {}" - "\nvisible_GPU : {}" - "\nuse_GPU : {}" - "\nseed : {}".format( - d_feat, - hidden_size, - num_layers, - dropout, - n_epochs, - lr, - metric, - early_stop, - optimizer.lower(), - loss, - base_model, - with_pretrain, - GPU, - self.use_gpu, - seed, - ) - ) - - self.HATS_model = HATSModel( - d_feat=self.d_feat, - hidden_size=self.hidden_size, - num_layers=self.num_layers, - dropout=self.dropout, - base_model=self.base_model, - ) - if optimizer.lower() == "adam": - self.train_optimizer = optim.Adam(self.HATS_model.parameters(), lr=self.lr) - elif optimizer.lower() == "gd": - self.train_optimizer = optim.SGD(self.HATS_model.parameters(), lr=self.lr) - else: - raise NotImplementedError("optimizer {} is not supported!".format(optimizer)) - - self._fitted = False - if self.use_gpu: - self.HATS_model.cuda() - # set the visible GPU - if self.visible_GPU: - os.environ["CUDA_VISIBLE_DEVICES"] = self.visible_GPU - - def mse(self, pred, label): - loss = (pred - label) ** 2 - return torch.mean(loss) - - def loss_fn(self, pred, label): - mask = ~torch.isnan(label) - - if self.loss == "mse": - return self.mse(pred[mask], label[mask]) - - raise ValueError("unknown loss `%s`" % self.loss) - - def metric_fn(self, pred, label): - mask = torch.isfinite(label) - - if self.metric == "" or self.metric == "loss": # use loss - return -self.loss_fn(pred[mask], label[mask]) - - raise ValueError("unknown metric `%s`" % self.metric) - - def get_daily_inter(self, df, shuffle=False): - # organize the train data into daily inter as daily batches - daily_count = df.groupby(level=0).size().values - daily_index = np.roll(np.cumsum(daily_count), 1) - daily_index[0] = 0 - if shuffle: - # shuffle the daily inter data - daily_shuffle = list(zip(daily_index, daily_count)) - np.random.shuffle(daily_shuffle) - daily_index, daily_count = zip(*daily_shuffle) - return daily_index, daily_count - - def train_epoch(self, x_train, y_train): - - x_train_values = x_train.values - y_train_values = np.squeeze(y_train.values) - - self.HATS_model.train() - - # organize the train data into daily inter as daily batches - daily_index, daily_count = self.get_daily_inter(x_train, shuffle=True) - - for idx, count in zip(daily_index, daily_count): - batch = slice(idx, idx + count) - feature = torch.from_numpy(x_train_values[batch]).float() - label = torch.from_numpy(y_train_values[batch]).float() - - if self.use_gpu: - feature = feature.cuda() - label = label.cuda() - - pred = self.HATS_model(feature) - loss = self.loss_fn(pred, label) - - self.train_optimizer.zero_grad() - loss.backward() - torch.nn.utils.clip_grad_value_(self.HATS_model.parameters(), 3.0) - self.train_optimizer.step() - - def test_epoch(self, data_x, data_y): - - # prepare testing data - x_values = data_x.values - y_values = np.squeeze(data_y.values) - - self.HATS_model.eval() - - scores = [] - losses = [] - - # organize the test data into daily inter as daily batches - daily_index, daily_count = self.get_daily_inter(data_x, shuffle=False) - - for idx, count in zip(daily_index, daily_count): - batch = slice(idx, idx + count) - feature = torch.from_numpy(x_values[batch]).float() - label = torch.from_numpy(y_values[batch]).float() - - if self.use_gpu: - feature = feature.cuda() - label = label.cuda() - - pred = self.HATS_model(feature) - loss = self.loss_fn(pred, label) - losses.append(loss.item()) - - score = self.metric_fn(pred, label) - scores.append(score.item()) - - return np.mean(losses), np.mean(scores) - - def fit( - self, - dataset: DatasetH, - evals_result=dict(), - verbose=True, - save_path=None, - ): - - df_train, df_valid, df_test = dataset.prepare( - ["train", "valid", "test"], col_set=["feature", "label"], data_key=DataHandlerLP.DK_L - ) - - x_train, y_train = df_train["feature"], df_train["label"] - x_valid, y_valid = df_valid["feature"], df_valid["label"] - - if save_path == None: - save_path = create_save_path(save_path) - stop_steps = 0 - best_score = -np.inf - best_epoch = 0 - evals_result["train"] = [] - evals_result["valid"] = [] - - # load pretrained base_model - if self.with_pretrain: - self.logger.info("Loading pretrained model...") - if self.base_model == "LSTM": - from ...contrib.model.pytorch_lstm import LSTMModel - - pretrained_model = LSTMModel() - pretrained_model.load_state_dict(torch.load("benchmarks/LSTM/model_lstm_csi300.pkl")) - elif self.base_model == "GRU": - from ...contrib.model.pytorch_gru import GRUModel - - pretrained_model = GRUModel() - pretrained_model.load_state_dict(torch.load("benchmarks/GRU/model_gru_csi300.pkl")) - model_dict = self.HATS_model.state_dict() - pretrained_dict = {k: v for k, v in pretrained_model.state_dict().items() if k in model_dict} - model_dict.update(pretrained_dict) - self.HATS_model.load_state_dict(model_dict) - self.logger.info("Loading pretrained model Done...") - - # train - self.logger.info("training...") - self._fitted = True - - for step in range(self.n_epochs): - self.logger.info("Epoch%d:", step) - self.logger.info("training...") - self.train_epoch(x_train, y_train) - self.logger.info("evaluating...") - train_loss, train_score = self.test_epoch(x_train, y_train) - val_loss, val_score = self.test_epoch(x_valid, y_valid) - self.logger.info("train %.6f, valid %.6f" % (train_score, val_score)) - evals_result["train"].append(train_score) - evals_result["valid"].append(val_score) - - if val_score > best_score: - best_score = val_score - stop_steps = 0 - best_epoch = step - best_param = copy.deepcopy(self.HATS_model.state_dict()) - else: - stop_steps += 1 - if stop_steps >= self.early_stop: - self.logger.info("early stop") - break - - self.logger.info("best score: %.6lf @ %d" % (best_score, best_epoch)) - self.HATS_model.load_state_dict(best_param) - torch.save(best_param, save_path) - - if self.use_gpu: - torch.cuda.empty_cache() - - def predict(self, dataset): - if not self._fitted: - raise ValueError("model is not fitted yet!") - - x_test = dataset.prepare("test", col_set="feature") - index = x_test.index - self.HATS_model.eval() - x_values = x_test.values - sample_num = x_values.shape[0] - preds = [] - - # organize the data into daily inter as daily batches - daily_index, daily_count = self.get_daily_inter(x_test, shuffle=False) - - for idx, count in zip(daily_index, daily_count): - batch = slice(idx, idx + count) - x_batch = torch.from_numpy(x_values[batch]).float() - - if self.use_gpu: - x_batch = x_batch.cuda() - - with torch.no_grad(): - if self.use_gpu: - pred = self.HATS_model(x_batch).detach().cpu().numpy() - else: - pred = self.HATS_model(x_batch).detach().numpy() - - preds.append(pred) - - return pd.Series(np.concatenate(preds), index=index) - - -class HATSModel(nn.Module): - def __init__(self, d_feat=6, hidden_size=64, num_layers=2, dropout=0.0, base_model="GRU"): - super().__init__() - - if base_model == "GRU": - self.model = nn.GRU( - input_size=d_feat, - hidden_size=hidden_size, - num_layers=num_layers, - batch_first=True, - dropout=dropout, - ) - elif base_model == "LSTM": - self.model = nn.LSTM( - input_size=d_feat, - hidden_size=hidden_size, - num_layers=num_layers, - batch_first=True, - dropout=dropout, - ) - else: - raise ValueError("unknown base model name `%s`" % base_model) - - self.hidden_size = hidden_size - self.bn1 = nn.BatchNorm1d(num_features=hidden_size, track_running_stats=False) - self.fc = nn.Linear(hidden_size, hidden_size) - self.bn2 = nn.BatchNorm1d(num_features=hidden_size, track_running_stats=False) - self.fc_out = nn.Linear(hidden_size, 1) - self.leaky_relu = nn.LeakyReLU() - self.softmax = nn.Softmax(dim=1) - self.d_feat = d_feat - - num_head_att = [1] * num_layers - hidden_dim = [hidden_size] * num_layers - dims = [d_feat] + [d * nh for (d, nh) in zip(hidden_dim, num_head_att[:-1])] + [num_head_att[-1]] - in_dims = dims[:-1] - out_dims = [d // nh for (d, nh) in zip(dims[1:], num_head_att)] - self.attn = nn.ModuleList( - [GraphAttention(i, o, nh, dropout) for (i, o, nh) in zip(in_dims, out_dims, num_head_att)] - ) - self.bns = nn.ModuleList([nn.BatchNorm1d(dim) for dim in dims[1:-1]]) - self.dropout = nn.Dropout(dropout) - self.elu = nn.ELU() - - def forward(self, x): - x = x.reshape(len(x), self.d_feat, -1) # [N, F, T] - x = x.permute(0, 2, 1) # [N, T, F] - out, _ = self.model(x) - hidden = out[:, -1, :] - hidden = self.bn1(hidden) - attention = GraphAttention.cal_attention(hidden, hidden) - output = attention.mm(hidden) - output = self.fc(output) - output = self.bn2(output) - output = self.leaky_relu(output) - return self.fc_out(output).squeeze() - - -class GraphAttention(nn.Module): - def __init__(self, input_dim, output_dim, num_heads, dropout=0.5): - - super().__init__() - - """ - Parameters - ---------- - input_dim : int - Dimension of input node features. - output_dim : int - Dimension of output node features. - num_heads : list of ints - Number of attention heads in each hidden layer and output layer. Must be non empty. Note that len(num_heads) = len(hidden_dims)+1. - dropout : float - Dropout rate. Default: 0.5. - """ - - self.input_dim = input_dim - self.output_dim = output_dim - self.num_heads = num_heads - - self.fcs = nn.ModuleList([nn.Linear(input_dim, output_dim) for _ in range(num_heads)]) - self.a = nn.ModuleList([nn.Linear(2 * output_dim, 1) for _ in range(num_heads)]) - - self.dropout = nn.Dropout(dropout) - self.softmax = nn.Softmax(dim=0) - self.leakyrelu = nn.LeakyReLU() - - def forward(self, features, nodes, mappings, rows): - - """ - Parameters - ---------- - features : torch.Tensor - An (n' x input_dim) tensor of input node features. - nodes : list of numpy array - nodes[i] is an array of the nodes in the ith layer of the - computation graph. - mappings : list of dictionary - mappings[i] is a dictionary mappings node v (labelled 0 to |V|-1) - in nodes[i] to its position in nodes[i]. For example, - if nodes[i] = [2,5], then mappings[i][2] = 0 and - mappings[i][5] = 1. - rows : numpy array - rows[i] is an array of neighbors of node i. - Returns - ------- - out : torch.Tensor - An (len(node_layers[-1]) x output_dim) tensor of output node features. - """ - - nprime = features.shape[0] - rows = [np.array([mappings[v] for v in row], dtype=np.int64) for row in rows] - sum_degs = np.hstack(([0], np.cumsum([len(row) for row in rows]))) - mapped_nodes = [mappings[v] for v in nodes] - indices = torch.LongTensor([[v, c] for (v, row) in zip(mapped_nodes, rows) for c in row]).t() - - out = [] - for k in range(self.num_heads): - h = self.fcs[k](features) - - nbr_h = torch.cat(tuple([h[row] for row in rows]), dim=0) - self_h = torch.cat( - tuple([h[mappings[nodes[i]]].repeat(len(row), 1) for (i, row) in enumerate(rows)]), dim=0 - ) - cat_h = torch.cat((self_h, nbr_h), dim=1) - - e = self.leakyrelu(self.a[k](cat_h)) - - alpha = [self.softmax(e[lo:hi]) for (lo, hi) in zip(sum_degs, sum_degs[1:])] - alpha = torch.cat(tuple(alpha), dim=0) - alpha = alpha.squeeze(1) - alpha = self.dropout(alpha) - - adj = torch.sparse.FloatTensor(indices, alpha, torch.Size([nprime, nprime])) - out.append(torch.sparse.mm(adj, h)[mapped_nodes]) - - return out - - @staticmethod - def cal_attention(x, y): - att_x = torch.mean(x, dim=1).reshape(-1, 1) - att_y = torch.mean(y, dim=1).reshape(-1, 1) - att = att_x.mm(torch.t(att_y)) - return ( - torch.mean( - x.reshape(x.shape[0], 1, x.shape[1]).repeat(1, y.shape[0], 1) - * y.reshape(1, y.shape[0], y.shape[1]).repeat(x.shape[0], 1, 1), - dim=2, - ) - - att - ) diff --git a/qlib/contrib/model/pytorch_lstm.py b/qlib/contrib/model/pytorch_lstm.py index f8951509a..1d1c0c986 100755 --- a/qlib/contrib/model/pytorch_lstm.py +++ b/qlib/contrib/model/pytorch_lstm.py @@ -11,7 +11,12 @@ import pandas as pd import copy from sklearn.metrics import roc_auc_score, mean_squared_error import logging -from ...utils import unpack_archive_with_buffer, save_multiple_parts_file, create_save_path, drop_nan_by_y_index +from ...utils import ( + unpack_archive_with_buffer, + save_multiple_parts_file, + create_save_path, + drop_nan_by_y_index, +) from ...log import get_module_logger, TimeInspector import torch @@ -109,14 +114,19 @@ class LSTM(Model): ) self.lstm_model = LSTMModel( - d_feat=self.d_feat, hidden_size=self.hidden_size, num_layers=self.num_layers, dropout=self.dropout + d_feat=self.d_feat, + hidden_size=self.hidden_size, + num_layers=self.num_layers, + dropout=self.dropout, ) if optimizer.lower() == "adam": self.train_optimizer = optim.Adam(self.lstm_model.parameters(), lr=self.lr) elif optimizer.lower() == "gd": self.train_optimizer = optim.SGD(self.lstm_model.parameters(), lr=self.lr) else: - raise NotImplementedError("optimizer {} is not supported!".format(optimizer)) + raise NotImplementedError( + "optimizer {} is not supported!".format(optimizer) + ) self._fitted = False if self.use_gpu: @@ -141,7 +151,7 @@ class LSTM(Model): mask = torch.isfinite(label) - if self.metric == "" or self.metric == "loss": # use loss + if self.metric == "" or self.metric == "loss": return -self.loss_fn(pred[mask], label[mask]) raise ValueError("unknown metric `%s`" % self.metric) @@ -161,8 +171,12 @@ class LSTM(Model): if len(indices) - i < self.batch_size: break - feature = torch.from_numpy(x_train_values[indices[i : i + self.batch_size]]).float() - label = torch.from_numpy(y_train_values[indices[i : i + self.batch_size]]).float() + feature = torch.from_numpy( + x_train_values[indices[i : i + self.batch_size]] + ).float() + label = torch.from_numpy( + y_train_values[indices[i : i + self.batch_size]] + ).float() if self.use_gpu: feature = feature.cuda() @@ -194,7 +208,9 @@ class LSTM(Model): if len(indices) - i < self.batch_size: break - feature = torch.from_numpy(x_values[indices[i : i + self.batch_size]]).float() + feature = torch.from_numpy( + x_values[indices[i : i + self.batch_size]] + ).float() label = torch.from_numpy(y_values[indices[i : i + self.batch_size]]).float() if self.use_gpu: @@ -219,7 +235,9 @@ class LSTM(Model): ): df_train, df_valid, df_test = dataset.prepare( - ["train", "valid", "test"], col_set=["feature", "label"], data_key=DataHandlerLP.DK_L + ["train", "valid", "test"], + col_set=["feature", "label"], + data_key=DataHandlerLP.DK_L, ) x_train, y_train = df_train["feature"], df_train["label"] diff --git a/qlib/contrib/model/pytorch_sfm.py b/qlib/contrib/model/pytorch_sfm.py index 1d27f3927..bebc408a8 100644 --- a/qlib/contrib/model/pytorch_sfm.py +++ b/qlib/contrib/model/pytorch_sfm.py @@ -19,7 +19,12 @@ import pandas as pd import copy from sklearn.metrics import roc_auc_score, mean_squared_error import logging -from ...utils import unpack_archive_with_buffer, save_multiple_parts_file, create_save_path, drop_nan_by_y_index +from ...utils import ( + unpack_archive_with_buffer, + save_multiple_parts_file, + create_save_path, + drop_nan_by_y_index, +) from ...log import get_module_logger, TimeInspector import torch @@ -33,7 +38,16 @@ from ...data.dataset.handler import DataHandlerLP class SFM_Model(nn.Module): - def __init__(self, d_feat=6, output_dim=1, freq_dim=10, hidden_size=64, dropout_W=0.0, dropout_U=0.0, device="cpu"): + def __init__( + self, + d_feat=6, + output_dim=1, + freq_dim=10, + hidden_size=64, + dropout_W=0.0, + dropout_U=0.0, + device="cpu", + ): super().__init__() self.input_dim = d_feat @@ -42,30 +56,52 @@ class SFM_Model(nn.Module): self.hidden_dim = hidden_size self.device = device - self.W_i = nn.Parameter(init.xavier_uniform_(torch.empty((self.input_dim, self.hidden_dim)))) - self.U_i = nn.Parameter(init.orthogonal_(torch.empty(self.hidden_dim, self.hidden_dim))) + self.W_i = nn.Parameter( + init.xavier_uniform_(torch.empty((self.input_dim, self.hidden_dim))) + ) + self.U_i = nn.Parameter( + init.orthogonal_(torch.empty(self.hidden_dim, self.hidden_dim)) + ) self.b_i = nn.Parameter(torch.zeros(self.hidden_dim)) - self.W_ste = nn.Parameter(init.xavier_uniform_(torch.empty(self.input_dim, self.hidden_dim))) - self.U_ste = nn.Parameter(init.orthogonal_(torch.empty(self.hidden_dim, self.hidden_dim))) + self.W_ste = nn.Parameter( + init.xavier_uniform_(torch.empty(self.input_dim, self.hidden_dim)) + ) + self.U_ste = nn.Parameter( + init.orthogonal_(torch.empty(self.hidden_dim, self.hidden_dim)) + ) self.b_ste = nn.Parameter(torch.ones(self.hidden_dim)) - self.W_fre = nn.Parameter(init.xavier_uniform_(torch.empty(self.input_dim, self.freq_dim))) - self.U_fre = nn.Parameter(init.orthogonal_(torch.empty(self.hidden_dim, self.freq_dim))) + self.W_fre = nn.Parameter( + init.xavier_uniform_(torch.empty(self.input_dim, self.freq_dim)) + ) + self.U_fre = nn.Parameter( + init.orthogonal_(torch.empty(self.hidden_dim, self.freq_dim)) + ) self.b_fre = nn.Parameter(torch.ones(self.freq_dim)) - self.W_c = nn.Parameter(init.xavier_uniform_(torch.empty(self.input_dim, self.hidden_dim))) - self.U_c = nn.Parameter(init.orthogonal_(torch.empty(self.hidden_dim, self.hidden_dim))) + self.W_c = nn.Parameter( + init.xavier_uniform_(torch.empty(self.input_dim, self.hidden_dim)) + ) + self.U_c = nn.Parameter( + init.orthogonal_(torch.empty(self.hidden_dim, self.hidden_dim)) + ) self.b_c = nn.Parameter(torch.zeros(self.hidden_dim)) - self.W_o = nn.Parameter(init.xavier_uniform_(torch.empty(self.input_dim, self.hidden_dim))) - self.U_o = nn.Parameter(init.orthogonal_(torch.empty(self.hidden_dim, self.hidden_dim))) + self.W_o = nn.Parameter( + init.xavier_uniform_(torch.empty(self.input_dim, self.hidden_dim)) + ) + self.U_o = nn.Parameter( + init.orthogonal_(torch.empty(self.hidden_dim, self.hidden_dim)) + ) self.b_o = nn.Parameter(torch.zeros(self.hidden_dim)) self.U_a = nn.Parameter(init.orthogonal_(torch.empty(self.freq_dim, 1))) self.b_a = nn.Parameter(torch.zeros(self.hidden_dim)) - self.W_p = nn.Parameter(init.xavier_uniform_(torch.empty(self.hidden_dim, self.output_dim))) + self.W_p = nn.Parameter( + init.xavier_uniform_(torch.empty(self.hidden_dim, self.output_dim)) + ) self.b_p = nn.Parameter(torch.zeros(self.output_dim)) self.activation = nn.Tanh() @@ -101,8 +137,12 @@ class SFM_Model(nn.Module): x_o = torch.matmul(x * B_W[0], self.W_o) + self.b_o i = self.inner_activation(x_i + torch.matmul(h_tm1 * B_U[0], self.U_i)) - ste = self.inner_activation(x_ste + torch.matmul(h_tm1 * B_U[0], self.U_ste)) - fre = self.inner_activation(x_fre + torch.matmul(h_tm1 * B_U[0], self.U_fre)) + ste = self.inner_activation( + x_ste + torch.matmul(h_tm1 * B_U[0], self.U_ste) + ) + fre = self.inner_activation( + x_fre + torch.matmul(h_tm1 * B_U[0], self.U_fre) + ) ste = torch.reshape(ste, (-1, self.hidden_dim, 1)) fre = torch.reshape(fre, (-1, 1, self.freq_dim)) @@ -157,7 +197,16 @@ class SFM_Model(nn.Module): init_state_time = torch.tensor(0).to(self.device) - self.states = [init_state_p, init_state_h, init_state_S_re, init_state_S_im, init_state_time, None, None, None] + self.states = [ + init_state_p, + init_state_h, + init_state_S_re, + init_state_S_im, + init_state_time, + None, + None, + None, + ] def get_constants(self, x): constants = [] @@ -282,7 +331,9 @@ class SFM(Model): elif optimizer.lower() == "gd": self.train_optimizer = optim.SGD(self.sfm_model.parameters(), lr=self.lr) else: - raise NotImplementedError("optimizer {} is not supported!".format(optimizer)) + raise NotImplementedError( + "optimizer {} is not supported!".format(optimizer) + ) self._fitted = False self.sfm_model.to(self.device) @@ -305,8 +356,16 @@ class SFM(Model): if len(indices) - i < self.batch_size: break - feature = torch.from_numpy(x_values[indices[i : i + self.batch_size]]).float().to(self.device) - label = torch.from_numpy(y_values[indices[i : i + self.batch_size]]).float().to(self.device) + feature = ( + torch.from_numpy(x_values[indices[i : i + self.batch_size]]) + .float() + .to(self.device) + ) + label = ( + torch.from_numpy(y_values[indices[i : i + self.batch_size]]) + .float() + .to(self.device) + ) pred = self.sfm_model(feature) loss = self.loss_fn(pred, label) @@ -332,8 +391,16 @@ class SFM(Model): if len(indices) - i < self.batch_size: break - feature = torch.from_numpy(x_train_values[indices[i : i + self.batch_size]]).float().to(self.device) - label = torch.from_numpy(y_train_values[indices[i : i + self.batch_size]]).float().to(self.device) + feature = ( + torch.from_numpy(x_train_values[indices[i : i + self.batch_size]]) + .float() + .to(self.device) + ) + label = ( + torch.from_numpy(y_train_values[indices[i : i + self.batch_size]]) + .float() + .to(self.device) + ) pred = self.sfm_model(feature) loss = self.loss_fn(pred, label) @@ -352,7 +419,9 @@ class SFM(Model): ): df_train, df_valid = dataset.prepare( - ["train", "valid"], col_set=["feature", "label"], data_key=DataHandlerLP.DK_L + ["train", "valid"], + col_set=["feature", "label"], + data_key=DataHandlerLP.DK_L, ) x_train, y_train = df_train["feature"], df_train["label"] x_valid, y_valid = df_valid["feature"], df_valid["label"] @@ -409,7 +478,7 @@ class SFM(Model): mask = torch.isfinite(label) - if self.metric == "" or self.metric == "loss": # use loss + if self.metric == "" or self.metric == "loss": return -self.loss_fn(pred[mask], label[mask]) raise ValueError("unknown metric `%s`" % self.metric) diff --git a/qlib/contrib/model/tabnet.py b/qlib/contrib/model/tabnet.py deleted file mode 100644 index bc13d1f62..000000000 --- a/qlib/contrib/model/tabnet.py +++ /dev/null @@ -1,85 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. - -import numpy as np -import pandas as pd -from pytorch_tabnet.tab_model import TabNetRegressor - -from ...model.base import Model -from ...data.dataset import DatasetH -from ...data.dataset.handler import DataHandlerLP - - -class TabNetModel(Model): - """TabNetModel Model""" - - def __init__( - self, - n_d, - n_a, - n_steps, - gamma, - n_independent, - n_shared, - seed, - momentum, - lambda_sparse, - optimizer_params, - **kwargs - ): - self.model = None - - self.n_d = n_d - self.n_a = n_a - self.n_steps = n_steps - self.gamma = gamma - self.n_independent = n_independent - self.n_shared = n_shared - self.seed = seed - self.momentum = momentum - self.lambda_sparse = lambda_sparse - self.optimizer_params = optimizer_params - - def fit( - self, - dataset: DatasetH, - n_d=8, - n_a=8, - n_steps=3, - gamma=1.3, - n_independent=2, - n_shared=2, - seed=0, - momentum=0.02, - lambda_sparse=1e-3, - optimizer_params={"lr": 2e-3}, - **kwargs - ): - - df_train, df_valid = dataset.prepare( - ["train", "valid"], col_set=["feature", "label"], data_key=DataHandlerLP.DK_L - ) - x_train, y_train = df_train["feature"].values, df_train["label"].values * 100 - x_valid, y_valid = df_valid["feature"].values, df_valid["label"].values * 100 - - self.model = TabNetRegressor( - n_d=self.n_d, - n_a=self.n_a, - n_steps=self.n_steps, - gamma=self.gamma, - n_independent=self.n_independent, - n_shared=self.n_shared, - seed=self.seed, - momentum=self.momentum, - lambda_sparse=self.lambda_sparse, - optimizer_params=self.optimizer_params, - **kwargs - ) - self.model.fit(x_train, y_train, eval_set=[(x_valid, y_valid)]) - - def predict(self, dataset): - if self.model is None: - raise ValueError("model is not fitted yet!") - x_test = dataset.prepare("test", col_set="feature") - test_pred = self.model.predict(x_test.values) - return pd.Series(test_pred.reshape([-1]), index=x_test.index) diff --git a/qlib/contrib/model/xgboost.py b/qlib/contrib/model/xgboost.py index 039fd2c80..32d631189 100755 --- a/qlib/contrib/model/xgboost.py +++ b/qlib/contrib/model/xgboost.py @@ -38,14 +38,18 @@ class XGBModel(Model): ): df_train, df_valid = dataset.prepare( - ["train", "valid"], col_set=["feature", "label"], data_key=DataHandlerLP.DK_L + ["train", "valid"], + col_set=["feature", "label"], + data_key=DataHandlerLP.DK_L, ) x_train, y_train = df_train["feature"], df_train["label"] x_valid, y_valid = df_valid["feature"], df_valid["label"] # Lightgbm need 1D array as its label if y_train.values.ndim == 2 and y_train.values.shape[1] == 1: - y_train_1d, y_valid_1d = np.squeeze(y_train.values), np.squeeze(y_valid.values) + y_train_1d, y_valid_1d = np.squeeze(y_train.values), np.squeeze( + y_valid.values + ) else: raise ValueError("XGBoost doesn't support multi-label training") @@ -68,4 +72,6 @@ class XGBModel(Model): if self.model is None: raise ValueError("model is not fitted yet!") x_test = dataset.prepare("test", col_set="feature") - return pd.Series(self.model.predict(xgb.DMatrix(x_test.values)), index=x_test.index) + return pd.Series( + self.model.predict(xgb.DMatrix(x_test.values)), index=x_test.index + ) From b89c191e6f37443142f05a32a243ea61cd6c3de5 Mon Sep 17 00:00:00 2001 From: lwwang1995 Date: Fri, 27 Nov 2020 22:31:50 +0800 Subject: [PATCH 217/241] Delete workflow code for testing baseline. --- examples/workflow_by_code_alstm.py | 138 ------------------------ examples/workflow_by_code_gats.py | 140 ------------------------ examples/workflow_by_code_gru.py | 144 ------------------------- examples/workflow_by_code_hats.py | 136 ------------------------ examples/workflow_by_code_lstm.py | 144 ------------------------- examples/workflow_by_code_sfm.py | 158 ---------------------------- examples/workflow_by_code_tabnet.py | 142 ------------------------- 7 files changed, 1002 deletions(-) delete mode 100644 examples/workflow_by_code_alstm.py delete mode 100644 examples/workflow_by_code_gats.py delete mode 100644 examples/workflow_by_code_gru.py delete mode 100644 examples/workflow_by_code_hats.py delete mode 100644 examples/workflow_by_code_lstm.py delete mode 100644 examples/workflow_by_code_sfm.py delete mode 100644 examples/workflow_by_code_tabnet.py diff --git a/examples/workflow_by_code_alstm.py b/examples/workflow_by_code_alstm.py deleted file mode 100644 index 8fd9e3565..000000000 --- a/examples/workflow_by_code_alstm.py +++ /dev/null @@ -1,138 +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.strategy.strategy import TopkDropoutStrategy -from qlib.contrib.evaluate import ( - backtest as normal_backtest, - risk_analysis, -) -from qlib.utils import exists_qlib_data -from qlib.utils import init_instance_by_config - -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": "2008-01-01", - "end_time": "2020-08-01", - "fit_start_time": "2008-01-01", - "fit_end_time": "2014-12-31", - "instruments": MARKET, - } - - TRAINER_CONFIG = { - "train_start_time": "2008-01-01", - "train_end_time": "2014-12-31", - "validate_start_time": "2015-01-01", - "validate_end_time": "2016-12-31", - "test_start_time": "2017-01-01", - "test_end_time": "2020-08-01", - } - - task = { - "model": { - "class": "ALSTM", - "module_path": "qlib.contrib.model.pytorch_alstm", - "kwargs": { - "d_feat": 6, - "hidden_size": 64, - "num_layers": 2, - "dropout": 0.0, - "n_epochs": 200, - "lr": 1e-3, - "early_stop": 20, - "batch_size": 800, - "metric": "IC", - "loss": "mse", - "seed": 0, - "GPU": "0", - "rnn_type": "GRU", - }, - }, - "dataset": { - "class": "DatasetH", - "module_path": "qlib.data.dataset", - "kwargs": { - "handler": { - "class": "ALPHA360_Denoise", - "module_path": "qlib.contrib.data.handler", - "kwargs": DATA_HANDLER_CONFIG, - }, - "segments": { - "train": ("2008-01-01", "2014-12-31"), - "valid": ("2015-01-01", "2016-12-31"), - "test": ("2017-01-01", "2020-08-01"), - }, - }, - } - # You shoud record the data in specific sequence - # "record": ['SignalRecord', 'SigAnaRecord', 'PortAnaRecord'], - } - - model = init_instance_by_config(task["model"]) - dataset = init_instance_by_config(task["dataset"]) - model.fit(dataset) - - pred_score = model.predict(dataset) - - # save pred_score to file - pred_score_path = Path("~/tmp/qlib/pred_score.pkl").expanduser() - pred_score_path.parent.mkdir(exist_ok=True, parents=True) - pred_score.to_pickle(pred_score_path) - - ################################### - # backtest - ################################### - STRATEGY_CONFIG = { - "topk": 50, - "n_drop": 5, - } - BACKTEST_CONFIG = { - "verbose": False, - "limit_threshold": 0.095, - "account": 100000000, - "benchmark": BENCHMARK, - "deal_price": "close", - "open_cost": 0.0005, - "close_cost": 0.0015, - "min_cost": 5, - } - - # use default strategy - # custom Strategy, refer to: TODO: Strategy API url - strategy = TopkDropoutStrategy(**STRATEGY_CONFIG) - report_normal, positions_normal = normal_backtest(pred_score, strategy=strategy, **BACKTEST_CONFIG) - - ################################### - # analyze - # If need a more detailed analysis, refer to: examples/train_and_bakctest.ipynb - ################################### - 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 - print(analysis_df) diff --git a/examples/workflow_by_code_gats.py b/examples/workflow_by_code_gats.py deleted file mode 100644 index 20f3ae552..000000000 --- a/examples/workflow_by_code_gats.py +++ /dev/null @@ -1,140 +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.strategy.strategy import TopkDropoutStrategy -from qlib.contrib.evaluate import ( - backtest as normal_backtest, - risk_analysis, -) -from qlib.utils import exists_qlib_data -from qlib.utils import init_instance_by_config - - -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": "2008-01-01", - "end_time": "2020-08-01", - "fit_start_time": "2008-01-01", - "fit_end_time": "2014-12-31", - "instruments": MARKET, - } - - TRAINER_CONFIG = { - "train_start_time": "2008-01-01", - "train_end_time": "2014-12-31", - "validate_start_time": "2015-01-01", - "validate_end_time": "2016-12-31", - "test_start_time": "2017-01-01", - "test_end_time": "2020-08-01", - } - - task = { - "model": { - "class": "GAT", - "module_path": "qlib.contrib.model.pytorch_gats", - "kwargs": { - "d_feat": 6, - "hidden_size": 64, - "num_layers": 2, - "dropout": 0.7, - "n_epochs": 200, - "lr": 1e-4, - "early_stop": 20, - "metric": "loss", - "loss": "mse", - "base_model": "LSTM", - "with_pretrain": True, - "seed": 0, - "GPU": "0", - }, - }, - "dataset": { - "class": "DatasetH", - "module_path": "qlib.data.dataset", - "kwargs": { - "handler": { - "class": "ALPHA360_Denoise", - "module_path": "qlib.contrib.data.handler", - "kwargs": DATA_HANDLER_CONFIG, - }, - "segments": { - "train": ("2008-01-01", "2014-12-31"), - "valid": ("2015-01-01", "2016-12-31"), - "test": ("2017-01-01", "2020-08-01"), - }, - }, - } - # You shoud record the data in specific sequence - # "record": ['SignalRecord', 'SigAnaRecord', 'PortAnaRecord'], - } - - model = init_instance_by_config(task["model"]) - dataset = init_instance_by_config(task["dataset"]) - model.fit(dataset) - - pred_score = model.predict(dataset) - - # save pred_score to file - pred_score_path = Path("~/tmp/qlib/pred_score.pkl").expanduser() - pred_score_path.parent.mkdir(exist_ok=True, parents=True) - pred_score.to_pickle(pred_score_path) - - ################################### - # backtest - ################################### - STRATEGY_CONFIG = { - "topk": 50, - "n_drop": 5, - } - BACKTEST_CONFIG = { - "verbose": False, - "limit_threshold": 0.095, - "account": 100000000, - "benchmark": BENCHMARK, - "deal_price": "close", - "open_cost": 0.0005, - "close_cost": 0.0015, - "min_cost": 5, - } - - # use default strategy - # custom Strategy, refer to: TODO: Strategy API url - strategy = TopkDropoutStrategy(**STRATEGY_CONFIG) - report_normal, positions_normal = normal_backtest(pred_score, strategy=strategy, **BACKTEST_CONFIG) - - ################################### - # analyze - # If need a more detailed analysis, refer to: examples/train_and_bakctest.ipynb - ################################### - 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 - print(analysis_df) diff --git a/examples/workflow_by_code_gru.py b/examples/workflow_by_code_gru.py deleted file mode 100644 index dece520d1..000000000 --- a/examples/workflow_by_code_gru.py +++ /dev/null @@ -1,144 +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.pytorch_gru import GRU -from qlib.contrib.data.handler import ALPHA360_Denoise -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 - -# from qlib.model.learner import train_model -from qlib.utils import init_instance_by_config - -import pickle - -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": "2008-01-01", - "end_time": "2020-08-01", - "fit_start_time": "2008-01-01", - "fit_end_time": "2014-12-31", - "instruments": MARKET, - } - - TRAINER_CONFIG = { - "train_start_time": "2008-01-01", - "train_end_time": "2014-12-31", - "validate_start_time": "2015-01-01", - "validate_end_time": "2016-12-31", - "test_start_time": "2017-01-01", - "test_end_time": "2020-08-01", - } - - task = { - "model": { - "class": "GRU", - "module_path": "qlib.contrib.model.pytorch_gru", - "kwargs": { - "d_feat": 6, - "hidden_size": 64, - "num_layers": 2, - "dropout": 0.0, - "n_epochs": 200, - "lr": 1e-3, - "early_stop": 20, - "batch_size": 800, - "metric": "loss", - "loss": "mse", - "seed": 0, - "GPU": 0, - }, - }, - "dataset": { - "class": "DatasetH", - "module_path": "qlib.data.dataset", - "kwargs": { - "handler": { - "class": "ALPHA360_Denoise", - "module_path": "qlib.contrib.data.handler", - "kwargs": DATA_HANDLER_CONFIG, - }, - "segments": { - "train": ("2008-01-01", "2014-12-31"), - "valid": ("2015-01-01", "2016-12-31"), - "test": ("2017-01-01", "2020-08-01"), - }, - }, - } - # You shoud record the data in specific sequence - # "record": ['SignalRecord', 'SigAnaRecord', 'PortAnaRecord'], - } - - # model = train_model(task) - model = init_instance_by_config(task["model"]) - dataset = init_instance_by_config(task["dataset"]) - model.fit(dataset) - - pred_score = model.predict(dataset) - - # save pred_score to file - pred_score_path = Path("~/tmp/qlib/pred_score.pkl").expanduser() - pred_score_path.parent.mkdir(exist_ok=True, parents=True) - pred_score.to_pickle(pred_score_path) - - ################################### - # backtest - ################################### - STRATEGY_CONFIG = { - "topk": 50, - "n_drop": 5, - } - BACKTEST_CONFIG = { - "verbose": False, - "limit_threshold": 0.095, - "account": 100000000, - "benchmark": BENCHMARK, - "deal_price": "close", - "open_cost": 0.0005, - "close_cost": 0.0015, - "min_cost": 5, - } - - # use default strategy - # custom Strategy, refer to: TODO: Strategy API url - strategy = TopkDropoutStrategy(**STRATEGY_CONFIG) - report_normal, positions_normal = normal_backtest(pred_score, strategy=strategy, **BACKTEST_CONFIG) - - ################################### - # analyze - # If need a more detailed analysis, refer to: examples/train_and_bakctest.ipynb - ################################### - 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 - print(analysis_df) diff --git a/examples/workflow_by_code_hats.py b/examples/workflow_by_code_hats.py deleted file mode 100644 index 64bc860b4..000000000 --- a/examples/workflow_by_code_hats.py +++ /dev/null @@ -1,136 +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.strategy.strategy import TopkDropoutStrategy -from qlib.contrib.evaluate import ( - backtest as normal_backtest, - risk_analysis, -) -from qlib.utils import exists_qlib_data -from qlib.utils import init_instance_by_config - -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": "2008-01-01", - "end_time": "2020-08-01", - "fit_start_time": "2008-01-01", - "fit_end_time": "2014-12-31", - "instruments": MARKET, - } - - TRAINER_CONFIG = { - "train_start_time": "2008-01-01", - "train_end_time": "2014-12-31", - "validate_start_time": "2015-01-01", - "validate_end_time": "2016-12-31", - "test_start_time": "2017-01-01", - "test_end_time": "2020-08-01", - } - - task = { - "model": { - "class": "HATS", - "module_path": "qlib.contrib.model.pytorch_hats", - "kwargs": { - "d_feat": 6, - "hidden_size": 64, - "num_layers": 2, - "dropout": 0.7, - "n_epochs": 200, - "lr": 1e-4, - "early_stop": 20, - "metric": "loss", - "loss": "mse", - "base_model": "LSTM", - "seed": 0, - "GPU": "2", - }, - }, - "dataset": { - "class": "DatasetH", - "module_path": "qlib.data.dataset", - "kwargs": { - "handler": { - "class": "ALPHA360_Denoise", - "module_path": "qlib.contrib.data.handler", - "kwargs": DATA_HANDLER_CONFIG, - }, - "segments": { - "train": ("2008-01-01", "2014-12-31"), - "valid": ("2015-01-01", "2016-12-31"), - "test": ("2017-01-01", "2020-08-01"), - }, - }, - } - # You shoud record the data in specific sequence - # "record": ['SignalRecord', 'SigAnaRecord', 'PortAnaRecord'], - } - - model = init_instance_by_config(task["model"]) - dataset = init_instance_by_config(task["dataset"]) - model.fit(dataset, save_path="benchmarks/HATS/model_hat.pkl") - - pred_score = model.predict(dataset) - - # save pred_score to file - pred_score_path = Path("~/tmp/qlib/pred_score.pkl").expanduser() - pred_score_path.parent.mkdir(exist_ok=True, parents=True) - pred_score.to_pickle(pred_score_path) - - ################################### - # backtest - ################################### - STRATEGY_CONFIG = { - "topk": 50, - "n_drop": 5, - } - BACKTEST_CONFIG = { - "verbose": False, - "limit_threshold": 0.095, - "account": 100000000, - "benchmark": BENCHMARK, - "deal_price": "close", - "open_cost": 0.0005, - "close_cost": 0.0015, - "min_cost": 5, - } - - # use default strategy - # custom Strategy, refer to: TODO: Strategy API url - strategy = TopkDropoutStrategy(**STRATEGY_CONFIG) - report_normal, positions_normal = normal_backtest(pred_score, strategy=strategy, **BACKTEST_CONFIG) - - ################################### - # analyze - # If need a more detailed analysis, refer to: examples/train_and_bakctest.ipynb - ################################### - 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 - print(analysis_df) diff --git a/examples/workflow_by_code_lstm.py b/examples/workflow_by_code_lstm.py deleted file mode 100644 index ee50c9aff..000000000 --- a/examples/workflow_by_code_lstm.py +++ /dev/null @@ -1,144 +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.pytorch_lstm import LSTM -from qlib.contrib.data.handler import ALPHA360_Denoise -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 - -# from qlib.model.learner import train_model -from qlib.utils import init_instance_by_config - -import pickle - -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": "2008-01-01", - "end_time": "2020-08-01", - "fit_start_time": "2008-01-01", - "fit_end_time": "2014-12-31", - "instruments": MARKET, - } - - TRAINER_CONFIG = { - "train_start_time": "2008-01-01", - "train_end_time": "2014-12-31", - "validate_start_time": "2015-01-01", - "validate_end_time": "2016-12-31", - "test_start_time": "2017-01-01", - "test_end_time": "2020-08-01", - } - - task = { - "model": { - "class": "LSTM", - "module_path": "qlib.contrib.model.pytorch_lstm", - "kwargs": { - "d_feat": 6, - "hidden_size": 64, - "num_layers": 2, - "dropout": 0.0, - "n_epochs": 200, - "lr": 1e-3, - "early_stop": 20, - "batch_size": 800, - "metric": "IC", - "loss": "mse", - "seed": 0, - "GPU": 0, - }, - }, - "dataset": { - "class": "DatasetH", - "module_path": "qlib.data.dataset", - "kwargs": { - "handler": { - "class": "ALPHA360_Denoise", - "module_path": "qlib.contrib.data.handler", - "kwargs": DATA_HANDLER_CONFIG, - }, - "segments": { - "train": ("2008-01-01", "2014-12-31"), - "valid": ("2015-01-01", "2016-12-31"), - "test": ("2017-01-01", "2020-08-01"), - }, - }, - } - # You shoud record the data in specific sequence - # "record": ['SignalRecord', 'SigAnaRecord', 'PortAnaRecord'], - } - - # model = train_model(task) - model = init_instance_by_config(task["model"]) - dataset = init_instance_by_config(task["dataset"]) - model.fit(dataset) - - pred_score = model.predict(dataset) - - # save pred_score to file - pred_score_path = Path("~/tmp/qlib/pred_score.pkl").expanduser() - pred_score_path.parent.mkdir(exist_ok=True, parents=True) - pred_score.to_pickle(pred_score_path) - - ################################### - # backtest - ################################### - STRATEGY_CONFIG = { - "topk": 50, - "n_drop": 5, - } - BACKTEST_CONFIG = { - "verbose": False, - "limit_threshold": 0.095, - "account": 100000000, - "benchmark": BENCHMARK, - "deal_price": "close", - "open_cost": 0.0005, - "close_cost": 0.0015, - "min_cost": 5, - } - - # use default strategy - # custom Strategy, refer to: TODO: Strategy API url - strategy = TopkDropoutStrategy(**STRATEGY_CONFIG) - report_normal, positions_normal = normal_backtest(pred_score, strategy=strategy, **BACKTEST_CONFIG) - - ################################### - # analyze - # If need a more detailed analysis, refer to: examples/train_and_bakctest.ipynb - ################################### - 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 - print(analysis_df) diff --git a/examples/workflow_by_code_sfm.py b/examples/workflow_by_code_sfm.py deleted file mode 100644 index 5bd91ded8..000000000 --- a/examples/workflow_by_code_sfm.py +++ /dev/null @@ -1,158 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import sys -from pathlib import Path - -import qlib -import pandas as pd -from qlib.config import REG_CN -from qlib.contrib.model.pytorch_gru import GRU -from qlib.contrib.data.handler import ALPHA360_Denoise -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 -from qlib.utils import init_instance_by_config - -import pickle - -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": "2008-01-01", - "end_time": "2020-08-01", - "fit_start_time": "2008-01-01", - "fit_end_time": "2014-12-31", - "instruments": MARKET, - } - - TRAINER_CONFIG = { - "train_start_time": "2008-01-01", - "train_end_time": "2014-12-31", - "validate_start_time": "2015-01-01", - "validate_end_time": "2016-12-31", - "test_start_time": "2017-01-01", - "test_end_time": "2020-08-01", - } - - task = { - "model": { - "class": "SFM", - "module_path": "qlib.contrib.model.pytorch_sfm", - "kwargs": { - "d_feat": 6, - "hidden_size": 64, - "output_dim": 32, - "freq_dim": 25, - "dropout_W": 0.5, - "dropout_U": 0.5, - "n_epochs": 15, - "lr": 1e-3, - "metric": "", - "batch_size": 1600, - "early_stop": 20, - "eval_steps": 5, - "loss": "mse", - "lr_decay": 0.96, - "lr_decay_steps": 100, - "optimizer": "adam", - "GPU": 3, - "seed": 710, - }, - }, - "dataset": { - "class": "DatasetH", - "module_path": "qlib.data.dataset", - "kwargs": { - "handler": { - "class": "ALPHA360_Denoise", - "module_path": "qlib.contrib.data.handler", - "kwargs": DATA_HANDLER_CONFIG, - }, - "segments": { - "train": ("2008-01-01", "2014-12-31"), - "valid": ("2015-01-01", "2016-12-31"), - "test": ("2017-01-01", "2020-08-01"), - }, - }, - } - # You shoud record the data in specific sequence - # "record": ['SignalRecord', 'SigAnaRecord', 'PortAnaRecord'], - } - - # model = train_model(task) - model = init_instance_by_config(task["model"]) - dataset = init_instance_by_config(task["dataset"]) - model.fit(dataset) - - pred_score = model.predict(dataset) - - # save pred_score to file - pred_score_path = Path("~/tmp/qlib/pred_score.pkl").expanduser() - pred_score_path.parent.mkdir(exist_ok=True, parents=True) - pred_score.to_pickle(pred_score_path) - - ################################### - # backtest - ################################### - STRATEGY_CONFIG = { - "topk": 50, - "n_drop": 5, - } - BACKTEST_CONFIG = { - "verbose": False, - "limit_threshold": 0.095, - "account": 100000000, - "benchmark": BENCHMARK, - "deal_price": "close", - "open_cost": 0.0005, - "close_cost": 0.0015, - "min_cost": 5, - } - - # use default strategy - # custom Strategy, refer to: TODO: Strategy API url - strategy = TopkDropoutStrategy(**STRATEGY_CONFIG) - report_normal, positions_normal = normal_backtest(pred_score, strategy=strategy, **BACKTEST_CONFIG) - - ################################### - # analyze - # If need a more detailed analysis, refer to: examples/train_and_bakctest.ipynb - ################################### - 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 - print(analysis_df) diff --git a/examples/workflow_by_code_tabnet.py b/examples/workflow_by_code_tabnet.py deleted file mode 100644 index 3778b9d59..000000000 --- a/examples/workflow_by_code_tabnet.py +++ /dev/null @@ -1,142 +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.tabnet import TabNetModel -from qlib.contrib.data.handler import ALPHA360_Denoise -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 - -# from qlib.model.learner import train_model -from qlib.utils import init_instance_by_config - -import pickle - -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": "2008-01-01", - "end_time": "2020-08-01", - "fit_start_time": "2008-01-01", - "fit_end_time": "2014-12-31", - "instruments": MARKET, - } - - TRAINER_CONFIG = { - "train_start_time": "2008-01-01", - "train_end_time": "2014-12-31", - "validate_start_time": "2015-01-01", - "validate_end_time": "2016-12-31", - "test_start_time": "2017-01-01", - "test_end_time": "2020-08-01", - } - - task = { - "model": { - "class": "TabNetModel", - "module_path": "qlib.contrib.model.tabnet", - "kwargs": { - "n_d": 8, - "n_a": 8, - "n_steps": 3, - "gamma": 1.3, - "n_independent": 2, - "n_shared": 2, - "seed": 0, - "momentum": 0.02, - "lambda_sparse": 1e-3, - "optimizer_params": {"lr": 2e-3}, - }, - }, - "dataset": { - "class": "DatasetH", - "module_path": "qlib.data.dataset", - "kwargs": { - "handler": { - "class": "ALPHA360_Denoise", - "module_path": "qlib.contrib.data.handler", - "kwargs": DATA_HANDLER_CONFIG, - }, - "segments": { - "train": ("2008-01-01", "2014-12-31"), - "valid": ("2015-01-01", "2016-12-31"), - "test": ("2017-01-01", "2020-08-01"), - }, - }, - } - # You shoud record the data in specific sequence - # "record": ['SignalRecord', 'SigAnaRecord', 'PortAnaRecord'], - } - - # model = train_model(task) - model = init_instance_by_config(task["model"]) - dataset = init_instance_by_config(task["dataset"]) - model.fit(dataset) - - pred_score = model.predict(dataset) - - # save pred_score to file - pred_score_path = Path("~/tmp/qlib/pred_score.pkl").expanduser() - pred_score_path.parent.mkdir(exist_ok=True, parents=True) - pred_score.to_pickle(pred_score_path) - - ################################### - # backtest - ################################### - STRATEGY_CONFIG = { - "topk": 50, - "n_drop": 5, - } - BACKTEST_CONFIG = { - "verbose": False, - "limit_threshold": 0.095, - "account": 100000000, - "benchmark": BENCHMARK, - "deal_price": "close", - "open_cost": 0.0005, - "close_cost": 0.0015, - "min_cost": 5, - } - - # use default strategy - # custom Strategy, refer to: TODO: Strategy API url - strategy = TopkDropoutStrategy(**STRATEGY_CONFIG) - report_normal, positions_normal = normal_backtest(pred_score, strategy=strategy, **BACKTEST_CONFIG) - - ################################### - # analyze - # If need a more detailed analysis, refer to: examples/train_and_bakctest.ipynb - ################################### - 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 - print(analysis_df) From c5a3b74a96ce319c1f2e1cd17f9009b7dd4825dd Mon Sep 17 00:00:00 2001 From: zhupr Date: Fri, 27 Nov 2020 22:33:22 +0800 Subject: [PATCH 218/241] Fix the target repository of get_data.py in google.colab --- examples/workflow_by_code.ipynb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/workflow_by_code.ipynb b/examples/workflow_by_code.ipynb index f8370789b..81dbf3e31 100644 --- a/examples/workflow_by_code.ipynb +++ b/examples/workflow_by_code.ipynb @@ -35,7 +35,7 @@ " scripts_dir = Path(\"~/tmp/qlib_code/scripts\").expanduser().resolve()\n", " scripts_dir.mkdir(parents=True, exist_ok=True)\n", " import requests\n", - " with requests.get(\"https://raw.githubusercontent.com/you-n-g/qlib/main/scripts/get_data.py\") as resp:\n", + " with requests.get(\"https://raw.githubusercontent.com/microsoft/qlib/main/scripts/get_data.py\") as resp:\n", " with open(scripts_dir.joinpath(\"get_data.py\"), \"wb\") as fp:\n", " fp.write(resp.content)" ] From 1353e81b5b8a277945959e3418d41ab736e19198 Mon Sep 17 00:00:00 2001 From: lwwang1995 Date: Fri, 27 Nov 2020 22:44:28 +0800 Subject: [PATCH 219/241] Fix code with block. --- qlib/contrib/model/catboost_model.py | 4 +- qlib/contrib/model/pytorch_alstm.py | 32 +++-------- qlib/contrib/model/pytorch_gats.py | 22 ++------ qlib/contrib/model/pytorch_gru.py | 16 ++---- qlib/contrib/model/pytorch_lstm.py | 16 ++---- qlib/contrib/model/pytorch_sfm.py | 80 +++++++--------------------- qlib/contrib/model/xgboost.py | 8 +-- 7 files changed, 42 insertions(+), 136 deletions(-) diff --git a/qlib/contrib/model/catboost_model.py b/qlib/contrib/model/catboost_model.py index 43a141418..01830d1b5 100644 --- a/qlib/contrib/model/catboost_model.py +++ b/qlib/contrib/model/catboost_model.py @@ -50,9 +50,7 @@ class CatBoostModel(Model): # CatBoost needs 1D array as its label if y_train.values.ndim == 2 and y_train.values.shape[1] == 1: - y_train_1d, y_valid_1d = np.squeeze(y_train.values), np.squeeze( - y_valid.values - ) + y_train_1d, y_valid_1d = np.squeeze(y_train.values), np.squeeze(y_valid.values) else: raise ValueError("CatBoost doesn't support multi-label training") diff --git a/qlib/contrib/model/pytorch_alstm.py b/qlib/contrib/model/pytorch_alstm.py index 227772499..40c2f8226 100644 --- a/qlib/contrib/model/pytorch_alstm.py +++ b/qlib/contrib/model/pytorch_alstm.py @@ -124,9 +124,7 @@ class ALSTM(Model): elif optimizer.lower() == "gd": self.train_optimizer = optim.SGD(self.ALSTM_model.parameters(), lr=self.lr) else: - raise NotImplementedError( - "optimizer {} is not supported!".format(optimizer) - ) + raise NotImplementedError("optimizer {} is not supported!".format(optimizer)) self._fitted = False if self.use_gpu: @@ -171,12 +169,8 @@ class ALSTM(Model): if len(indices) - i < self.batch_size: break - feature = torch.from_numpy( - x_train_values[indices[i : i + self.batch_size]] - ).float() - label = torch.from_numpy( - y_train_values[indices[i : i + self.batch_size]] - ).float() + feature = torch.from_numpy(x_train_values[indices[i : i + self.batch_size]]).float() + label = torch.from_numpy(y_train_values[indices[i : i + self.batch_size]]).float() if self.use_gpu: feature = feature.cuda() @@ -208,9 +202,7 @@ class ALSTM(Model): if len(indices) - i < self.batch_size: break - feature = torch.from_numpy( - x_values[indices[i : i + self.batch_size]] - ).float() + feature = torch.from_numpy(x_values[indices[i : i + self.batch_size]]).float() label = torch.from_numpy(y_values[indices[i : i + self.batch_size]]).float() if self.use_gpu: @@ -320,9 +312,7 @@ class ALSTM(Model): class ALSTMModel(nn.Module): - def __init__( - self, d_feat=6, hidden_size=64, num_layers=2, dropout=0.0, rnn_type="GRU" - ): + def __init__(self, d_feat=6, hidden_size=64, num_layers=2, dropout=0.0, rnn_type="GRU"): super().__init__() self.hid_size = hidden_size self.input_size = d_feat @@ -337,9 +327,7 @@ class ALSTMModel(nn.Module): except: raise ValueError("unknown rnn_type `%s`" % self.rnn_type) self.net = nn.Sequential() - self.net.add_module( - "fc_in", nn.Linear(in_features=self.input_size, out_features=self.hid_size) - ) + self.net.add_module("fc_in", nn.Linear(in_features=self.input_size, out_features=self.hid_size)) self.net.add_module("act", nn.Tanh()) self.rnn = klass( input_size=self.hid_size, @@ -365,12 +353,8 @@ class ALSTMModel(nn.Module): def forward(self, inputs): # inputs: [batch_size, input_size*input_day] inputs = inputs.view(len(inputs), self.input_size, -1) - inputs = inputs.permute( - 0, 2, 1 - ) # [batch, input_size, seq_len] -> [batch, seq_len, input_size] - rnn_out, _ = self.rnn( - self.net(inputs) - ) # [batch, seq_len, num_directions * hidden_size] + inputs = inputs.permute(0, 2, 1) # [batch, input_size, seq_len] -> [batch, seq_len, input_size] + rnn_out, _ = self.rnn(self.net(inputs)) # [batch, seq_len, num_directions * hidden_size] attention_score = self.att_net(rnn_out) # [batch, seq_len, 1] out_att = torch.mul(rnn_out, attention_score) out_att = torch.sum(out_att, dim=1) diff --git a/qlib/contrib/model/pytorch_gats.py b/qlib/contrib/model/pytorch_gats.py index 226204fe7..61a0ef714 100644 --- a/qlib/contrib/model/pytorch_gats.py +++ b/qlib/contrib/model/pytorch_gats.py @@ -126,9 +126,7 @@ class GATs(Model): elif optimizer.lower() == "gd": self.train_optimizer = optim.SGD(self.GAT_model.parameters(), lr=self.lr) else: - raise NotImplementedError( - "optimizer {} is not supported!".format(optimizer) - ) + raise NotImplementedError("optimizer {} is not supported!".format(optimizer)) self._fitted = False if self.use_gpu: @@ -258,22 +256,14 @@ class GATs(Model): self.logger.info("Loading pretrained model...") if self.base_model == "LSTM": pretrained_model = LSTMModel() - pretrained_model.load_state_dict( - torch.load("benchmarks/LSTM/model_lstm_csi300.pkl") - ) + pretrained_model.load_state_dict(torch.load("benchmarks/LSTM/model_lstm_csi300.pkl")) elif self.base_model == "GRU": pretrained_model = GRUModel() - pretrained_model.load_state_dict( - torch.load("benchmarks/GRU/model_gru_csi300.pkl") - ) + pretrained_model.load_state_dict(torch.load("benchmarks/GRU/model_gru_csi300.pkl")) model_dict = self.GAT_model.state_dict() - pretrained_dict = { - k: v - for k, v in pretrained_model.state_dict().items() - if k in model_dict - } + pretrained_dict = {k: v for k, v in pretrained_model.state_dict().items() if k in model_dict} model_dict.update(pretrained_dict) self.GAT_model.load_state_dict(model_dict) self.logger.info("Loading pretrained model Done...") @@ -343,9 +333,7 @@ class GATs(Model): class GATModel(nn.Module): - def __init__( - self, d_feat=6, hidden_size=64, num_layers=2, dropout=0.0, base_model="GRU" - ): + def __init__(self, d_feat=6, hidden_size=64, num_layers=2, dropout=0.0, base_model="GRU"): super().__init__() if base_model == "GRU": diff --git a/qlib/contrib/model/pytorch_gru.py b/qlib/contrib/model/pytorch_gru.py index 935716bcc..5daf4707e 100755 --- a/qlib/contrib/model/pytorch_gru.py +++ b/qlib/contrib/model/pytorch_gru.py @@ -124,9 +124,7 @@ class GRU(Model): elif optimizer.lower() == "gd": self.train_optimizer = optim.SGD(self.gru_model.parameters(), lr=self.lr) else: - raise NotImplementedError( - "optimizer {} is not supported!".format(optimizer) - ) + raise NotImplementedError("optimizer {} is not supported!".format(optimizer)) self._fitted = False if self.use_gpu: @@ -171,12 +169,8 @@ class GRU(Model): if len(indices) - i < self.batch_size: break - feature = torch.from_numpy( - x_train_values[indices[i : i + self.batch_size]] - ).float() - label = torch.from_numpy( - y_train_values[indices[i : i + self.batch_size]] - ).float() + feature = torch.from_numpy(x_train_values[indices[i : i + self.batch_size]]).float() + label = torch.from_numpy(y_train_values[indices[i : i + self.batch_size]]).float() if self.use_gpu: feature = feature.cuda() @@ -208,9 +202,7 @@ class GRU(Model): if len(indices) - i < self.batch_size: break - feature = torch.from_numpy( - x_values[indices[i : i + self.batch_size]] - ).float() + feature = torch.from_numpy(x_values[indices[i : i + self.batch_size]]).float() label = torch.from_numpy(y_values[indices[i : i + self.batch_size]]).float() if self.use_gpu: diff --git a/qlib/contrib/model/pytorch_lstm.py b/qlib/contrib/model/pytorch_lstm.py index 1d1c0c986..eef1680ec 100755 --- a/qlib/contrib/model/pytorch_lstm.py +++ b/qlib/contrib/model/pytorch_lstm.py @@ -124,9 +124,7 @@ class LSTM(Model): elif optimizer.lower() == "gd": self.train_optimizer = optim.SGD(self.lstm_model.parameters(), lr=self.lr) else: - raise NotImplementedError( - "optimizer {} is not supported!".format(optimizer) - ) + raise NotImplementedError("optimizer {} is not supported!".format(optimizer)) self._fitted = False if self.use_gpu: @@ -171,12 +169,8 @@ class LSTM(Model): if len(indices) - i < self.batch_size: break - feature = torch.from_numpy( - x_train_values[indices[i : i + self.batch_size]] - ).float() - label = torch.from_numpy( - y_train_values[indices[i : i + self.batch_size]] - ).float() + feature = torch.from_numpy(x_train_values[indices[i : i + self.batch_size]]).float() + label = torch.from_numpy(y_train_values[indices[i : i + self.batch_size]]).float() if self.use_gpu: feature = feature.cuda() @@ -208,9 +202,7 @@ class LSTM(Model): if len(indices) - i < self.batch_size: break - feature = torch.from_numpy( - x_values[indices[i : i + self.batch_size]] - ).float() + feature = torch.from_numpy(x_values[indices[i : i + self.batch_size]]).float() label = torch.from_numpy(y_values[indices[i : i + self.batch_size]]).float() if self.use_gpu: diff --git a/qlib/contrib/model/pytorch_sfm.py b/qlib/contrib/model/pytorch_sfm.py index bebc408a8..8fddd1612 100644 --- a/qlib/contrib/model/pytorch_sfm.py +++ b/qlib/contrib/model/pytorch_sfm.py @@ -56,52 +56,30 @@ class SFM_Model(nn.Module): self.hidden_dim = hidden_size self.device = device - self.W_i = nn.Parameter( - init.xavier_uniform_(torch.empty((self.input_dim, self.hidden_dim))) - ) - self.U_i = nn.Parameter( - init.orthogonal_(torch.empty(self.hidden_dim, self.hidden_dim)) - ) + self.W_i = nn.Parameter(init.xavier_uniform_(torch.empty((self.input_dim, self.hidden_dim)))) + self.U_i = nn.Parameter(init.orthogonal_(torch.empty(self.hidden_dim, self.hidden_dim))) self.b_i = nn.Parameter(torch.zeros(self.hidden_dim)) - self.W_ste = nn.Parameter( - init.xavier_uniform_(torch.empty(self.input_dim, self.hidden_dim)) - ) - self.U_ste = nn.Parameter( - init.orthogonal_(torch.empty(self.hidden_dim, self.hidden_dim)) - ) + self.W_ste = nn.Parameter(init.xavier_uniform_(torch.empty(self.input_dim, self.hidden_dim))) + self.U_ste = nn.Parameter(init.orthogonal_(torch.empty(self.hidden_dim, self.hidden_dim))) self.b_ste = nn.Parameter(torch.ones(self.hidden_dim)) - self.W_fre = nn.Parameter( - init.xavier_uniform_(torch.empty(self.input_dim, self.freq_dim)) - ) - self.U_fre = nn.Parameter( - init.orthogonal_(torch.empty(self.hidden_dim, self.freq_dim)) - ) + self.W_fre = nn.Parameter(init.xavier_uniform_(torch.empty(self.input_dim, self.freq_dim))) + self.U_fre = nn.Parameter(init.orthogonal_(torch.empty(self.hidden_dim, self.freq_dim))) self.b_fre = nn.Parameter(torch.ones(self.freq_dim)) - self.W_c = nn.Parameter( - init.xavier_uniform_(torch.empty(self.input_dim, self.hidden_dim)) - ) - self.U_c = nn.Parameter( - init.orthogonal_(torch.empty(self.hidden_dim, self.hidden_dim)) - ) + self.W_c = nn.Parameter(init.xavier_uniform_(torch.empty(self.input_dim, self.hidden_dim))) + self.U_c = nn.Parameter(init.orthogonal_(torch.empty(self.hidden_dim, self.hidden_dim))) self.b_c = nn.Parameter(torch.zeros(self.hidden_dim)) - self.W_o = nn.Parameter( - init.xavier_uniform_(torch.empty(self.input_dim, self.hidden_dim)) - ) - self.U_o = nn.Parameter( - init.orthogonal_(torch.empty(self.hidden_dim, self.hidden_dim)) - ) + self.W_o = nn.Parameter(init.xavier_uniform_(torch.empty(self.input_dim, self.hidden_dim))) + self.U_o = nn.Parameter(init.orthogonal_(torch.empty(self.hidden_dim, self.hidden_dim))) self.b_o = nn.Parameter(torch.zeros(self.hidden_dim)) self.U_a = nn.Parameter(init.orthogonal_(torch.empty(self.freq_dim, 1))) self.b_a = nn.Parameter(torch.zeros(self.hidden_dim)) - self.W_p = nn.Parameter( - init.xavier_uniform_(torch.empty(self.hidden_dim, self.output_dim)) - ) + self.W_p = nn.Parameter(init.xavier_uniform_(torch.empty(self.hidden_dim, self.output_dim))) self.b_p = nn.Parameter(torch.zeros(self.output_dim)) self.activation = nn.Tanh() @@ -137,12 +115,8 @@ class SFM_Model(nn.Module): x_o = torch.matmul(x * B_W[0], self.W_o) + self.b_o i = self.inner_activation(x_i + torch.matmul(h_tm1 * B_U[0], self.U_i)) - ste = self.inner_activation( - x_ste + torch.matmul(h_tm1 * B_U[0], self.U_ste) - ) - fre = self.inner_activation( - x_fre + torch.matmul(h_tm1 * B_U[0], self.U_fre) - ) + ste = self.inner_activation(x_ste + torch.matmul(h_tm1 * B_U[0], self.U_ste)) + fre = self.inner_activation(x_fre + torch.matmul(h_tm1 * B_U[0], self.U_fre)) ste = torch.reshape(ste, (-1, self.hidden_dim, 1)) fre = torch.reshape(fre, (-1, 1, self.freq_dim)) @@ -331,9 +305,7 @@ class SFM(Model): elif optimizer.lower() == "gd": self.train_optimizer = optim.SGD(self.sfm_model.parameters(), lr=self.lr) else: - raise NotImplementedError( - "optimizer {} is not supported!".format(optimizer) - ) + raise NotImplementedError("optimizer {} is not supported!".format(optimizer)) self._fitted = False self.sfm_model.to(self.device) @@ -356,16 +328,8 @@ class SFM(Model): if len(indices) - i < self.batch_size: break - feature = ( - torch.from_numpy(x_values[indices[i : i + self.batch_size]]) - .float() - .to(self.device) - ) - label = ( - torch.from_numpy(y_values[indices[i : i + self.batch_size]]) - .float() - .to(self.device) - ) + feature = torch.from_numpy(x_values[indices[i : i + self.batch_size]]).float().to(self.device) + label = torch.from_numpy(y_values[indices[i : i + self.batch_size]]).float().to(self.device) pred = self.sfm_model(feature) loss = self.loss_fn(pred, label) @@ -391,16 +355,8 @@ class SFM(Model): if len(indices) - i < self.batch_size: break - feature = ( - torch.from_numpy(x_train_values[indices[i : i + self.batch_size]]) - .float() - .to(self.device) - ) - label = ( - torch.from_numpy(y_train_values[indices[i : i + self.batch_size]]) - .float() - .to(self.device) - ) + feature = torch.from_numpy(x_train_values[indices[i : i + self.batch_size]]).float().to(self.device) + label = torch.from_numpy(y_train_values[indices[i : i + self.batch_size]]).float().to(self.device) pred = self.sfm_model(feature) loss = self.loss_fn(pred, label) diff --git a/qlib/contrib/model/xgboost.py b/qlib/contrib/model/xgboost.py index 32d631189..c9e45d4ac 100755 --- a/qlib/contrib/model/xgboost.py +++ b/qlib/contrib/model/xgboost.py @@ -47,9 +47,7 @@ class XGBModel(Model): # Lightgbm need 1D array as its label if y_train.values.ndim == 2 and y_train.values.shape[1] == 1: - y_train_1d, y_valid_1d = np.squeeze(y_train.values), np.squeeze( - y_valid.values - ) + y_train_1d, y_valid_1d = np.squeeze(y_train.values), np.squeeze(y_valid.values) else: raise ValueError("XGBoost doesn't support multi-label training") @@ -72,6 +70,4 @@ class XGBModel(Model): if self.model is None: raise ValueError("model is not fitted yet!") x_test = dataset.prepare("test", col_set="feature") - return pd.Series( - self.model.predict(xgb.DMatrix(x_test.values)), index=x_test.index - ) + return pd.Series(self.model.predict(xgb.DMatrix(x_test.values)), index=x_test.index) From c52b84b3fe7423125fb8373ea78d4f9bf7f8734c Mon Sep 17 00:00:00 2001 From: lwwang1995 Date: Fri, 27 Nov 2020 22:50:07 +0800 Subject: [PATCH 220/241] Fix sfm readme. --- examples/benchmarks/SFM/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/benchmarks/SFM/README.md b/examples/benchmarks/SFM/README.md index 06ca50485..eb1c8b157 100644 --- a/examples/benchmarks/SFM/README.md +++ b/examples/benchmarks/SFM/README.md @@ -1,4 +1,4 @@ # State-Frequency-Memory -- State Frequency Memory (SFM) is a novel recurrent network that uses Discrete Fourier Transform (DFT) to decompose the hidden states of memory cells and capture the multi-frequency trading patterns from past market data to make stock price predictions. -- The code used in Qlib is a pyTorch implementation of SFM (Zhang, L., Aggarwal, C., & Qi, G. J. (2017,)). -- Paper: Stock Price Prediction via Discovering Multi-Frequency Trading Patterns. https://www.cs.ucf.edu/~gqi/publications/kdd2017_stock.pdf. \ No newline at end of file +- State Frequency Memory (SFM) is a novel recurrent network that uses Discrete Fourier Transform to decompose the hidden states of memory cells and capture the multi-frequency trading patterns from past market data to make stock price predictions. +- The code used in Qlib is a pyTorch implementation of SFM. +- Paper: Stock Price Prediction via Discovering Multi-Frequency Trading Patterns. [https://www.cs.ucf.edu/~gqi/publications/kdd2017_stock.pdf.](https://www.cs.ucf.edu/~gqi/publications/kdd2017_stock.pdf.) \ No newline at end of file From d7fc90ddcd623640546268deaad4495405036a20 Mon Sep 17 00:00:00 2001 From: lwwang1995 Date: Fri, 27 Nov 2020 23:36:29 +0800 Subject: [PATCH 221/241] Update readme. --- examples/benchmarks/ALSTM/README.md | 4 +--- examples/benchmarks/SFM/README.md | 1 - 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/examples/benchmarks/ALSTM/README.md b/examples/benchmarks/ALSTM/README.md index cd9dd3493..1b749bd80 100644 --- a/examples/benchmarks/ALSTM/README.md +++ b/examples/benchmarks/ALSTM/README.md @@ -2,9 +2,7 @@ - ALSTM contains a temporal attentive aggregation layer based on normal LSTM. -- The code used in Qlib is a pyTorch implementation of Code: https://github.com/fulifeng/Adv-ALSTM - - Paper: A dual-stage attention-based recurrent neural network for time series prediction. - https://www.ijcai.org/Proceedings/2017/0366.pdf + [https://www.ijcai.org/Proceedings/2017/0366.pdf](https://www.ijcai.org/Proceedings/2017/0366.pdf) diff --git a/examples/benchmarks/SFM/README.md b/examples/benchmarks/SFM/README.md index eb1c8b157..5f74c15d2 100644 --- a/examples/benchmarks/SFM/README.md +++ b/examples/benchmarks/SFM/README.md @@ -1,4 +1,3 @@ # State-Frequency-Memory - State Frequency Memory (SFM) is a novel recurrent network that uses Discrete Fourier Transform to decompose the hidden states of memory cells and capture the multi-frequency trading patterns from past market data to make stock price predictions. -- The code used in Qlib is a pyTorch implementation of SFM. - Paper: Stock Price Prediction via Discovering Multi-Frequency Trading Patterns. [https://www.cs.ucf.edu/~gqi/publications/kdd2017_stock.pdf.](https://www.cs.ucf.edu/~gqi/publications/kdd2017_stock.pdf.) \ No newline at end of file From 47cbfdc50cf4ea8505f988c32829004fe2088078 Mon Sep 17 00:00:00 2001 From: lwwang1995 Date: Sat, 28 Nov 2020 00:10:09 +0800 Subject: [PATCH 222/241] Update --- examples/benchmarks/GATs/workflow_config_gats.yaml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/examples/benchmarks/GATs/workflow_config_gats.yaml b/examples/benchmarks/GATs/workflow_config_gats.yaml index 7212e0ee2..c38b4b312 100644 --- a/examples/benchmarks/GATs/workflow_config_gats.yaml +++ b/examples/benchmarks/GATs/workflow_config_gats.yaml @@ -40,19 +40,19 @@ port_analysis_config: &port_analysis_config min_cost: 5 task: model: - class: GAT_Classic - module_path: qlib.contrib.model.pytorch_gats_classic + class: GATs + module_path: qlib.contrib.model.pytorch_gats kwargs: d_feat: 6 hidden_size: 64 num_layers: 2 - dropout: 0.0 + dropout: 0.7 n_epochs: 200 - lr: 1e-3 + lr: 1e-4 early_stop: 20 metric: loss loss: mse - base_model: GRU + base_model: LSTM seed: 0 GPU: 0 dataset: From 6fc4ff0a62b32e2526cad69eab7e5556d4f24928 Mon Sep 17 00:00:00 2001 From: zhupr Date: Sat, 28 Nov 2020 00:36:23 +0800 Subject: [PATCH 223/241] Fix yahoo collector --- scripts/data_collector/yahoo/collector.py | 69 +++++++++++++++++------ 1 file changed, 52 insertions(+), 17 deletions(-) diff --git a/scripts/data_collector/yahoo/collector.py b/scripts/data_collector/yahoo/collector.py index 69c7f8f15..0d41251f1 100644 --- a/scripts/data_collector/yahoo/collector.py +++ b/scripts/data_collector/yahoo/collector.py @@ -44,6 +44,7 @@ class YahooCollector: delay=0, check_data_length: bool = False, limit_nums: int = None, + show_1m_logging: bool = False, ): """ @@ -67,10 +68,13 @@ class YahooCollector: check data length, by default False limit_nums: int using for debug, by default None + show_1m_logging: bool + show 1m logging, by default False; if True, there may be many warning logs """ self.save_dir = Path(save_dir).expanduser().resolve() self.save_dir.mkdir(parents=True, exist_ok=True) self._delay = delay + self._show_1m_logging = show_1m_logging self.stock_list = sorted(set(self.get_stock_list())) if limit_nums is not None: try: @@ -83,7 +87,7 @@ class YahooCollector: self._interval = interval self._check_small_data = check_data_length self._start_datetime = pd.Timestamp(str(start)) if start else self.START_DATETIME - self._end_datetime = pd.Timestamp(str(end)) if end else self.END_DATETIME + self._end_datetime = min(pd.Timestamp(str(end)) if end else self.END_DATETIME, self.END_DATETIME) if self._interval == "1m": self._start_datetime = max(self._start_datetime, self.HIGH_FREQ_START_DATETIME) elif self._interval == "1d": @@ -91,8 +95,12 @@ class YahooCollector: else: raise ValueError(f"interval error: {self._interval}") + # using for 1m + self._next_datetime = self.convert_datetime(self._start_datetime.date() + pd.Timedelta(days=1)) + self._latest_datetime = self.convert_datetime(self._end_datetime.date()) + self._start_datetime = self.convert_datetime(self._start_datetime) - self._end_datetime = self.convert_datetime(min(self._end_datetime, self.END_DATETIME)) + self._end_datetime = self.convert_datetime(self._end_datetime) @property @abc.abstractmethod @@ -100,20 +108,24 @@ class YahooCollector: # daily, one year: 252 / 4 # us 1min, a week: 6.5 * 60 * 5 # cn 1min, a week: 4 * 60 * 5 - raise NotImplementedError("rewirte min_numbers_trading") + raise NotImplementedError("rewrite min_numbers_trading") @abc.abstractmethod def get_stock_list(self): - raise NotImplementedError("rewirte get_stock_list") + raise NotImplementedError("rewrite get_stock_list") @property - @abc.abstractclassmethod + @abc.abstractmethod def _timezone(self): raise NotImplementedError("rewrite get_timezone") - def convert_datetime(self, dt: pd.Timestamp): - dt = pd.Timestamp(dt, tz=self._timezone).timestamp() - return pd.Timestamp(dt, tz=tzlocal(), unit="s") + def convert_datetime(self, dt: [pd.Timestamp, datetime.date, str]): + try: + dt = pd.Timestamp(dt, tz=self._timezone).timestamp() + dt = pd.Timestamp(dt, tz=tzlocal(), unit="s") + except ValueError as e: + pass + return dt def _sleep(self): time.sleep(self._delay) @@ -136,7 +148,7 @@ class YahooCollector: df["symbol"] = symbol if stock_path.exists(): with stock_path.open("a") as fp: - df.to_csv(fp, index=False, header=None) + df.to_csv(fp, index=False, header=False) else: with stock_path.open("w") as fp: df.to_csv(fp, index=False) @@ -155,34 +167,47 @@ class YahooCollector: def _get_from_remote(self, symbol): def _get_simple(start_, end_): self._sleep() + error_msg = f"{symbol}-{self._interval}-{start_}-{end_}" + + def _show_logging_func(): + if self._interval == "1m" and self._show_1m_logging: + logger.warning(f"{error_msg}:{_resp}") + try: _resp = Ticker(symbol, asynchronous=False).history(interval=self._interval, start=start_, end=end_) if isinstance(_resp, pd.DataFrame): return _resp.reset_index() + elif isinstance(_resp, dict): + _temp_data = _resp.get(symbol, {}) + if isinstance(_temp_data, str) or ( + isinstance(_resp, dict) and _temp_data.get("indicators", {}).get("quote", None) is None + ): + _show_logging_func() else: - logger.warning(f"{symbol}-{self._interval}-{start_}-{end_}:{_resp}") + _show_logging_func() except Exception as e: - logger.warning(f"{symbol}-{self._interval}-{start_}-{end_}:{e}") + logger.warning(f"{error_msg}:{e}") _result = None if self._interval == "1d": _result = _get_simple(self._start_datetime, self._end_datetime) elif self._interval == "1m": - _start_date = self._start_datetime.date() + pd.Timedelta(days=1) - _end_date = self._end_datetime.date() - if _start_date >= _end_date: + if self._next_datetime >= self._latest_datetime: _result = _get_simple(self._start_datetime, self._end_datetime) else: _res = [] def _get_multi(start_, end_): _resp = _get_simple(start_, end_) - if _resp is not None: + if _resp is not None and not _resp.empty: _res.append(_resp) - for _s, _e in ((self._start_datetime, _start_date), (_end_date, self._end_datetime)): + for _s, _e in ( + (self._start_datetime, self._next_datetime), + (self._latest_datetime, self._end_datetime), + ): _get_multi(_s, _e) - for _start in pd.date_range(_start_date, _end_date, closed="left"): + for _start in pd.date_range(self._next_datetime, self._latest_datetime, closed="left"): _end = _start + pd.Timedelta(days=1) self._sleep() _get_multi(_start, _end) @@ -472,6 +497,7 @@ class Run: interval="1d", check_data_length=False, limit_nums=None, + show_1m_logging=False, ): """download data from Internet @@ -491,6 +517,9 @@ class Run: check data length, by default False limit_nums: int using for debug, by default None + show_1m_logging: bool + show 1m logging, by default False; if True, there may be many warning logs + Examples --------- # get daily data @@ -510,6 +539,7 @@ class Run: interval=interval, check_data_length=check_data_length, limit_nums=limit_nums, + show_1m_logging=show_1m_logging, ).collector_data() def normalize_data(self): @@ -531,6 +561,7 @@ class Run: interval="1d", check_data_length=False, limit_nums=None, + show_1m_logging=False, ): """download -> normalize @@ -550,6 +581,9 @@ class Run: check data length, by default False limit_nums: int using for debug, by default None + show_1m_logging: bool + show 1m logging, by default False; if True, there may be many warning logs + Examples ------- python collector.py collector_data --source_dir ~/.qlib/stock_data/source --normalize_dir ~/.qlib/stock_data/normalize --region CN --start 2020-11-01 --end 2020-11-10 --delay 0.1 --interval 1d @@ -562,6 +596,7 @@ class Run: interval=interval, check_data_length=check_data_length, limit_nums=limit_nums, + show_1m_logging=show_1m_logging, ) self.normalize_data() From aa2b28386aca03b8f953bc5005ee8cdf0b1ae744 Mon Sep 17 00:00:00 2001 From: Hong Zhang Date: Sat, 28 Nov 2020 02:04:35 +0800 Subject: [PATCH 224/241] revised HATS --- examples/benchmarks/HATS/README.md | 12 + examples/benchmarks/HATS/requirements.txt | 4 + .../benchmarks/HATS/worflow_config_hats.yaml | 77 +++ examples/run_all_model_records/0/meta.yaml | 4 + qlib/contrib/model/pytorch_gats.py | 1 + qlib/contrib/model/pytorch_hats.py | 491 ++++++++++++++++++ 6 files changed, 589 insertions(+) create mode 100644 examples/benchmarks/HATS/README.md create mode 100644 examples/benchmarks/HATS/requirements.txt create mode 100644 examples/benchmarks/HATS/worflow_config_hats.yaml create mode 100644 examples/run_all_model_records/0/meta.yaml create mode 100644 qlib/contrib/model/pytorch_hats.py diff --git a/examples/benchmarks/HATS/README.md b/examples/benchmarks/HATS/README.md new file mode 100644 index 000000000..1a0ac7bb3 --- /dev/null +++ b/examples/benchmarks/HATS/README.md @@ -0,0 +1,12 @@ +## Requirement + +* pandas==1.1.2 +* numpy==1.17.4 +* scikit_learn==0.23.2 +* torch==1.7.0 + +## HATS + +* HATS is a a hierarchical attention network for stock prediction which uses attention layers to broadcast weight for stock market prediction. HATS selectively aggregates information on different relation types and adds the information to the representations of each company. HATS is used as a module with initialized node representations.Furthermore, HATS can predict not only individual stock prices but also market index movements, which is similar to the graph classification task. +* HATS uses pretrained model of GRU and LSTM. The code of GRU and LSTM used in Qlib is a pyTorch implemention of GRU and LSTM. +* Paper address:HATS: A Hierarchical Graph Attention Network for Stock Movement Prediction https://arxiv.org/pdf/1908.07999.pdf \ No newline at end of file diff --git a/examples/benchmarks/HATS/requirements.txt b/examples/benchmarks/HATS/requirements.txt new file mode 100644 index 000000000..16de0a438 --- /dev/null +++ b/examples/benchmarks/HATS/requirements.txt @@ -0,0 +1,4 @@ +pandas==1.1.2 +numpy==1.17.4 +scikit_learn==0.23.2 +torch==1.7.0 diff --git a/examples/benchmarks/HATS/worflow_config_hats.yaml b/examples/benchmarks/HATS/worflow_config_hats.yaml new file mode 100644 index 000000000..ea9f21e76 --- /dev/null +++ b/examples/benchmarks/HATS/worflow_config_hats.yaml @@ -0,0 +1,77 @@ +provider_uri: "~/.qlib/qlib_data/cn_data" +region: cn +market: &market csi300 +benchmark: &benchmark SH000300 +data_handler_config: &data_handler_config + start_time: 2008-01-01 + end_time: 2020-08-01 + fit_start_time: 2008-01-01 + fit_end_time: 2014-12-31 + instruments: *market + infer_processors: + - class: RobustZScoreNorm + kwargs: + fields_group: feature + clip_outlier: true + - class: Fillna + kwargs: + fields_group: feature + learn_processors: + - class: DropnaLabel + - class: CSRankNorm + kwargs: + fields_group: label + label: ["Ref($close, -2) / Ref($close, -1) - 1"] +port_analysis_config: &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 +task: + model: + class: HATS + module_path: qlib.contrib.model.pytorch_hats + kwargs: + d_feat: 6 + hidden_size: 64 + num_layers: 2 + dropout: 0.6 + n_epochs: 200 + lr: 1e-3 + early_stop: 20 + metric: loss + loss: mse + base_model: LSTM + seed: 0 + GPU: 0 + dataset: + class: DatasetH + module_path: qlib.data.dataset + kwargs: + handler: + class: ALPHA360 + module_path: qlib.contrib.data.handler + kwargs: *data_handler_config + segments: + train: [2008-01-01, 2014-12-31] + valid: [2015-01-01, 2016-12-31] + test: [2017-01-01, 2020-08-01] + record: + - class: SignalRecord + module_path: qlib.workflow.record_temp + kwargs: {} + - class: PortAnaRecord + module_path: qlib.workflow.record_temp + kwargs: + config: *port_analysis_config \ No newline at end of file diff --git a/examples/run_all_model_records/0/meta.yaml b/examples/run_all_model_records/0/meta.yaml new file mode 100644 index 000000000..df706fda3 --- /dev/null +++ b/examples/run_all_model_records/0/meta.yaml @@ -0,0 +1,4 @@ +artifact_location: file:///home/v-hozhan/qlib/examples/run_all_model_records/0 +experiment_id: '0' +lifecycle_stage: active +name: Default diff --git a/qlib/contrib/model/pytorch_gats.py b/qlib/contrib/model/pytorch_gats.py index 61a0ef714..e9cbcf9cb 100644 --- a/qlib/contrib/model/pytorch_gats.py +++ b/qlib/contrib/model/pytorch_gats.py @@ -12,6 +12,7 @@ import copy from ...utils import create_save_path from ...log import get_module_logger + import torch import torch.nn as nn import torch.optim as optim diff --git a/qlib/contrib/model/pytorch_hats.py b/qlib/contrib/model/pytorch_hats.py new file mode 100644 index 000000000..7affea73c --- /dev/null +++ b/qlib/contrib/model/pytorch_hats.py @@ -0,0 +1,491 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +from __future__ import division +from __future__ import print_function + +import os +import numpy as np +import pandas as pd +import copy +from ...utils import create_save_path +from ...log import get_module_logger + +import torch +import torch.nn as nn +import torch.optim as optim + +from ...model.base import Model +from ...data.dataset import DatasetH +from ...data.dataset.handler import DataHandlerLP + + +class HATS(Model): + """HATS Model + + Parameters + ---------- + d_feat : int + input dimension for each time step + metric: str + the evaluate metric used in early stop + optimizer : str + optimizer name + GPU : str + the GPU ID(s) used for training + """ + + def __init__( + self, + d_feat=6, + hidden_size=64, + num_layers=2, + dropout=0.5, + n_epochs=200, + lr=0.01, + metric="", + early_stop=20, + loss="mse", + base_model="GRU", + with_pretrain=True, + optimizer="adam", + GPU="0", + seed=0, + **kwargs + ): + # Set logger. + self.logger = get_module_logger("HATS") + self.logger.info("HATS pytorch version...") + + # set hyper-parameters. + self.d_feat = d_feat + self.hidden_size = hidden_size + self.num_layers = num_layers + self.dropout = dropout + self.n_epochs = n_epochs + self.lr = lr + self.metric = metric + self.early_stop = early_stop + self.optimizer = optimizer.lower() + self.loss = loss + self.base_model = base_model + self.with_pretrain = with_pretrain + self.visible_GPU = GPU + self.use_gpu = torch.cuda.is_available() + self.seed = seed + + self.logger.info( + "HATS parameters setting:" + "\nd_feat : {}" + "\nhidden_size : {}" + "\nnum_layers : {}" + "\ndropout : {}" + "\nn_epochs : {}" + "\nlr : {}" + "\nmetric : {}" + "\nearly_stop : {}" + "\noptimizer : {}" + "\nloss_type : {}" + "\nbase_model : {}" + "\nwith_pretrain : {}" + "\nvisible_GPU : {}" + "\nuse_GPU : {}" + "\nseed : {}".format( + d_feat, + hidden_size, + num_layers, + dropout, + n_epochs, + lr, + metric, + early_stop, + optimizer.lower(), + loss, + base_model, + with_pretrain, + GPU, + self.use_gpu, + seed, + ) + ) + + self.HATS_model = HATSModel( + d_feat=self.d_feat, + hidden_size=self.hidden_size, + num_layers=self.num_layers, + dropout=self.dropout, + base_model=self.base_model, + ) + if optimizer.lower() == "adam": + self.train_optimizer = optim.Adam(self.HATS_model.parameters(), lr=self.lr) + elif optimizer.lower() == "gd": + self.train_optimizer = optim.SGD(self.HATS_model.parameters(), lr=self.lr) + else: + raise NotImplementedError("optimizer {} is not supported!".format(optimizer)) + + self._fitted = False + if self.use_gpu: + self.HATS_model.cuda() + # set the visible GPU + if self.visible_GPU: + os.environ["CUDA_VISIBLE_DEVICES"] = self.visible_GPU + + def mse(self, pred, label): + loss = (pred - label) ** 2 + return torch.mean(loss) + + def loss_fn(self, pred, label): + mask = ~torch.isnan(label) + + if self.loss == "mse": + return self.mse(pred[mask], label[mask]) + + raise ValueError("unknown loss `%s`" % self.loss) + + def metric_fn(self, pred, label): + mask = torch.isfinite(label) + + if self.metric == "" or self.metric == "loss": # use loss + return -self.loss_fn(pred[mask], label[mask]) + + raise ValueError("unknown metric `%s`" % self.metric) + + def get_daily_inter(self, df, shuffle=False): + # organize the train data into daily inter as daily batches + daily_count = df.groupby(level=0).size().values + daily_index = np.roll(np.cumsum(daily_count), 1) + daily_index[0] = 0 + if shuffle: + # shuffle the daily inter data + daily_shuffle = list(zip(daily_index, daily_count)) + np.random.shuffle(daily_shuffle) + daily_index, daily_count = zip(*daily_shuffle) + return daily_index, daily_count + + def train_epoch(self, x_train, y_train): + + x_train_values = x_train.values + y_train_values = np.squeeze(y_train.values) + + self.HATS_model.train() + + # organize the train data into daily inter as daily batches + daily_index, daily_count = self.get_daily_inter(x_train, shuffle=True) + + for idx, count in zip(daily_index, daily_count): + batch = slice(idx, idx + count) + feature = torch.from_numpy(x_train_values[batch]).float() + label = torch.from_numpy(y_train_values[batch]).float() + + if self.use_gpu: + feature = feature.cuda() + label = label.cuda() + + pred = self.HATS_model(feature) + loss = self.loss_fn(pred, label) + + self.train_optimizer.zero_grad() + loss.backward() + torch.nn.utils.clip_grad_value_(self.HATS_model.parameters(), 3.0) + self.train_optimizer.step() + + def test_epoch(self, data_x, data_y): + + # prepare testing data + x_values = data_x.values + y_values = np.squeeze(data_y.values) + + self.HATS_model.eval() + + scores = [] + losses = [] + + # organize the test data into daily inter as daily batches + daily_index, daily_count = self.get_daily_inter(data_x, shuffle=False) + + for idx, count in zip(daily_index, daily_count): + batch = slice(idx, idx + count) + feature = torch.from_numpy(x_values[batch]).float() + label = torch.from_numpy(y_values[batch]).float() + + if self.use_gpu: + feature = feature.cuda() + label = label.cuda() + + pred = self.HATS_model(feature) + loss = self.loss_fn(pred, label) + losses.append(loss.item()) + + score = self.metric_fn(pred, label) + scores.append(score.item()) + + return np.mean(losses), np.mean(scores) + + def fit( + self, + dataset: DatasetH, + evals_result=dict(), + verbose=True, + save_path=None, + ): + + df_train, df_valid, df_test = dataset.prepare( + ["train", "valid", "test"], col_set=["feature", "label"], data_key=DataHandlerLP.DK_L + ) + + x_train, y_train = df_train["feature"], df_train["label"] + x_valid, y_valid = df_valid["feature"], df_valid["label"] + + if save_path == None: + save_path = create_save_path(save_path) + stop_steps = 0 + best_score = -np.inf + best_epoch = 0 + evals_result["train"] = [] + evals_result["valid"] = [] + + # load pretrained base_model + if self.with_pretrain: + self.logger.info("Loading pretrained model...") + if self.base_model == "LSTM": + from ...contrib.model.pytorch_lstm import LSTMModel + + pretrained_model = LSTMModel() + pretrained_model.load_state_dict(torch.load("benchmarks/LSTM/model_lstm_csi300.pkl")) + elif self.base_model == "GRU": + from ...contrib.model.pytorch_gru import GRUModel + + pretrained_model = GRUModel() + pretrained_model.load_state_dict(torch.load("benchmarks/GRU/model_gru_csi300.pkl")) + model_dict = self.HATS_model.state_dict() + pretrained_dict = {k: v for k, v in pretrained_model.state_dict().items() if k in model_dict} + model_dict.update(pretrained_dict) + self.HATS_model.load_state_dict(model_dict) + self.logger.info("Loading pretrained model Done...") + + # train + self.logger.info("training...") + self._fitted = True + + for step in range(self.n_epochs): + self.logger.info("Epoch%d:", step) + self.logger.info("training...") + self.train_epoch(x_train, y_train) + self.logger.info("evaluating...") + train_loss, train_score = self.test_epoch(x_train, y_train) + val_loss, val_score = self.test_epoch(x_valid, y_valid) + self.logger.info("train %.6f, valid %.6f" % (train_score, val_score)) + evals_result["train"].append(train_score) + evals_result["valid"].append(val_score) + + if val_score > best_score: + best_score = val_score + stop_steps = 0 + best_epoch = step + best_param = copy.deepcopy(self.HATS_model.state_dict()) + else: + stop_steps += 1 + if stop_steps >= self.early_stop: + self.logger.info("early stop") + break + + self.logger.info("best score: %.6lf @ %d" % (best_score, best_epoch)) + self.HATS_model.load_state_dict(best_param) + torch.save(best_param, save_path) + + if self.use_gpu: + torch.cuda.empty_cache() + + def predict(self, dataset): + if not self._fitted: + raise ValueError("model is not fitted yet!") + + x_test = dataset.prepare("test", col_set="feature") + index = x_test.index + self.HATS_model.eval() + x_values = x_test.values + sample_num = x_values.shape[0] + preds = [] + + # organize the data into daily inter as daily batches + daily_index, daily_count = self.get_daily_inter(x_test, shuffle=False) + + for idx, count in zip(daily_index, daily_count): + batch = slice(idx, idx + count) + x_batch = torch.from_numpy(x_values[batch]).float() + + if self.use_gpu: + x_batch = x_batch.cuda() + + with torch.no_grad(): + if self.use_gpu: + pred = self.HATS_model(x_batch).detach().cpu().numpy() + else: + pred = self.HATS_model(x_batch).detach().numpy() + + preds.append(pred) + + return pd.Series(np.concatenate(preds), index=index) + + +class HATSModel(nn.Module): + def __init__(self, d_feat=6, hidden_size=64, num_layers=2, dropout=0.0, base_model="GRU"): + super().__init__() + + if base_model == "GRU": + self.model = nn.GRU( + input_size=d_feat, + hidden_size=hidden_size, + num_layers=num_layers, + batch_first=True, + dropout=dropout, + ) + elif base_model == "LSTM": + self.model = nn.LSTM( + input_size=d_feat, + hidden_size=hidden_size, + num_layers=num_layers, + batch_first=True, + dropout=dropout, + ) + else: + raise ValueError("unknown base model name `%s`" % base_model) + + self.hidden_size = hidden_size + self.bn1 = nn.BatchNorm1d(num_features=hidden_size, track_running_stats=False) + self.fc = nn.Linear(hidden_size, hidden_size) + self.bn2 = nn.BatchNorm1d(num_features=hidden_size, track_running_stats=False) + self.fc_out = nn.Linear(hidden_size, 1) + self.leaky_relu = nn.LeakyReLU() + self.softmax = nn.Softmax(dim=1) + self.d_feat = d_feat + + num_head_att = [1] * num_layers + hidden_dim = [hidden_size] * num_layers + dims = [d_feat] + [d * nh for (d, nh) in zip(hidden_dim, num_head_att[:-1])] + [num_head_att[-1]] + in_dims = dims[:-1] + out_dims = [d // nh for (d, nh) in zip(dims[1:], num_head_att)] + self.attn = nn.ModuleList( + [GraphAttention(i, o, nh, dropout) for (i, o, nh) in zip(in_dims, out_dims, num_head_att)] + ) + self.bns = nn.ModuleList([nn.BatchNorm1d(dim) for dim in dims[1:-1]]) + self.dropout = nn.Dropout(dropout) + self.elu = nn.ELU() + + def forward(self, x): + x = x.reshape(len(x), self.d_feat, -1) # [N, F, T] + x = x.permute(0, 2, 1) # [N, T, F] + out, _ = self.model(x) + hidden = out[:, -1, :] + hidden = self.bn1(hidden) + attention = GraphAttention.cal_attention(hidden, hidden) + output = attention.mm(hidden) + output = self.fc(output) + output = self.bn2(output) + output = self.leaky_relu(output) + return self.fc_out(output).squeeze() + + +class GraphAttention(nn.Module): + def __init__(self, input_dim, output_dim, num_heads, dropout=0.5): + + super().__init__() + + """ + Parameters + ---------- + input_dim : int + Dimension of input node features. + output_dim : int + Dimension of output node features. + num_heads : list of ints + Number of attention heads in each hidden layer and output layer. Must be non empty. Note that len(num_heads) = len(hidden_dims)+1. + dropout : float + Dropout rate. Default: 0.5. + """ + + self.input_dim = input_dim + self.output_dim = output_dim + self.num_heads = num_heads + + self.fcs = nn.ModuleList([nn.Linear(input_dim, output_dim) for _ in range(num_heads)]) + self.a = nn.ModuleList([nn.Linear(2 * output_dim, 1) for _ in range(num_heads)]) + + self.dropout = nn.Dropout(dropout) + self.softmax = nn.Softmax(dim=0) + self.leakyrelu = nn.LeakyReLU() + + def forward(self, features, nodes, mappings, rows): + + """ + Parameters + ---------- + features : torch.Tensor + An (n' x input_dim) tensor of input node features. + nodes : list of numpy array + nodes[i] is an array of the nodes in the ith layer of the + computation graph. + mappings : list of dictionary + mappings[i] is a dictionary mappings node v (labelled 0 to |V|-1) + in nodes[i] to its position in nodes[i]. For example, + if nodes[i] = [2,5], then mappings[i][2] = 0 and + mappings[i][5] = 1. + rows : numpy array + rows[i] is an array of neighbors of node i. + Returns + ------- + out : torch.Tensor + An (len(node_layers[-1]) x output_dim) tensor of output node features. + """ + + nprime = features.shape[0] + rows = [np.array([mappings[v] for v in row], dtype=np.int64) for row in rows] + sum_degs = np.hstack(([0], np.cumsum([len(row) for row in rows]))) + mapped_nodes = [mappings[v] for v in nodes] + indices = torch.LongTensor([[v, c] for (v, row) in zip(mapped_nodes, rows) for c in row]).t() + + out = [] + for k in range(self.num_heads): + h = self.fcs[k](features) + + nbr_h = torch.cat(tuple([h[row] for row in rows]), dim=0) + self_h = torch.cat( + tuple([h[mappings[nodes[i]]].repeat(len(row), 1) for (i, row) in enumerate(rows)]), dim=0 + ) + cat_h = torch.cat((self_h, nbr_h), dim=1) + + e = self.leakyrelu(self.a[k](cat_h)) + + alpha = [self.softmax(e[lo:hi]) for (lo, hi) in zip(sum_degs, sum_degs[1:])] + alpha = torch.cat(tuple(alpha), dim=0) + alpha = alpha.squeeze(1) + alpha = self.dropout(alpha) + + adj = torch.sparse.FloatTensor(indices, alpha, torch.Size([nprime, nprime])) + out.append(torch.sparse.mm(adj, h)[mapped_nodes]) + + return out + + @staticmethod + def cal_attention(x, y): + att_x = torch.mean(x, dim=1).reshape(-1, 1) + att_y = torch.mean(y, dim=1).reshape(-1, 1) + att = att_x.mm(torch.t(att_y)) + return ( + torch.mean( + x.reshape(x.shape[0], 1, x.shape[1]).repeat(1, y.shape[0], 1) + * y.reshape(1, y.shape[0], y.shape[1]).repeat(x.shape[0], 1, 1), + dim=2, + ) + - att + ) From 4f69ab6ed84b3bbbb34180735e5e9acdd37ae031 Mon Sep 17 00:00:00 2001 From: Dong Zhou Date: Sat, 28 Nov 2020 08:25:38 +0800 Subject: [PATCH 225/241] clean up --- examples/portfolio_optimization_example.ipynb | 437 ------------------ examples/run_all_model_records/0/meta.yaml | 4 - 2 files changed, 441 deletions(-) delete mode 100644 examples/portfolio_optimization_example.ipynb delete mode 100644 examples/run_all_model_records/0/meta.yaml diff --git a/examples/portfolio_optimization_example.ipynb b/examples/portfolio_optimization_example.ipynb deleted file mode 100644 index 7ef593efa..000000000 --- a/examples/portfolio_optimization_example.ipynb +++ /dev/null @@ -1,437 +0,0 @@ -{ - "metadata": { - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.7.9-final" - }, - "orig_nbformat": 2, - "kernelspec": { - "name": "python3", - "display_name": "Python 3" - } - }, - "nbformat": 4, - "nbformat_minor": 2, - "cells": [ - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [], - "source": [ - "import sys\n", - "import copy\n", - "from pathlib import Path\n", - "\n", - "import qlib\n", - "import numpy as np\n", - "import pandas as pd\n", - "from qlib.config import REG_CN\n", - "from qlib.contrib.model.gbdt import LGBModel\n", - "from qlib.contrib.data.handler import Alpha158\n", - "from qlib.contrib.strategy.strategy import TopkDropoutStrategy\n", - "from qlib.contrib.evaluate import (\n", - " backtest as normal_backtest,\n", - " risk_analysis,\n", - ")\n", - "from qlib.utils import exists_qlib_data, init_instance_by_config\n", - "from qlib.workflow import R\n", - "from qlib.workflow.record_temp import SignalRecord, PortAnaRecord\n", - "from qlib.utils import flatten_dict" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [ - { - "output_type": "stream", - "name": "stderr", - "text": [ - "[36502:MainThread](2020-11-27 16:26:57,240) INFO - qlib.Initialization - [__init__.py:41] - default_conf: client.\n", - "[36502:MainThread](2020-11-27 16:26:57,242) WARNING - qlib.Initialization - [__init__.py:57] - redis connection failed(host=127.0.0.1 port=6379), cache will not be used!\n", - "[36502:MainThread](2020-11-27 16:26:57,243) INFO - qlib.Initialization - [__init__.py:76] - qlib successfully initialized based on client settings.\n", - "[36502:MainThread](2020-11-27 16:26:57,244) INFO - qlib.Initialization - [__init__.py:79] - data_path=/home/dongzho/.qlib/qlib_data/cn_data\n" - ] - } - ], - "source": [ - "# use default data\n", - "# NOTE: need to download data from remote: python scripts/get_data.py qlib_data_cn --target_dir ~/.qlib/qlib_data/cn_data\n", - "provider_uri = \"~/.qlib/qlib_data/cn_data\" # target_dir\n", - "if not exists_qlib_data(provider_uri):\n", - " print(f\"Qlib data is not found in {provider_uri}\")\n", - " sys.path.append(str(Path.cwd().parent.joinpath(\"scripts\")))\n", - " from get_data import GetData\n", - " GetData().qlib_data(target_dir=provider_uri, region=REG_CN)\n", - "qlib.init(provider_uri=provider_uri, region=REG_CN)" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [], - "source": [ - "market = \"csi300\"\n", - "benchmark = \"SH000300\"" - ] - }, - { - "source": [ - "## Model Training" - ], - "cell_type": "markdown", - "metadata": {} - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": {}, - "outputs": [ - { - "output_type": "stream", - "name": "stderr", - "text": [ - "[36502:MainThread](2020-11-27 16:27:17,338) INFO - qlib.timer - [log.py:81] - Time cost: 19.994s | Loading data Done\n", - "[36502:MainThread](2020-11-27 16:27:18,164) INFO - qlib.timer - [log.py:81] - Time cost: 0.245s | DropnaLabel Done\n", - "[36502:MainThread](2020-11-27 16:27:26,086) INFO - qlib.timer - [log.py:81] - Time cost: 7.921s | CSZScoreNorm Done\n", - "[36502:MainThread](2020-11-27 16:27:26,087) INFO - qlib.timer - [log.py:81] - Time cost: 8.747s | fit & process data Done\n", - "[36502:MainThread](2020-11-27 16:27:26,088) INFO - qlib.timer - [log.py:81] - Time cost: 28.744s | Init data Done\n", - "[36502:MainThread](2020-11-27 16:27:26,097) INFO - qlib.workflow - [exp.py:180] - Experiment 2 starts running ...\n", - "[36502:MainThread](2020-11-27 16:27:26,221) INFO - qlib.workflow - [recorder.py:234] - Recorder 3fa4def1f6694119a3d336a7a06c88cb starts running under Experiment 2 ...\n", - "[36502:MainThread](2020-11-27 16:27:26,223) INFO - qlib.workflow - [expm.py:251] - No tracking URI is provided. The default tracking URI is set as `mlruns` under the working directory.\n", - "Training until validation scores don't improve for 50 rounds\n", - "[20]\ttrain's l2: 0.990559\tvalid's l2: 0.994332\n", - "[40]\ttrain's l2: 0.98687\tvalid's l2: 0.993702\n", - "[60]\ttrain's l2: 0.984308\tvalid's l2: 0.993503\n", - "[80]\ttrain's l2: 0.982202\tvalid's l2: 0.993446\n", - "[100]\ttrain's l2: 0.980318\tvalid's l2: 0.993423\n", - "[120]\ttrain's l2: 0.97854\tvalid's l2: 0.993409\n", - "[140]\ttrain's l2: 0.97679\tvalid's l2: 0.993413\n", - "[160]\ttrain's l2: 0.975116\tvalid's l2: 0.993473\n", - "Early stopping, best iteration is:\n", - "[127]\ttrain's l2: 0.977957\tvalid's l2: 0.993381\n" - ] - } - ], - "source": [ - "###################################\n", - "# train model\n", - "###################################\n", - "data_handler_config = {\n", - " \"start_time\": \"2008-01-01\",\n", - " \"end_time\": \"2020-08-01\",\n", - " \"fit_start_time\": \"2008-01-01\",\n", - " \"fit_end_time\": \"2014-12-31\",\n", - " \"instruments\": market,\n", - "}\n", - "\n", - "task = {\n", - " \"model\": {\n", - " \"class\": \"LGBModel\",\n", - " \"module_path\": \"qlib.contrib.model.gbdt\",\n", - " \"kwargs\": {\n", - " \"loss\": \"mse\",\n", - " \"colsample_bytree\": 0.8879,\n", - " \"learning_rate\": 0.0421,\n", - " \"subsample\": 0.8789,\n", - " \"lambda_l1\": 205.6999,\n", - " \"lambda_l2\": 580.9768,\n", - " \"max_depth\": 8,\n", - " \"num_leaves\": 210,\n", - " \"num_threads\": 20,\n", - " },\n", - " },\n", - " \"dataset\": {\n", - " \"class\": \"DatasetH\",\n", - " \"module_path\": \"qlib.data.dataset\",\n", - " \"kwargs\": {\n", - " \"handler\": {\n", - " \"class\": \"Alpha158\",\n", - " \"module_path\": \"qlib.contrib.data.handler\",\n", - " \"kwargs\": data_handler_config,\n", - " },\n", - " \"segments\": {\n", - " \"train\": (\"2008-01-01\", \"2014-12-31\"),\n", - " \"valid\": (\"2015-01-01\", \"2016-12-31\"),\n", - " \"test\": (\"2017-01-01\", \"2017-12-31\"), # NOTE: use a shorter time range\n", - " },\n", - " },\n", - " },\n", - "}\n", - "\n", - "# model initiaiton\n", - "model = init_instance_by_config(task[\"model\"])\n", - "dataset = init_instance_by_config(task[\"dataset\"])\n", - "\n", - "# start exp to train model\n", - "with R.start(experiment_name=\"train_model\"):\n", - " R.log_params(**flatten_dict(task))\n", - " model.fit(dataset)\n", - " R.save_objects(trained_model=model)\n", - " rid = R.get_recorder().id\n" - ] - }, - { - "source": [ - "## Optimization Based Strategy" - ], - "cell_type": "markdown", - "metadata": {} - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": {}, - "outputs": [], - "source": [ - "from qlib.contrib.strategy.strategy import BaseStrategy\n", - "\n", - "\n", - "class OptBasedStrategy(BaseStrategy):\n", - " \"\"\"Optimization Based Strategy\"\"\"\n", - "\n", - " def __init__(self, data_handler, cov_estimator, optimizer):\n", - " self.data_handler = data_handler\n", - " self.cov_estimator = cov_estimator\n", - " self.optimizer = optimizer\n", - "\n", - " def generate_order_list(self, score_series, current, trade_exchange, pred_date, trade_date):\n", - " \"\"\"\n", - " Parameters\n", - " -----------\n", - " score_series : pd.Seires\n", - " stock_id , score.\n", - " current : Position()\n", - " current of account.\n", - " trade_exchange : Exchange()\n", - " exchange.\n", - " trade_date : pd.Timestamp\n", - " date.\n", - " \"\"\"\n", - " score_series = score_series.dropna()\n", - "\n", - " # check stock holdings, if\n", - " # 1. doesn't have score: target amount = 0 (force sell)\n", - " # 2. stock not tradable: target amount = current amount\n", - " current_position = current.get_stock_amount_dict()\n", - " target_position = {}\n", - " for stock_id in current_position:\n", - " if not trade_exchange.is_stock_tradable(stock_id=stock_id, trade_date=trade_date):\n", - " target_position[stock_id] = current_position[stock_id]\n", - " elif stock_id not in score_series.index:\n", - " target_position[stock_id] = 0\n", - " else:\n", - " # need to be solved by optimizer\n", - " pass\n", - "\n", - " # filter scores, if\n", - " # 1. kept in `amount_dict` by previous rules\n", - " # 2. not tradable\n", - " skipped = []\n", - " for stock_id in score_series.index:\n", - " if stock_id in target_position:\n", - " skipped.append(stock_id)\n", - " elif not trade_exchange.is_stock_tradable(stock_id=stock_id, trade_date=trade_date):\n", - " skipped.append(stock_id)\n", - " score_series = score_series[~score_series.index.isin(skipped)]\n", - "\n", - " # calc remaining value\n", - " current_value = pd.Series({\n", - " stock_id: current.get_stock_price(stock_id) * amount\n", - " for stock_id, amount in current_position.items()\n", - " })\n", - " risk_total_value = self.get_risk_degree(trade_date) * current.calculate_value()\n", - " traded_value = risk_total_value - current_value.loc[list(target_position)].sum()\n", - "\n", - " # portfolio init weight\n", - " init_weight = current_value.reindex(score_series.index, fill_value=0)\n", - " init_weight_sum = init_weight.sum()\n", - " if init_weight_sum > 0:\n", - " init_weight /= init_weight_sum\n", - "\n", - " # covariance estimation\n", - " selector = (self.data_handler.get_range_selector(pred_date, 252), score_series.index)\n", - " price = self.data_handler.fetch(selector, level=None, squeeze=True)\n", - " cov = self.cov_estimator(price)\n", - " cov = cov.reindex(\n", - " index=score_series.index, \n", - " columns=score_series.index, \n", - " #fill_value=cov.max().max()\n", - " )\n", - "\n", - " # optimize target portfolio\n", - " try:\n", - " if init_weight.sum() > 0:\n", - " target_weight = self.optimizer(cov, score_series, init_weight)\n", - " else:\n", - " target_weight = self.optimizer(cov, score_series)\n", - " target_weight = target_weight[target_weight > 1e-6]\n", - " for stock_id, weight in target_weight.items():\n", - " target_position[stock_id] = int(traded_value * weight / trade_exchange.get_close(stock_id, pred_date))\n", - " except Exception as e:\n", - " print('Unknown exception:', trade_date, e)\n", - " for stock_id in score_series.index:\n", - " if stock_id in current_position:\n", - " target_position[stock_id] = current_position[stock_id]\n", - "\n", - " # generate order list\n", - " order_list = trade_exchange.generate_order_for_target_amount_position(\n", - " target_position=target_position,\n", - " current_position=current_position,\n", - " trade_date=trade_date,\n", - " )\n", - "\n", - " return order_list" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": {}, - "outputs": [], - "source": [ - "from qlib.data.dataset.loader import QlibDataLoader\n", - "from qlib.data.dataset.handler import DataHandler\n", - "from qlib.model.riskmodel import ShrinkCovEstimator\n", - "from qlib.portfolio.optimizer import PortfolioOptimizer" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "metadata": {}, - "outputs": [ - { - "output_type": "stream", - "name": "stderr", - "text": [ - "[36502:MainThread](2020-11-27 16:27:43,722) INFO - qlib.timer - [log.py:81] - Time cost: 6.369s | Loading data Done\n", - "[36502:MainThread](2020-11-27 16:27:43,724) INFO - qlib.timer - [log.py:81] - Time cost: 6.371s | Init data Done\n" - ] - } - ], - "source": [ - "data_loader = QlibDataLoader([\"$close\"])\n", - "data_handler = DataHandler(\"all\", \"2015-01-01\", \"2020-08-01\", data_loader)\n", - "cov_estimator = ShrinkCovEstimator(nan_option=\"mask\")\n", - "optimizer = PortfolioOptimizer(\"mvo\", lamb=2, delta=0.2, tol=1e-5)\n", - "strategy = OptBasedStrategy(data_handler, cov_estimator, optimizer)" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "metadata": { - "tags": [] - }, - "outputs": [ - { - "output_type": "stream", - "name": "stderr", - "text": [ - "[36502:MainThread](2020-11-27 16:27:43,761) INFO - qlib.workflow - [exp.py:180] - Experiment 3 starts running ...\n", - "[36502:MainThread](2020-11-27 16:27:43,779) INFO - qlib.workflow - [recorder.py:234] - Recorder 67d105113f424259889fc0b6b0b94973 starts running under Experiment 3 ...\n", - "[36502:MainThread](2020-11-27 16:27:43,780) INFO - qlib.workflow - [expm.py:251] - No tracking URI is provided. The default tracking URI is set as `mlruns` under the working directory.\n", - "[36502:MainThread](2020-11-27 16:27:43,991) INFO - qlib.workflow - [record_temp.py:127] - Signal record 'pred.pkl' has been saved as the artifact of the Experiment 3\n", - "[36502:MainThread](2020-11-27 16:27:44,050) INFO - qlib.Evaluate - [evaluate.py:161] - Create new exchange\n", - "'The following are prediction results of the LGBModel model.'\n", - " score\n", - "datetime instrument \n", - "2017-01-03 SH600000 -0.053414\n", - " SH600008 0.001820\n", - " SH600009 0.023472\n", - " SH600010 -0.005625\n", - " SH600015 -0.137476\n", - "/home/dongzho/miniconda3/lib/python3.7/site-packages/ipykernel_launcher.py:55: DeprecationWarning: The default dtype for empty Series will be 'object' instead of 'float64' in a future version. Specify a dtype explicitly to silence this warning.\n", - "/home/dongzho/qlib/qlib/portfolio/optimizer.py:256: UserWarning: optimization not success (9)\n", - " warnings.warn(f\"optimization not success ({sol.status})\")\n", - "Unknown exception: 2017-01-16 00:00:00 ('SZ300104', Timestamp('2017-01-13 00:00:00'))\n", - "Unknown exception: 2017-01-23 00:00:00 ('SZ000671', Timestamp('2017-01-20 00:00:00'))\n", - "Unknown exception: 2017-03-03 00:00:00 ('SZ002465', Timestamp('2017-03-02 00:00:00'))\n", - "Unknown exception: 2017-03-07 00:00:00 ('SH601127', Timestamp('2017-03-06 00:00:00'))\n", - "/home/dongzho/qlib/qlib/portfolio/optimizer.py:256: UserWarning: optimization not success (4)\n", - " warnings.warn(f\"optimization not success ({sol.status})\")\n", - "Unknown exception: 2017-05-08 00:00:00 ('SH601727', Timestamp('2017-05-05 00:00:00'))\n", - "Unknown exception: 2017-06-20 00:00:00 ('SH600036', Timestamp('2017-06-19 00:00:00'))\n", - "Unknown exception: 2017-06-21 00:00:00 ('SH600739', Timestamp('2017-06-20 00:00:00'))\n", - "Unknown exception: 2017-06-29 00:00:00 ('SZ300168', Timestamp('2017-06-28 00:00:00'))\n", - "Unknown exception: 2017-09-01 00:00:00 ('SH601088', Timestamp('2017-08-31 00:00:00'))\n", - "Unknown exception: 2017-09-12 00:00:00 ('SH601872', Timestamp('2017-09-11 00:00:00'))\n", - "Unknown exception: 2017-09-21 00:00:00 ('SH600100', Timestamp('2017-09-20 00:00:00'))\n", - "Unknown exception: 2017-09-22 00:00:00 ('SH600021', Timestamp('2017-09-21 00:00:00'))\n", - "Unknown exception: 2017-10-11 00:00:00 ('SH600959', Timestamp('2017-10-10 00:00:00'))\n", - "Unknown exception: 2017-10-25 00:00:00 ('SZ000792', Timestamp('2017-10-24 00:00:00'))\n", - "Unknown exception: 2017-12-26 00:00:00 ('SH600682', Timestamp('2017-12-25 00:00:00'))\n", - "[36502:MainThread](2020-11-27 17:28:14,269) INFO - qlib.workflow - [record_temp.py:249] - Portfolio analysis record 'port_analysis.pkl' has been saved as the artifact of the Experiment 3\n", - "'The following are analysis results of the excess return without cost.'\n", - " risk\n", - "mean 0.001247\n", - "std 0.005437\n", - "annualized_return 0.314237\n", - "information_ratio 3.640637\n", - "max_drawdown -0.033416\n", - "'The following are analysis results of the excess return with cost.'\n", - " risk\n", - "mean 0.001028\n", - "std 0.005432\n", - "annualized_return 0.259041\n", - "information_ratio 3.003970\n", - "max_drawdown -0.041455\n" - ] - } - ], - "source": [ - "###################################\n", - "# prediction, backtest & analysis\n", - "###################################\n", - "port_analysis_config = {\n", - " \"strategy\": strategy,\n", - " \"backtest\": {\n", - " \"verbose\": False,\n", - " \"limit_threshold\": 0.095,\n", - " \"account\": 100000000,\n", - " \"benchmark\": benchmark,\n", - " \"deal_price\": \"close\",\n", - " \"open_cost\": 0.0005,\n", - " \"close_cost\": 0.0015,\n", - " \"min_cost\": 5,\n", - " },\n", - "}\n", - "\n", - "\n", - "# backtest and analysis\n", - "with R.start(experiment_name=\"backtest_analysis\"):\n", - " recorder = R.get_recorder(rid, experiment_name=\"train_model\")\n", - " model = recorder.load_object(\"trained_model\")\n", - "\n", - " # prediction\n", - " recorder = R.get_recorder()\n", - " ba_rid = recorder.id\n", - " sr = SignalRecord(model, dataset, recorder)\n", - " sr.generate()\n", - "\n", - " # backtest & analysis\n", - " par = PortAnaRecord(recorder, port_analysis_config)\n", - " par.generate()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - } - ] -} \ No newline at end of file diff --git a/examples/run_all_model_records/0/meta.yaml b/examples/run_all_model_records/0/meta.yaml deleted file mode 100644 index df706fda3..000000000 --- a/examples/run_all_model_records/0/meta.yaml +++ /dev/null @@ -1,4 +0,0 @@ -artifact_location: file:///home/v-hozhan/qlib/examples/run_all_model_records/0 -experiment_id: '0' -lifecycle_stage: active -name: Default From dc8bf6f077cfc2a40a993429621b1287e7621dbe Mon Sep 17 00:00:00 2001 From: Dong Zhou Date: Sat, 28 Nov 2020 08:26:18 +0800 Subject: [PATCH 226/241] add colab button --- examples/workflow_by_code.ipynb | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/examples/workflow_by_code.ipynb b/examples/workflow_by_code.ipynb index 81dbf3e31..20fce92fe 100644 --- a/examples/workflow_by_code.ipynb +++ b/examples/workflow_by_code.ipynb @@ -1,5 +1,12 @@ { "cells": [ + { + "source": [ + "[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/microsoft/qlib/blob/main/examples/workflow_by_code.ipynb)" + ], + "cell_type": "markdown", + "metadata": {} + }, { "cell_type": "code", "execution_count": null, @@ -369,4 +376,4 @@ }, "nbformat": 4, "nbformat_minor": 4 -} +} \ No newline at end of file From cde5c59b7e8bcd6aade991434f975ecf408e6c3c Mon Sep 17 00:00:00 2001 From: Dong Zhou Date: Sat, 28 Nov 2020 08:38:42 +0800 Subject: [PATCH 227/241] fix colab link --- examples/workflow_by_code.ipynb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/workflow_by_code.ipynb b/examples/workflow_by_code.ipynb index 20fce92fe..d5711e0b5 100644 --- a/examples/workflow_by_code.ipynb +++ b/examples/workflow_by_code.ipynb @@ -2,7 +2,7 @@ "cells": [ { "source": [ - "[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/microsoft/qlib/blob/main/examples/workflow_by_code.ipynb)" + "\"Open" ], "cell_type": "markdown", "metadata": {} From d820d5b8f6abca3e0a3840f0fab62ca26f56c438 Mon Sep 17 00:00:00 2001 From: lwwang1995 Date: Sat, 28 Nov 2020 10:40:31 +0800 Subject: [PATCH 228/241] Delete Hats --- examples/benchmarks/HATS/README.md | 12 - examples/benchmarks/HATS/requirements.txt | 4 - .../benchmarks/HATS/worflow_config_hats.yaml | 77 --- qlib/contrib/model/pytorch_hats.py | 491 ------------------ 4 files changed, 584 deletions(-) delete mode 100644 examples/benchmarks/HATS/README.md delete mode 100644 examples/benchmarks/HATS/requirements.txt delete mode 100644 examples/benchmarks/HATS/worflow_config_hats.yaml delete mode 100644 qlib/contrib/model/pytorch_hats.py diff --git a/examples/benchmarks/HATS/README.md b/examples/benchmarks/HATS/README.md deleted file mode 100644 index 1a0ac7bb3..000000000 --- a/examples/benchmarks/HATS/README.md +++ /dev/null @@ -1,12 +0,0 @@ -## Requirement - -* pandas==1.1.2 -* numpy==1.17.4 -* scikit_learn==0.23.2 -* torch==1.7.0 - -## HATS - -* HATS is a a hierarchical attention network for stock prediction which uses attention layers to broadcast weight for stock market prediction. HATS selectively aggregates information on different relation types and adds the information to the representations of each company. HATS is used as a module with initialized node representations.Furthermore, HATS can predict not only individual stock prices but also market index movements, which is similar to the graph classification task. -* HATS uses pretrained model of GRU and LSTM. The code of GRU and LSTM used in Qlib is a pyTorch implemention of GRU and LSTM. -* Paper address:HATS: A Hierarchical Graph Attention Network for Stock Movement Prediction https://arxiv.org/pdf/1908.07999.pdf \ No newline at end of file diff --git a/examples/benchmarks/HATS/requirements.txt b/examples/benchmarks/HATS/requirements.txt deleted file mode 100644 index 16de0a438..000000000 --- a/examples/benchmarks/HATS/requirements.txt +++ /dev/null @@ -1,4 +0,0 @@ -pandas==1.1.2 -numpy==1.17.4 -scikit_learn==0.23.2 -torch==1.7.0 diff --git a/examples/benchmarks/HATS/worflow_config_hats.yaml b/examples/benchmarks/HATS/worflow_config_hats.yaml deleted file mode 100644 index ea9f21e76..000000000 --- a/examples/benchmarks/HATS/worflow_config_hats.yaml +++ /dev/null @@ -1,77 +0,0 @@ -provider_uri: "~/.qlib/qlib_data/cn_data" -region: cn -market: &market csi300 -benchmark: &benchmark SH000300 -data_handler_config: &data_handler_config - start_time: 2008-01-01 - end_time: 2020-08-01 - fit_start_time: 2008-01-01 - fit_end_time: 2014-12-31 - instruments: *market - infer_processors: - - class: RobustZScoreNorm - kwargs: - fields_group: feature - clip_outlier: true - - class: Fillna - kwargs: - fields_group: feature - learn_processors: - - class: DropnaLabel - - class: CSRankNorm - kwargs: - fields_group: label - label: ["Ref($close, -2) / Ref($close, -1) - 1"] -port_analysis_config: &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 -task: - model: - class: HATS - module_path: qlib.contrib.model.pytorch_hats - kwargs: - d_feat: 6 - hidden_size: 64 - num_layers: 2 - dropout: 0.6 - n_epochs: 200 - lr: 1e-3 - early_stop: 20 - metric: loss - loss: mse - base_model: LSTM - seed: 0 - GPU: 0 - dataset: - class: DatasetH - module_path: qlib.data.dataset - kwargs: - handler: - class: ALPHA360 - module_path: qlib.contrib.data.handler - kwargs: *data_handler_config - segments: - train: [2008-01-01, 2014-12-31] - valid: [2015-01-01, 2016-12-31] - test: [2017-01-01, 2020-08-01] - record: - - class: SignalRecord - module_path: qlib.workflow.record_temp - kwargs: {} - - class: PortAnaRecord - module_path: qlib.workflow.record_temp - kwargs: - config: *port_analysis_config \ No newline at end of file diff --git a/qlib/contrib/model/pytorch_hats.py b/qlib/contrib/model/pytorch_hats.py deleted file mode 100644 index 7affea73c..000000000 --- a/qlib/contrib/model/pytorch_hats.py +++ /dev/null @@ -1,491 +0,0 @@ -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - - -from __future__ import division -from __future__ import print_function - -import os -import numpy as np -import pandas as pd -import copy -from ...utils import create_save_path -from ...log import get_module_logger - -import torch -import torch.nn as nn -import torch.optim as optim - -from ...model.base import Model -from ...data.dataset import DatasetH -from ...data.dataset.handler import DataHandlerLP - - -class HATS(Model): - """HATS Model - - Parameters - ---------- - d_feat : int - input dimension for each time step - metric: str - the evaluate metric used in early stop - optimizer : str - optimizer name - GPU : str - the GPU ID(s) used for training - """ - - def __init__( - self, - d_feat=6, - hidden_size=64, - num_layers=2, - dropout=0.5, - n_epochs=200, - lr=0.01, - metric="", - early_stop=20, - loss="mse", - base_model="GRU", - with_pretrain=True, - optimizer="adam", - GPU="0", - seed=0, - **kwargs - ): - # Set logger. - self.logger = get_module_logger("HATS") - self.logger.info("HATS pytorch version...") - - # set hyper-parameters. - self.d_feat = d_feat - self.hidden_size = hidden_size - self.num_layers = num_layers - self.dropout = dropout - self.n_epochs = n_epochs - self.lr = lr - self.metric = metric - self.early_stop = early_stop - self.optimizer = optimizer.lower() - self.loss = loss - self.base_model = base_model - self.with_pretrain = with_pretrain - self.visible_GPU = GPU - self.use_gpu = torch.cuda.is_available() - self.seed = seed - - self.logger.info( - "HATS parameters setting:" - "\nd_feat : {}" - "\nhidden_size : {}" - "\nnum_layers : {}" - "\ndropout : {}" - "\nn_epochs : {}" - "\nlr : {}" - "\nmetric : {}" - "\nearly_stop : {}" - "\noptimizer : {}" - "\nloss_type : {}" - "\nbase_model : {}" - "\nwith_pretrain : {}" - "\nvisible_GPU : {}" - "\nuse_GPU : {}" - "\nseed : {}".format( - d_feat, - hidden_size, - num_layers, - dropout, - n_epochs, - lr, - metric, - early_stop, - optimizer.lower(), - loss, - base_model, - with_pretrain, - GPU, - self.use_gpu, - seed, - ) - ) - - self.HATS_model = HATSModel( - d_feat=self.d_feat, - hidden_size=self.hidden_size, - num_layers=self.num_layers, - dropout=self.dropout, - base_model=self.base_model, - ) - if optimizer.lower() == "adam": - self.train_optimizer = optim.Adam(self.HATS_model.parameters(), lr=self.lr) - elif optimizer.lower() == "gd": - self.train_optimizer = optim.SGD(self.HATS_model.parameters(), lr=self.lr) - else: - raise NotImplementedError("optimizer {} is not supported!".format(optimizer)) - - self._fitted = False - if self.use_gpu: - self.HATS_model.cuda() - # set the visible GPU - if self.visible_GPU: - os.environ["CUDA_VISIBLE_DEVICES"] = self.visible_GPU - - def mse(self, pred, label): - loss = (pred - label) ** 2 - return torch.mean(loss) - - def loss_fn(self, pred, label): - mask = ~torch.isnan(label) - - if self.loss == "mse": - return self.mse(pred[mask], label[mask]) - - raise ValueError("unknown loss `%s`" % self.loss) - - def metric_fn(self, pred, label): - mask = torch.isfinite(label) - - if self.metric == "" or self.metric == "loss": # use loss - return -self.loss_fn(pred[mask], label[mask]) - - raise ValueError("unknown metric `%s`" % self.metric) - - def get_daily_inter(self, df, shuffle=False): - # organize the train data into daily inter as daily batches - daily_count = df.groupby(level=0).size().values - daily_index = np.roll(np.cumsum(daily_count), 1) - daily_index[0] = 0 - if shuffle: - # shuffle the daily inter data - daily_shuffle = list(zip(daily_index, daily_count)) - np.random.shuffle(daily_shuffle) - daily_index, daily_count = zip(*daily_shuffle) - return daily_index, daily_count - - def train_epoch(self, x_train, y_train): - - x_train_values = x_train.values - y_train_values = np.squeeze(y_train.values) - - self.HATS_model.train() - - # organize the train data into daily inter as daily batches - daily_index, daily_count = self.get_daily_inter(x_train, shuffle=True) - - for idx, count in zip(daily_index, daily_count): - batch = slice(idx, idx + count) - feature = torch.from_numpy(x_train_values[batch]).float() - label = torch.from_numpy(y_train_values[batch]).float() - - if self.use_gpu: - feature = feature.cuda() - label = label.cuda() - - pred = self.HATS_model(feature) - loss = self.loss_fn(pred, label) - - self.train_optimizer.zero_grad() - loss.backward() - torch.nn.utils.clip_grad_value_(self.HATS_model.parameters(), 3.0) - self.train_optimizer.step() - - def test_epoch(self, data_x, data_y): - - # prepare testing data - x_values = data_x.values - y_values = np.squeeze(data_y.values) - - self.HATS_model.eval() - - scores = [] - losses = [] - - # organize the test data into daily inter as daily batches - daily_index, daily_count = self.get_daily_inter(data_x, shuffle=False) - - for idx, count in zip(daily_index, daily_count): - batch = slice(idx, idx + count) - feature = torch.from_numpy(x_values[batch]).float() - label = torch.from_numpy(y_values[batch]).float() - - if self.use_gpu: - feature = feature.cuda() - label = label.cuda() - - pred = self.HATS_model(feature) - loss = self.loss_fn(pred, label) - losses.append(loss.item()) - - score = self.metric_fn(pred, label) - scores.append(score.item()) - - return np.mean(losses), np.mean(scores) - - def fit( - self, - dataset: DatasetH, - evals_result=dict(), - verbose=True, - save_path=None, - ): - - df_train, df_valid, df_test = dataset.prepare( - ["train", "valid", "test"], col_set=["feature", "label"], data_key=DataHandlerLP.DK_L - ) - - x_train, y_train = df_train["feature"], df_train["label"] - x_valid, y_valid = df_valid["feature"], df_valid["label"] - - if save_path == None: - save_path = create_save_path(save_path) - stop_steps = 0 - best_score = -np.inf - best_epoch = 0 - evals_result["train"] = [] - evals_result["valid"] = [] - - # load pretrained base_model - if self.with_pretrain: - self.logger.info("Loading pretrained model...") - if self.base_model == "LSTM": - from ...contrib.model.pytorch_lstm import LSTMModel - - pretrained_model = LSTMModel() - pretrained_model.load_state_dict(torch.load("benchmarks/LSTM/model_lstm_csi300.pkl")) - elif self.base_model == "GRU": - from ...contrib.model.pytorch_gru import GRUModel - - pretrained_model = GRUModel() - pretrained_model.load_state_dict(torch.load("benchmarks/GRU/model_gru_csi300.pkl")) - model_dict = self.HATS_model.state_dict() - pretrained_dict = {k: v for k, v in pretrained_model.state_dict().items() if k in model_dict} - model_dict.update(pretrained_dict) - self.HATS_model.load_state_dict(model_dict) - self.logger.info("Loading pretrained model Done...") - - # train - self.logger.info("training...") - self._fitted = True - - for step in range(self.n_epochs): - self.logger.info("Epoch%d:", step) - self.logger.info("training...") - self.train_epoch(x_train, y_train) - self.logger.info("evaluating...") - train_loss, train_score = self.test_epoch(x_train, y_train) - val_loss, val_score = self.test_epoch(x_valid, y_valid) - self.logger.info("train %.6f, valid %.6f" % (train_score, val_score)) - evals_result["train"].append(train_score) - evals_result["valid"].append(val_score) - - if val_score > best_score: - best_score = val_score - stop_steps = 0 - best_epoch = step - best_param = copy.deepcopy(self.HATS_model.state_dict()) - else: - stop_steps += 1 - if stop_steps >= self.early_stop: - self.logger.info("early stop") - break - - self.logger.info("best score: %.6lf @ %d" % (best_score, best_epoch)) - self.HATS_model.load_state_dict(best_param) - torch.save(best_param, save_path) - - if self.use_gpu: - torch.cuda.empty_cache() - - def predict(self, dataset): - if not self._fitted: - raise ValueError("model is not fitted yet!") - - x_test = dataset.prepare("test", col_set="feature") - index = x_test.index - self.HATS_model.eval() - x_values = x_test.values - sample_num = x_values.shape[0] - preds = [] - - # organize the data into daily inter as daily batches - daily_index, daily_count = self.get_daily_inter(x_test, shuffle=False) - - for idx, count in zip(daily_index, daily_count): - batch = slice(idx, idx + count) - x_batch = torch.from_numpy(x_values[batch]).float() - - if self.use_gpu: - x_batch = x_batch.cuda() - - with torch.no_grad(): - if self.use_gpu: - pred = self.HATS_model(x_batch).detach().cpu().numpy() - else: - pred = self.HATS_model(x_batch).detach().numpy() - - preds.append(pred) - - return pd.Series(np.concatenate(preds), index=index) - - -class HATSModel(nn.Module): - def __init__(self, d_feat=6, hidden_size=64, num_layers=2, dropout=0.0, base_model="GRU"): - super().__init__() - - if base_model == "GRU": - self.model = nn.GRU( - input_size=d_feat, - hidden_size=hidden_size, - num_layers=num_layers, - batch_first=True, - dropout=dropout, - ) - elif base_model == "LSTM": - self.model = nn.LSTM( - input_size=d_feat, - hidden_size=hidden_size, - num_layers=num_layers, - batch_first=True, - dropout=dropout, - ) - else: - raise ValueError("unknown base model name `%s`" % base_model) - - self.hidden_size = hidden_size - self.bn1 = nn.BatchNorm1d(num_features=hidden_size, track_running_stats=False) - self.fc = nn.Linear(hidden_size, hidden_size) - self.bn2 = nn.BatchNorm1d(num_features=hidden_size, track_running_stats=False) - self.fc_out = nn.Linear(hidden_size, 1) - self.leaky_relu = nn.LeakyReLU() - self.softmax = nn.Softmax(dim=1) - self.d_feat = d_feat - - num_head_att = [1] * num_layers - hidden_dim = [hidden_size] * num_layers - dims = [d_feat] + [d * nh for (d, nh) in zip(hidden_dim, num_head_att[:-1])] + [num_head_att[-1]] - in_dims = dims[:-1] - out_dims = [d // nh for (d, nh) in zip(dims[1:], num_head_att)] - self.attn = nn.ModuleList( - [GraphAttention(i, o, nh, dropout) for (i, o, nh) in zip(in_dims, out_dims, num_head_att)] - ) - self.bns = nn.ModuleList([nn.BatchNorm1d(dim) for dim in dims[1:-1]]) - self.dropout = nn.Dropout(dropout) - self.elu = nn.ELU() - - def forward(self, x): - x = x.reshape(len(x), self.d_feat, -1) # [N, F, T] - x = x.permute(0, 2, 1) # [N, T, F] - out, _ = self.model(x) - hidden = out[:, -1, :] - hidden = self.bn1(hidden) - attention = GraphAttention.cal_attention(hidden, hidden) - output = attention.mm(hidden) - output = self.fc(output) - output = self.bn2(output) - output = self.leaky_relu(output) - return self.fc_out(output).squeeze() - - -class GraphAttention(nn.Module): - def __init__(self, input_dim, output_dim, num_heads, dropout=0.5): - - super().__init__() - - """ - Parameters - ---------- - input_dim : int - Dimension of input node features. - output_dim : int - Dimension of output node features. - num_heads : list of ints - Number of attention heads in each hidden layer and output layer. Must be non empty. Note that len(num_heads) = len(hidden_dims)+1. - dropout : float - Dropout rate. Default: 0.5. - """ - - self.input_dim = input_dim - self.output_dim = output_dim - self.num_heads = num_heads - - self.fcs = nn.ModuleList([nn.Linear(input_dim, output_dim) for _ in range(num_heads)]) - self.a = nn.ModuleList([nn.Linear(2 * output_dim, 1) for _ in range(num_heads)]) - - self.dropout = nn.Dropout(dropout) - self.softmax = nn.Softmax(dim=0) - self.leakyrelu = nn.LeakyReLU() - - def forward(self, features, nodes, mappings, rows): - - """ - Parameters - ---------- - features : torch.Tensor - An (n' x input_dim) tensor of input node features. - nodes : list of numpy array - nodes[i] is an array of the nodes in the ith layer of the - computation graph. - mappings : list of dictionary - mappings[i] is a dictionary mappings node v (labelled 0 to |V|-1) - in nodes[i] to its position in nodes[i]. For example, - if nodes[i] = [2,5], then mappings[i][2] = 0 and - mappings[i][5] = 1. - rows : numpy array - rows[i] is an array of neighbors of node i. - Returns - ------- - out : torch.Tensor - An (len(node_layers[-1]) x output_dim) tensor of output node features. - """ - - nprime = features.shape[0] - rows = [np.array([mappings[v] for v in row], dtype=np.int64) for row in rows] - sum_degs = np.hstack(([0], np.cumsum([len(row) for row in rows]))) - mapped_nodes = [mappings[v] for v in nodes] - indices = torch.LongTensor([[v, c] for (v, row) in zip(mapped_nodes, rows) for c in row]).t() - - out = [] - for k in range(self.num_heads): - h = self.fcs[k](features) - - nbr_h = torch.cat(tuple([h[row] for row in rows]), dim=0) - self_h = torch.cat( - tuple([h[mappings[nodes[i]]].repeat(len(row), 1) for (i, row) in enumerate(rows)]), dim=0 - ) - cat_h = torch.cat((self_h, nbr_h), dim=1) - - e = self.leakyrelu(self.a[k](cat_h)) - - alpha = [self.softmax(e[lo:hi]) for (lo, hi) in zip(sum_degs, sum_degs[1:])] - alpha = torch.cat(tuple(alpha), dim=0) - alpha = alpha.squeeze(1) - alpha = self.dropout(alpha) - - adj = torch.sparse.FloatTensor(indices, alpha, torch.Size([nprime, nprime])) - out.append(torch.sparse.mm(adj, h)[mapped_nodes]) - - return out - - @staticmethod - def cal_attention(x, y): - att_x = torch.mean(x, dim=1).reshape(-1, 1) - att_y = torch.mean(y, dim=1).reshape(-1, 1) - att = att_x.mm(torch.t(att_y)) - return ( - torch.mean( - x.reshape(x.shape[0], 1, x.shape[1]).repeat(1, y.shape[0], 1) - * y.reshape(1, y.shape[0], y.shape[1]).repeat(x.shape[0], 1, 1), - dim=2, - ) - - att - ) From eaa9f1a80dd764370f603064cd3f58f037b050bf Mon Sep 17 00:00:00 2001 From: Jactus Date: Sat, 28 Nov 2020 10:50:39 +0800 Subject: [PATCH 229/241] Fix tft --- README.md | 2 -- examples/benchmarks/TFT/tft.py | 3 +-- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/README.md b/README.md index 884bbb5c0..123134319 100644 --- a/README.md +++ b/README.md @@ -201,9 +201,7 @@ Here is a list of models built on `Qlib`. - [LSTM based on pytorcn](qlib/contrib/model/pytorch_lstm.py) - [ALSTM based on pytorcn](qlib/contrib/model/pytorch_alstm.py) - [GATs based on pytorch](qlib/contrib/model/pytorch_gats.py) -- [TabNet based on pytorch](qlib/contrib/model/tabnet.py) - [SFM based on pytorch](qlib/contrib/model/pytorch_sfm.py) -- [HATs based on pytorch](qlib/contrib/model/pytorch_hats.py) - [TFT based on tensorflow](examples/benchmarks/TFT/tft.py) Your PR of new Quant models is highly welcomed. diff --git a/examples/benchmarks/TFT/tft.py b/examples/benchmarks/TFT/tft.py index a3b4fc919..3387a5947 100644 --- a/examples/benchmarks/TFT/tft.py +++ b/examples/benchmarks/TFT/tft.py @@ -233,9 +233,8 @@ class TFTModel(ModelFT): tf.keras.backend.set_session(default_keras_session) predict = format_score(p90_forecast, "pred", 0) # self.label_shift - label = format_score(targets, "label", 0) # ===========================Predicting Process=========================== - return predict, label + return predict def finetune(self, dataset: DatasetH): """ From ac96dde4c992777961f5993898123ad2946fd320 Mon Sep 17 00:00:00 2001 From: Jactus Date: Sat, 28 Nov 2020 10:53:24 +0800 Subject: [PATCH 230/241] Rename DNN --- examples/benchmarks/{DNN => MLP}/requirements.txt | 0 .../workflow_config_dnn.yaml => MLP/workflow_config_mlp.yaml} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename examples/benchmarks/{DNN => MLP}/requirements.txt (100%) rename examples/benchmarks/{DNN/workflow_config_dnn.yaml => MLP/workflow_config_mlp.yaml} (100%) diff --git a/examples/benchmarks/DNN/requirements.txt b/examples/benchmarks/MLP/requirements.txt similarity index 100% rename from examples/benchmarks/DNN/requirements.txt rename to examples/benchmarks/MLP/requirements.txt diff --git a/examples/benchmarks/DNN/workflow_config_dnn.yaml b/examples/benchmarks/MLP/workflow_config_mlp.yaml similarity index 100% rename from examples/benchmarks/DNN/workflow_config_dnn.yaml rename to examples/benchmarks/MLP/workflow_config_mlp.yaml From 627fc628e082094044970e652a2a97ac5ae81450 Mon Sep 17 00:00:00 2001 From: zhupr Date: Sat, 28 Nov 2020 12:57:00 +0800 Subject: [PATCH 231/241] Fix get_data.py detection --- examples/workflow_by_code.ipynb | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/examples/workflow_by_code.ipynb b/examples/workflow_by_code.ipynb index d5711e0b5..5a992e339 100644 --- a/examples/workflow_by_code.ipynb +++ b/examples/workflow_by_code.ipynb @@ -1,11 +1,11 @@ { "cells": [ { + "cell_type": "markdown", + "metadata": {}, "source": [ "\"Open" - ], - "cell_type": "markdown", - "metadata": {} + ] }, { "cell_type": "code", @@ -28,16 +28,17 @@ "import sys, site\n", "from pathlib import Path\n", "\n", - "TEMP_CODE_DIR = str(Path(\"~/tmp/qlib_code\").expanduser().resolve())\n", "\n", "try:\n", " import qlib\n", - " scripts_dir = Path.cwd().parent.joinpath(\"scripts\")\n", "except ImportError:\n", " # install qlib\n", " ! pip install pyqlib\n", " # reload\n", " site.main()\n", + "\n", + "scripts_dir = Path.cwd().parent.joinpath(\"scripts\")\n", + "if not scripts_dir.joinpath(\"get_data.py\").exists():\n", " # download get_data.py script\n", " scripts_dir = Path(\"~/tmp/qlib_code/scripts\").expanduser().resolve()\n", " scripts_dir.mkdir(parents=True, exist_ok=True)\n", @@ -376,4 +377,4 @@ }, "nbformat": 4, "nbformat_minor": 4 -} \ No newline at end of file +} From 22c38066722d955b58a99e129fe5788b440db7c5 Mon Sep 17 00:00:00 2001 From: Young Date: Sat, 28 Nov 2020 05:56:01 +0000 Subject: [PATCH 232/241] update docs --- README.md | 7 +++++-- docs/component/workflow.rst | 5 +++-- docs/introduction/quick.rst | 4 ++-- docs/start/initialization.rst | 9 +++++---- docs/start/integration.rst | 5 +++-- qlib/contrib/model/gbdt.py | 1 + 6 files changed, 19 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index c890afaca..4f2509188 100644 --- a/README.md +++ b/README.md @@ -228,8 +228,11 @@ Your PR of new Quant models is highly welcomed. # Quant Dataset Zoo Dataset plays a very important role in Quant. Here is a list of the datasets built on `Qlib`. -- [Alpha360](./qlib/contrib/data/handler.py) -- [Alpha158](./qlib/contrib/data/handler.py) + +| Dataset | US Market | China Market | +| -- | -- | -- | +| [Alpha360](./qlib/contrib/data/handler.py) | √ | √ | +| [Alpha158](./qlib/contrib/data/handler.py) | √ | √ | [Here](https://qlib.readthedocs.io/en/latest/advanced/alpha.html) is a tutorial to build dataset with `Qlib`. Your PR to build new Quant dataset is highly welcomed. diff --git a/docs/component/workflow.rst b/docs/component/workflow.rst index 4ca010851..c44f1100f 100644 --- a/docs/component/workflow.rst +++ b/docs/component/workflow.rst @@ -19,9 +19,10 @@ With ``qrun``, user can easily run an `experiment`, which includes the following - Processing - Slicing - Model - - Training and inference (static or rolling) + - Training and inference - Saving & loading - Evaluation + - Forecast signal analysis - Backtest For each `experiment`, ``Qlib`` has a complete system to tracking all the information as well as artifacts generated during training, inference and evaluation phase. For more information about how Qlib handles `experiment`, please refer to the related document: `Recorder: Experiment Management <../component/recorder.html>`_. @@ -276,4 +277,4 @@ Here is the configuration details of different `Record Template` such as ``Signa kwargs: config: *port_analysis_config -For more information about the ``Record`` module in ``Qlib``, user can refer to the related document: `Record <../component/recorder.html#record-template>`_. \ No newline at end of file +For more information about the ``Record`` module in ``Qlib``, user can refer to the related document: `Record <../component/recorder.html#record-template>`_. diff --git a/docs/introduction/quick.rst b/docs/introduction/quick.rst index 32752fd83..ee906b6f6 100644 --- a/docs/introduction/quick.rst +++ b/docs/introduction/quick.rst @@ -61,7 +61,7 @@ Auto Quant Research Workflow - Workflow result - The result of ``qrun`` is as follows, which is also the result of ``Intraday Trading``. Please refer to `Intraday Trading <../component/backtest.html>`_. for more details about the result. + The result of ``qrun`` is as follows, which is also the typical result of ``Forecast model(alpha)``. Please refer to `Intraday Trading <../component/backtest.html>`_. for more details about the result. .. code-block:: python @@ -91,4 +91,4 @@ Auto Quant Research Workflow Custom Model Integration =============================================== -``Qlib`` provides several models such as ``lightGBM`` and ``DNN`` model as the baseline of ``Interday Model``. In addition to the default model, users can integrate their own custom models into ``Qlib``. If users are interested in the custom model, please refer to `Custom Model Integration <../start/integration.html>`_. +``Qlib`` provides a batch of models (such as ``lightGBM`` and ``DNN`` models) as examples of ``Interday Model``. In addition to the default model, users can integrate their own custom models into ``Qlib``. If users are interested in the custom model, please refer to `Custom Model Integration <../start/integration.html>`_. diff --git a/docs/start/initialization.rst b/docs/start/initialization.rst index 423d7edf8..05a329df7 100644 --- a/docs/start/initialization.rst +++ b/docs/start/initialization.rst @@ -63,13 +63,14 @@ Besides `provider_uri` and `region`, `qlib.init` has other parameters. The follo If Qlib fails to connect redis via `redis_host` and `redis_port`, cache mechanism will not be used! Please refer to `Cache <../component/data.html#cache>`_ for details. - `exp_manager` Type: dict, optional parameter, the setting of `experiment manager` to be used in qlib. Users can specify an experiment manager class, as well as the tracking URI for all the experiments. However, please be aware that we only support input of a dictionary in the following style for `exp_manager`. For more information about `exp_manager`, users can refer to `Recorder: Experiment Management <../component/recorder.html>`_. - :: + .. code-block:: Python - { + # For example, if you want to set your tracking_uri to a , you can initialize qlib below + qlib.init(provider_uri=provider_uri, region=REG_CN, exp_manager= { "class": "MLflowExpManager", "module_path": "qlib.workflow.expm", "kwargs": { - "uri": "python_execution_path/mlruns"), + "uri": "python_execution_path/mlruns", "default_exp_name": "Experiment", } - } \ No newline at end of file + }) diff --git a/docs/start/integration.rst b/docs/start/integration.rst index 102d88425..09e8648f7 100644 --- a/docs/start/integration.rst +++ b/docs/start/integration.rst @@ -5,7 +5,7 @@ Custom Model Integration Introduction =================== -``Qlib``'s `Model Zoo` includes models such as ``LightGBM``, ``DNN``, ``LSTM``, etc.. These models are treated as the baselines of ``Interday Model``. In addition to the default models ``Qlib`` provide, users can integrate their own custom models into ``Qlib``. +``Qlib``'s `Model Zoo` includes models such as ``LightGBM``, ``DNN``, ``LSTM``, etc.. These models are examples of ``Interday Model``. In addition to the default models ``Qlib`` provide, users can integrate their own custom models into ``Qlib``. Users can integrate their own custom models according to the following steps. @@ -87,6 +87,7 @@ The Custom models need to inherit `qlib.model.base.Model <../reference/api.html# .. code-block:: Python def finetune(self, dataset: DatasetH, num_boost_round=10, verbose_eval=20): + # Based on existing model and finetune by train more rounds dtrain, _ = self._prepare_data(dataset) self.model = lgb.train( self.params, @@ -101,7 +102,7 @@ The Custom models need to inherit `qlib.model.base.Model <../reference/api.html# Configuration File ======================= -The configuration file is described in detail in the `Workflow <../component/workflow.html#complete-example>`_ document. In order to integrate the custom model into ``Qlib``, users need to modify the "model" field in the configuration file. +The configuration file is described in detail in the `Workflow <../component/workflow.html#complete-example>`_ document. In order to integrate the custom model into ``Qlib``, users need to modify the "model" field in the configuration file. The configuration describes which models to use and how we can initialize it. - Example: The following example describes the `model` field of configuration file about the custom lightgbm model mentioned above, where `module_path` is the module path, `class` is the class name, and `args` is the hyperparameter passed into the __init__ method. All parameters in the field is passed to `self._params` by `\*\*kwargs` in `__init__` except `loss = mse`. diff --git a/qlib/contrib/model/gbdt.py b/qlib/contrib/model/gbdt.py index e52c05906..058d9a0e3 100644 --- a/qlib/contrib/model/gbdt.py +++ b/qlib/contrib/model/gbdt.py @@ -80,6 +80,7 @@ class LGBModel(ModelFT): verbose_eval : int verbose level """ + # Based on existing model and finetune by train more rounds dtrain, _ = self._prepare_data(dataset) self.model = lgb.train( self.params, From 3f47a282ccaa343b38a36d0fcc393b8beba49eb2 Mon Sep 17 00:00:00 2001 From: Young Date: Sat, 28 Nov 2020 08:17:18 +0000 Subject: [PATCH 233/241] update version number & model license --- examples/workflow_by_code_finetune.py | 128 -------------------------- qlib/__init__.py | 2 +- qlib/contrib/model/catboost_model.py | 13 +-- qlib/contrib/model/pytorch_sfm.py | 13 +-- qlib/contrib/model/xgboost.py | 13 +-- qlib/model/base.py | 17 ++++ setup.py | 2 +- 7 files changed, 25 insertions(+), 163 deletions(-) delete mode 100644 examples/workflow_by_code_finetune.py diff --git a/examples/workflow_by_code_finetune.py b/examples/workflow_by_code_finetune.py deleted file mode 100644 index 5e7c179ae..000000000 --- a/examples/workflow_by_code_finetune.py +++ /dev/null @@ -1,128 +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 -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": "2008-01-01", - "end_time": "2020-08-01", - "fit_start_time": "2008-01-01", - "fit_end_time": "2014-12-31", - "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": ("2008-01-01", "2014-12-31"), - "valid": ("2015-01-01", "2016-12-31"), - "test": ("2017-01-01", "2020-08-01"), - }, - }, - }, - } - - 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, - }, - } - - # model initiaiton - model = init_instance_by_config(task["model"]) - dataset = init_instance_by_config(task["dataset"]) - - # start exp to train init model - with R.start(experiment_name="init models"): - model.fit(dataset) - R.save_objects(init_model=model) - rid = R.get_recorder().id - - # Finetune model based on previous trained model - with R.start(experiment_name="finetune model"): - recorder = R.get_recorder(rid, experiment_name="init models") - model = recorder.load_object("init_model") - model.finetune(dataset, num_boost_round=10) - R.save_objects(model=model) - - # 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/__init__.py b/qlib/__init__.py index 3fecc85c3..2b8989303 100644 --- a/qlib/__init__.py +++ b/qlib/__init__.py @@ -2,7 +2,7 @@ # Licensed under the MIT License. -__version__ = "0.5.1.dev0" +__version__ = "0.6.0.alpha" import os import re diff --git a/qlib/contrib/model/catboost_model.py b/qlib/contrib/model/catboost_model.py index 01830d1b5..d57c32b70 100644 --- a/qlib/contrib/model/catboost_model.py +++ b/qlib/contrib/model/catboost_model.py @@ -1,14 +1,5 @@ -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. import numpy as np import pandas as pd diff --git a/qlib/contrib/model/pytorch_sfm.py b/qlib/contrib/model/pytorch_sfm.py index 8fddd1612..228c0aee5 100644 --- a/qlib/contrib/model/pytorch_sfm.py +++ b/qlib/contrib/model/pytorch_sfm.py @@ -1,15 +1,6 @@ # Copyright (c) Microsoft Corporation. -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. +# Licensed under the MIT License. + from __future__ import division from __future__ import print_function diff --git a/qlib/contrib/model/xgboost.py b/qlib/contrib/model/xgboost.py index c9e45d4ac..ba2e5789b 100755 --- a/qlib/contrib/model/xgboost.py +++ b/qlib/contrib/model/xgboost.py @@ -1,14 +1,5 @@ -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. import numpy as np import pandas as pd diff --git a/qlib/model/base.py b/qlib/model/base.py index fd220cd7e..c9bef1152 100644 --- a/qlib/model/base.py +++ b/qlib/model/base.py @@ -56,6 +56,23 @@ class ModelFT(Model): def finetune(self, dataset: Dataset): """finetune model based given dataset + A typical use case of finetuning model with qlib.workflow.R + + .. code-block:: python + + # start exp to train init model + with R.start(experiment_name="init models"): + model.fit(dataset) + R.save_objects(init_model=model) + rid = R.get_recorder().id + + # Finetune model based on previous trained model + with R.start(experiment_name="finetune model"): + recorder = R.get_recorder(rid, experiment_name="init models") + model = recorder.load_object("init_model") + model.finetune(dataset, num_boost_round=10) + + Parameters ---------- dataset : Dataset diff --git a/setup.py b/setup.py index 3438781b2..0696a766f 100644 --- a/setup.py +++ b/setup.py @@ -12,7 +12,7 @@ from setuptools import find_packages, setup, Extension NAME = "pyqlib" DESCRIPTION = "A Quantitative-research Platform" REQUIRES_PYTHON = ">=3.5.0" -VERSION = "0.5.1.dev0" +VERSION = "0.6.0.alpha" # Detect Cython try: From 680e8f9260c763c7ef30442984d8616f95e308f3 Mon Sep 17 00:00:00 2001 From: Jactus Date: Sat, 28 Nov 2020 16:36:51 +0800 Subject: [PATCH 234/241] Update docs --- README.md | 4 +++- docs/component/data.rst | 2 +- docs/component/model.rst | 2 +- docs/introduction/quick.rst | 2 +- docs/start/integration.rst | 2 +- examples/run_all_model.py | 8 ++++---- 6 files changed, 11 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 123134319..35028328f 100644 --- a/README.md +++ b/README.md @@ -207,10 +207,12 @@ Here is a list of models built on `Qlib`. Your PR of new Quant models is highly welcomed. ## Run a single model +All the models listed above are runnable with ``Qlib``. Users can find the config files we provide and some details about the model through the [benchmarks](examples/benchmarks) folder. More information can be retrieved at the model files listed above. + `Qlib` provides three different ways to run a single model, users can pick the one that fits their cases best: - User can use the tool `qrun` mentioned above to run a model's workflow based from a config file. - User can create a `workflow_by_code` python script based on the [one](examples/workflow_by_code.py) listed in the `examples` folder. -- User can use the script [`run_all_model.py`](examples/run_all_model.py) listed in the `examples` folder to run a model. Here is an example of the specific shell command to be used: `python run_all_model.py --models=lightgbm`. For more use cases, please refer to the file's [docstrings](examples/run_all_model.py). +- User can use the script [`run_all_model.py`](examples/run_all_model.py) listed in the `examples` folder to run a model. Here is an example of the specific shell command to be used: `python run_all_model.py --models=lightgbm`, where the `--models` arguments can take any number of models listed above. For more use cases, please refer to the file's [docstrings](examples/run_all_model.py). ## Run multiple models `Qlib` also provides a script [`run_all_model.py`](examples/run_all_model.py) which can run multiple models for several iterations. (**Note**: the script only supprots *Linux* now. Other OS will be supported in the future.) diff --git a/docs/component/data.rst b/docs/component/data.rst index aa01fe226..55d6c7207 100644 --- a/docs/component/data.rst +++ b/docs/component/data.rst @@ -321,7 +321,7 @@ Dataset The ``Dataset`` module in ``Qlib`` aims to prepare data for model training and inferencing. -The motivation of this module is that we want to maximize the flexibility of of different models to handle data that are suitable for themselves. This module gives the model the rights to process their data in an unique way. For instance, models such as ``GBDT`` may work well on data that contains `nan` or `None` value, while neural networks such as ``DNN`` will break down on such data. +The motivation of this module is that we want to maximize the flexibility of of different models to handle data that are suitable for themselves. This module gives the model the rights to process their data in an unique way. For instance, models such as ``GBDT`` may work well on data that contains `nan` or `None` value, while neural networks such as ``MLP`` will break down on such data. The ``DatasetH`` class is the `dataset` with `Data Handler`. Here is the most important interface of the class: diff --git a/docs/component/model.rst b/docs/component/model.rst index b4e341df8..e4aa4ca91 100644 --- a/docs/component/model.rst +++ b/docs/component/model.rst @@ -63,7 +63,7 @@ For other interfaces such as `finetune`, please refer to `Model API <../referenc Example ================== -``Qlib``'s `Model Zoo` includes models such as ``LightGBM``, ``DNN``, ``LSTM``, etc.. These models are treated as the baselines of ``Interday Model``. The following steps show how to run`` LightGBM`` as an independent module. +``Qlib``'s `Model Zoo` includes models such as ``LightGBM``, ``MLP``, ``LSTM``, etc.. These models are treated as the baselines of ``Interday Model``. The following steps show how to run`` LightGBM`` as an independent module. - Initialize ``Qlib`` with `qlib.init` first, please refer to `Initialization <../start/initialization.html>`_. - Run the following code to get the `prediction score` `pred_score` diff --git a/docs/introduction/quick.rst b/docs/introduction/quick.rst index 32752fd83..55835b970 100644 --- a/docs/introduction/quick.rst +++ b/docs/introduction/quick.rst @@ -91,4 +91,4 @@ Auto Quant Research Workflow Custom Model Integration =============================================== -``Qlib`` provides several models such as ``lightGBM`` and ``DNN`` model as the baseline of ``Interday Model``. In addition to the default model, users can integrate their own custom models into ``Qlib``. If users are interested in the custom model, please refer to `Custom Model Integration <../start/integration.html>`_. +``Qlib`` provides several models such as ``lightGBM`` and ``MLP`` model as the baseline of ``Interday Model``. In addition to the default model, users can integrate their own custom models into ``Qlib``. If users are interested in the custom model, please refer to `Custom Model Integration <../start/integration.html>`_. diff --git a/docs/start/integration.rst b/docs/start/integration.rst index 102d88425..437c5ef6a 100644 --- a/docs/start/integration.rst +++ b/docs/start/integration.rst @@ -5,7 +5,7 @@ Custom Model Integration Introduction =================== -``Qlib``'s `Model Zoo` includes models such as ``LightGBM``, ``DNN``, ``LSTM``, etc.. These models are treated as the baselines of ``Interday Model``. In addition to the default models ``Qlib`` provide, users can integrate their own custom models into ``Qlib``. +``Qlib``'s `Model Zoo` includes models such as ``LightGBM``, ``MLP``, ``LSTM``, etc.. These models are treated as the baselines of ``Interday Model``. In addition to the default models ``Qlib`` provide, users can integrate their own custom models into ``Qlib``. Users can integrate their own custom models according to the following steps. diff --git a/examples/run_all_model.py b/examples/run_all_model.py index 05839a125..8843573ab 100644 --- a/examples/run_all_model.py +++ b/examples/run_all_model.py @@ -204,16 +204,16 @@ def run(times=1, models=None, exclude=False): python run_all_model.py 3 # Case 2 - run specific models multiple times - python run_all_model.py 3 dnn + python run_all_model.py 3 mlp # Case 3 - run other models except those are given as arguments for multiple times - python run_all_model.py 3 [dnn,tft,lstm] True + python run_all_model.py 3 [mlp,tft,lstm] True # Case 4 - run specific models for one time - python run_all_model.py --models=[dnn,lightgbm] + python run_all_model.py --models=[mlp,lightgbm] # Case 5 - run other models except those are given as aruments for one time - python run_all_model.py --models=[dnn,tft,sfm] --exclude=True + python run_all_model.py --models=[mlp,tft,sfm] --exclude=True """ # get all folders From e333b786507a53b3ad99023b412c5eff77bbde04 Mon Sep 17 00:00:00 2001 From: Wendi Li Date: Sat, 28 Nov 2020 22:51:22 +0800 Subject: [PATCH 235/241] Update tft.py --- examples/benchmarks/TFT/tft.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/examples/benchmarks/TFT/tft.py b/examples/benchmarks/TFT/tft.py index 3387a5947..b5398e7f2 100644 --- a/examples/benchmarks/TFT/tft.py +++ b/examples/benchmarks/TFT/tft.py @@ -82,7 +82,7 @@ def process_predicted(df, col_name): """ df_res = df.copy() - df_res = df_res.rename(columns={"forecast_time": "datetime", "identifier": "instrument", "t+5": col_name}) + df_res = df_res.rename(columns={"forecast_time": "datetime", "identifier": "instrument", "t+4": col_name}) df_res = df_res.set_index(["datetime", "instrument"]).sort_index() df_res = df_res[[col_name]] return df_res @@ -232,7 +232,9 @@ class TFTModel(ModelFT): p90_forecast = self.data_formatter.format_predictions(output_map["p90"]) tf.keras.backend.set_session(default_keras_session) - predict = format_score(p90_forecast, "pred", 0) # self.label_shift + predict50 = format_score(p50_forecast, "pred", 1) + predict90 = format_score(p90_forecast, "pred", 1) + predict = (predict50 + predict90)/2 # self.label_shift # ===========================Predicting Process=========================== return predict From 30ab4a8d8b4aa9a73e0d5cfa50a3effb22a81c35 Mon Sep 17 00:00:00 2001 From: Wendi Li Date: Sat, 28 Nov 2020 22:52:40 +0800 Subject: [PATCH 236/241] Update qlib_Alpha158.py --- .../TFT/data_formatters/qlib_Alpha158.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/examples/benchmarks/TFT/data_formatters/qlib_Alpha158.py b/examples/benchmarks/TFT/data_formatters/qlib_Alpha158.py index da3d14343..44a9284f7 100644 --- a/examples/benchmarks/TFT/data_formatters/qlib_Alpha158.py +++ b/examples/benchmarks/TFT/data_formatters/qlib_Alpha158.py @@ -194,10 +194,10 @@ class Alpha158Formatter(GenericDataFormatter): """Returns fixed model parameters for experiments.""" fixed_params = { - "total_time_steps": 16 + 6, - "num_encoder_steps": 16, + "total_time_steps": 6 + 6, + "num_encoder_steps": 6, "num_epochs": 100, - "early_stopping_patience": 5, + "early_stopping_patience": 10, "multiprocessing_workers": 5, } @@ -207,11 +207,11 @@ class Alpha158Formatter(GenericDataFormatter): """Returns default optimised model parameters.""" model_params = { - "dropout_rate": 0.3, - "hidden_layer_size": 160, - "learning_rate": 0.001, - "minibatch_size": 64, - "max_gradient_norm": 0.01, + "dropout_rate": 0.4, + "hidden_layer_size": 16, + "learning_rate": 0.0001, + "minibatch_size": 128, + "max_gradient_norm": 0.0135, "num_heads": 1, "stack_size": 1, } From fdf0f9a1827615e2e513c5d39dafdbf84157b804 Mon Sep 17 00:00:00 2001 From: v-blin Date: Sat, 28 Nov 2020 14:55:16 +0000 Subject: [PATCH 237/241] Update TFT --- examples/benchmarks/TFT/libs/tft_model.py | 12 ++---------- examples/benchmarks/TFT/tft.py | 2 +- 2 files changed, 3 insertions(+), 11 deletions(-) diff --git a/examples/benchmarks/TFT/libs/tft_model.py b/examples/benchmarks/TFT/libs/tft_model.py index 658bae60f..3e6e4346e 100644 --- a/examples/benchmarks/TFT/libs/tft_model.py +++ b/examples/benchmarks/TFT/libs/tft_model.py @@ -721,12 +721,7 @@ class TemporalFusionTransformer(object): encoder_steps = self.num_encoder_steps # Inputs. - all_inputs = tf.keras.layers.Input( - shape=( - time_steps, - combined_input_size, - ) - ) + all_inputs = tf.keras.layers.Input(shape=(time_steps, combined_input_size,)) unknown_inputs, known_combined_layer, obs_inputs, static_inputs = self.get_tft_embeddings(all_inputs) @@ -866,10 +861,7 @@ class TemporalFusionTransformer(object): """Returns LSTM cell initialized with default parameters.""" if self.use_cudnn: lstm = tf.keras.layers.CuDNNLSTM( - self.hidden_layer_size, - return_sequences=True, - return_state=return_state, - stateful=False, + self.hidden_layer_size, return_sequences=True, return_state=return_state, stateful=False, ) else: lstm = tf.keras.layers.LSTM( diff --git a/examples/benchmarks/TFT/tft.py b/examples/benchmarks/TFT/tft.py index b5398e7f2..388ec7f14 100644 --- a/examples/benchmarks/TFT/tft.py +++ b/examples/benchmarks/TFT/tft.py @@ -234,7 +234,7 @@ class TFTModel(ModelFT): predict50 = format_score(p50_forecast, "pred", 1) predict90 = format_score(p90_forecast, "pred", 1) - predict = (predict50 + predict90)/2 # self.label_shift + predict = (predict50 + predict90) / 2 # self.label_shift # ===========================Predicting Process=========================== return predict From 0fb0109f9ce6f9989eca5f6322d7eee43633f031 Mon Sep 17 00:00:00 2001 From: Dong Zhou Date: Sun, 29 Nov 2020 17:17:03 +0800 Subject: [PATCH 238/241] black format --- examples/benchmarks/TFT/libs/tft_model.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/examples/benchmarks/TFT/libs/tft_model.py b/examples/benchmarks/TFT/libs/tft_model.py index 3e6e4346e..658bae60f 100644 --- a/examples/benchmarks/TFT/libs/tft_model.py +++ b/examples/benchmarks/TFT/libs/tft_model.py @@ -721,7 +721,12 @@ class TemporalFusionTransformer(object): encoder_steps = self.num_encoder_steps # Inputs. - all_inputs = tf.keras.layers.Input(shape=(time_steps, combined_input_size,)) + all_inputs = tf.keras.layers.Input( + shape=( + time_steps, + combined_input_size, + ) + ) unknown_inputs, known_combined_layer, obs_inputs, static_inputs = self.get_tft_embeddings(all_inputs) @@ -861,7 +866,10 @@ class TemporalFusionTransformer(object): """Returns LSTM cell initialized with default parameters.""" if self.use_cudnn: lstm = tf.keras.layers.CuDNNLSTM( - self.hidden_layer_size, return_sequences=True, return_state=return_state, stateful=False, + self.hidden_layer_size, + return_sequences=True, + return_state=return_state, + stateful=False, ) else: lstm = tf.keras.layers.LSTM( From 89f907bf6c0a3bad6b380ee5eb34d87d15c2ecc6 Mon Sep 17 00:00:00 2001 From: Young Date: Sun, 29 Nov 2020 09:21:46 +0000 Subject: [PATCH 239/241] set the task base class --- README.md | 2 +- qlib/model/task.py | 163 +++++++-------------------------------------- 2 files changed, 25 insertions(+), 140 deletions(-) diff --git a/README.md b/README.md index 89d14e9eb..f9fdf1719 100644 --- a/README.md +++ b/README.md @@ -210,7 +210,7 @@ Your PR of new Quant models is highly welcomed. `Qlib` provides three different ways to run a single model, users can pick the one that fits their cases best: - User can use the tool `qrun` mentioned above to run a model's workflow based from a config file. - User can create a `workflow_by_code` python script based on the [one](examples/workflow_by_code.py) listed in the `examples` folder. -- User can use the script [`run_all_model.py`](examples/run_all_model.py) listed in the `examples` folder to run a model. Here is an example of the specific shell command to be used: `python run_all_model.py --models=lightgbm`. For more use cases, please refer to the file's [docstrings](examples/run_all_model.py). +- User can use the script [`run_all_model.py`](examples/run_all_model.py) listed in the `examples` folder to run a model. Here is an example of the specific shell command to be used: `python run_all_model.py --models=lightgbm`(the available models can be found in [benchmarks](examples/benchmarks/)). For more use cases, please refer to the file's [docstrings](examples/run_all_model.py). ## Run multiple models `Qlib` also provides a script [`run_all_model.py`](examples/run_all_model.py) which can run multiple models for several iterations. (**Note**: the script only supprots *Linux* now. Other OS will be supported in the future.) diff --git a/qlib/model/task.py b/qlib/model/task.py index e66159233..f29f513a4 100644 --- a/qlib/model/task.py +++ b/qlib/model/task.py @@ -1,142 +1,27 @@ -''' -Please implement similar function here - -# Rolling relealted +import abc +import typing - def split_rolling_periods( - self, - train_start_date, - train_end_date, - validate_start_date, - validate_end_date, - test_start_date, - test_end_date, - rolling_period, - calendar_freq="day", - ): + +class TaskGen(metaclass=abc.ABCMeta): + @abc.abstractmethod + def __call__(self, *args, **kwargs) -> typing.List[dict]: """ - Calculating the Rolling split periods, the period rolling on market calendar. - :param train_start_date: - :param train_end_date: - :param validate_start_date: - :param validate_end_date: - :param test_start_date: - :param test_end_date: - :param rolling_period: The market period of rolling - :param calendar_freq: The frequence of the market calendar - :yield: Rolling split periods + generate + + Parameters + ---------- + args, kwargs: + The info for generating tasks + Example 1): + input: a specific task template + output: rolling version of the tasks + Example 2): + input: a specific task template + output: a set of tasks with different losses + + Returns + ------- + typing.List[dict]: + A list of tasks """ - - def get_start_index(calendar, start_date): - start_index = bisect.bisect_left(calendar, start_date) - return start_index - - def get_end_index(calendar, end_date): - end_index = bisect.bisect_right(calendar, end_date) - return end_index - 1 - - calendar = self.raw_df.index.get_level_values("datetime").unique() - - train_start_index = get_start_index(calendar, pd.Timestamp(train_start_date)) - train_end_index = get_end_index(calendar, pd.Timestamp(train_end_date)) - valid_start_index = get_start_index(calendar, pd.Timestamp(validate_start_date)) - valid_end_index = get_end_index(calendar, pd.Timestamp(validate_end_date)) - test_start_index = get_start_index(calendar, pd.Timestamp(test_start_date)) - test_end_index = test_start_index + rolling_period - 1 - - need_stop_split = False - - bound_test_end_index = get_end_index(calendar, pd.Timestamp(test_end_date)) - - while not need_stop_split: - - if test_end_index > bound_test_end_index: - test_end_index = bound_test_end_index - need_stop_split = True - - yield ( - calendar[train_start_index], - calendar[train_end_index], - calendar[valid_start_index], - calendar[valid_end_index], - calendar[test_start_index], - calendar[test_end_index], - ) - - train_start_index += rolling_period - train_end_index += rolling_period - valid_start_index += rolling_period - valid_end_index += rolling_period - test_start_index += rolling_period - test_end_index += rolling_period - - def get_rolling_data( - self, - train_start_date, - train_end_date, - validate_start_date, - validate_end_date, - test_start_date, - test_end_date, - rolling_period, - calendar_freq="day", - ): - # Set generator. - for period in self.split_rolling_periods( - train_start_date, - train_end_date, - validate_start_date, - validate_end_date, - test_start_date, - test_end_date, - rolling_period, - calendar_freq, - ): - ( - x_train, - y_train, - x_validate, - y_validate, - x_test, - y_test, - ) = self.get_split_data(*period) - yield x_train, y_train, x_validate, y_validate, x_test, y_test - - def get_split_data( - self, - train_start_date, - train_end_date, - validate_start_date, - validate_end_date, - test_start_date, - test_end_date, - ): - """ - all return types are DataFrame - """ - ## TODO: loc can be slow, expecially when we put it at the second level index. - if self.raw_df.index.names[0] == "instrument": - df_train = self.raw_df.loc(axis=0)[:, train_start_date:train_end_date] - df_validate = self.raw_df.loc(axis=0)[:, validate_start_date:validate_end_date] - df_test = self.raw_df.loc(axis=0)[:, test_start_date:test_end_date] - else: - df_train = self.raw_df.loc[train_start_date:train_end_date] - df_validate = self.raw_df.loc[validate_start_date:validate_end_date] - df_test = self.raw_df.loc[test_start_date:test_end_date] - - TimeInspector.set_time_mark() - df_train, df_validate, df_test = self.process_data(df_train, df_validate, df_test) - TimeInspector.log_cost_time("Finished setup processed data.") - - x_train = df_train[self.feature_names] - y_train = df_train[self.label_names] - - x_validate = df_validate[self.feature_names] - y_validate = df_validate[self.label_names] - - x_test = df_test[self.feature_names] - y_test = df_test[self.label_names] - - return x_train, y_train, x_validate, y_validate, x_test, y_test - -''' + pass From b3657d1c8f9ef9b47382db6039ab74e944efbdd5 Mon Sep 17 00:00:00 2001 From: Dong Zhou Date: Sun, 29 Nov 2020 17:22:37 +0800 Subject: [PATCH 240/241] add linear model --- examples/benchmarks/Linear/requirements.txt | 3 + .../Linear/workflow_config_linear.yaml | 71 +++++++++++++++ qlib/contrib/model/linear.py | 91 +++++++++++++++++++ 3 files changed, 165 insertions(+) create mode 100644 examples/benchmarks/Linear/requirements.txt create mode 100644 examples/benchmarks/Linear/workflow_config_linear.yaml create mode 100644 qlib/contrib/model/linear.py diff --git a/examples/benchmarks/Linear/requirements.txt b/examples/benchmarks/Linear/requirements.txt new file mode 100644 index 000000000..6a53211f9 --- /dev/null +++ b/examples/benchmarks/Linear/requirements.txt @@ -0,0 +1,3 @@ +numpy>=1.17.4 +pandas>=1.0.1 +scikit-learn>=0.23.1 diff --git a/examples/benchmarks/Linear/workflow_config_linear.yaml b/examples/benchmarks/Linear/workflow_config_linear.yaml new file mode 100644 index 000000000..70d3eaf68 --- /dev/null +++ b/examples/benchmarks/Linear/workflow_config_linear.yaml @@ -0,0 +1,71 @@ +provider_uri: "~/.qlib/qlib_data/cn_data" +region: cn +market: &market csi300 +benchmark: &benchmark SH000300 +data_handler_config: &data_handler_config + start_time: 2008-01-01 + end_time: 2020-08-01 + fit_start_time: 2008-01-01 + fit_end_time: 2014-12-31 + instruments: *market + infer_processors: + - class: RobustZScoreNorm + kwargs: + fields_group: feature + clip_outlier: true + - class: Fillna + kwargs: + fields_group: feature + learn_processors: + - class: DropnaLabel + - class: CSRankNorm + kwargs: + fields_group: label + label: ["Ref($close, -2) / Ref($close, -1) - 1"] +port_analysis_config: &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 +task: + model: + class: LinearModel + module_path: qlib.contrib.model.linear + kwargs: + estimator: ols + dataset: + class: DatasetH + module_path: qlib.data.dataset + kwargs: + handler: + class: Alpha158 + module_path: qlib.contrib.data.handler + kwargs: *data_handler_config + segments: + train: [2008-01-01, 2014-12-31] + valid: [2015-01-01, 2016-12-31] + test: [2017-01-01, 2020-08-01] + record: + - class: SignalRecord + module_path: qlib.workflow.record_temp + kwargs: {} + - class: SigAnaRecord + module_path: qlib.workflow.record_temp + kwargs: + ana_long_short: True + ann_scaler: 252 + - class: PortAnaRecord + module_path: qlib.workflow.record_temp + kwargs: + config: *port_analysis_config diff --git a/qlib/contrib/model/linear.py b/qlib/contrib/model/linear.py new file mode 100644 index 000000000..0f9223737 --- /dev/null +++ b/qlib/contrib/model/linear.py @@ -0,0 +1,91 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +import numpy as np +import pandas as pd + +from scipy.optimize import nnls +from sklearn.linear_model import LinearRegression, Ridge, Lasso + +from ...model.base import Model +from ...data.dataset import DatasetH +from ...data.dataset.handler import DataHandlerLP + + +class LinearModel(Model): + """Linear Model + + Solve one of the following regression problems: + - `ols`: min_w |y - Xw|^2_2 + - `nnls`: min_w |y - Xw|^2_2, s.t. w >= 0 + - `ridge`: min_w |y - Xw|^2_2 + \alpha*|w|^2_2 + - `lasso`: min_w |y - Xw|^2_2 + \alpha*|w|_1 + where `w` is the regression coefficient. + """ + + OLS = "ols" + NNLS = "nnls" + RIDGE = "ridge" + LASSO = "lasso" + + def __init__(self, estimator="ols", alpha=0.0, fit_intercept=False): + """ + Parameters + ---------- + estimator : str + which estimator to use for linear regression + alpha : float + l1 or l2 regularization parameter + fit_intercept : bool + whether fit intercept + """ + assert estimator in [self.OLS, self.NNLS, self.RIDGE, self.LASSO], f"unsupported estimator `{estimator}`" + self.estimator = estimator + + assert alpha == 0 or estimator in [self.RIDGE, self.LASSO], f"alpha is only supported in `ridge`&`lasso`" + self.alpha = alpha + + self.fit_intercept = fit_intercept + + self.coef_ = None + + def fit(self, dataset: DatasetH): + df_train = dataset.prepare("train", col_set=["feature", "label"], data_key=DataHandlerLP.DK_L) + X, y = df_train["feature"].values, np.squeeze(df_train["label"].values) + + if self.estimator in [self.OLS, self.RIDGE, self.LASSO]: + self._fit(X, y) + elif self.estimator == self.NNLS: + self._fit_nnls(X, y) + else: + raise ValueError(f"unknown estimator `{self.estimator}`") + + return self + + def _fit(self, X, y): + if self.estimator == self.OLS: + model = LinearRegression(fit_intercept=self.fit_intercept, copy_X=False) + else: + model = {self.RIDGE: Ridge, self.LASSO: Lasso}[self.estimator]( + alpha=self.alpha, fit_intercept=self.fit_intercept, copy_X=False + ) + model.fit(X, y) + self.coef_ = model.coef_ + self.intercept_ = model.intercept_ + + def _fit_nnls(self, X, y): + if self.fit_intercept: + X = np.c_[X, np.ones(len(X))] # NOTE: mem copy + coef = nnls(X, y)[0] + if self.fit_intercept: + self.coef_ = coef[:-1] + self.intercept_ = coef[-1] + else: + self.coef_ = coef + self.intercept_ = 0.0 + + def predict(self, dataset): + if self.coef_ is None: + raise ValueError("model is not fitted yet!") + x_test = dataset.prepare("test", col_set="feature", data_key=DataHandlerLP.DK_I) + return pd.Series(x_test.values @ self.coef_ + self.intercept_, index=x_test.index) From a939445da39ae3e289aaeaba17cff6ee7d93fa2e Mon Sep 17 00:00:00 2001 From: Young Date: Sun, 29 Nov 2020 13:00:35 +0000 Subject: [PATCH 241/241] fix mlflow bug --- qlib/workflow/exp.py | 13 ++++++++----- qlib/workflow/expm.py | 3 ++- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/qlib/workflow/exp.py b/qlib/workflow/exp.py index c23f27f09..09c680e59 100644 --- a/qlib/workflow/exp.py +++ b/qlib/workflow/exp.py @@ -2,6 +2,7 @@ # Licensed under the MIT License. import mlflow +from mlflow.entities import ViewType from mlflow.exceptions import MlflowException from pathlib import Path from .recorder import Recorder, MLflowRecorder @@ -226,7 +227,7 @@ class MLflowExperiment(Experiment): if recorder_name is None: recorder_name = self._default_rec_name logger.info(f"No valid recorder found. Create a new recorder with name {recorder_name}.") - return self.create(recorder_name), True + return self.create_recorder(recorder_name), True def _get_recorder(self, recorder_id=None, recorder_name=None): """ @@ -241,7 +242,7 @@ class MLflowExperiment(Experiment): run = self.client.get_run(recorder_id) recorder = MLflowRecorder(self.id, self._uri, mlflow_run=run) return recorder - except MlflowException as e: + except MlflowException: raise ValueError("No valid recorder has been found, please make sure the input recorder id is correct.") elif recorder_name is not None: logger.warning( @@ -269,15 +270,17 @@ class MLflowExperiment(Experiment): if recorder_id is not None: self.client.delete_run(recorder_id) else: - recorder = self._get_recorder_by_name(recorder_name) + recorder = self._get_recorder(recorder_name=recorder_name) self.client.delete_run(recorder.id) except MlflowException as e: raise Exception( f"Error: {e}. Something went wrong when deleting recorder. Please check if the name/id of the recorder is correct." ) - def list_recorders(self): - runs = self.client.search_runs(self.id, run_view_type=1)[::-1] + UNLIMITED = 50000 # FIXME: Mlflow can only list 50000 records at most!!!!!!! + + def list_recorders(self, max_results=UNLIMITED): + runs = self.client.search_runs(self.id, run_view_type=ViewType.ACTIVE_ONLY, max_results=max_results)[::-1] recorders = dict() for i in range(len(runs)): recorder = MLflowRecorder(self.id, self._uri, mlflow_run=runs[i]) diff --git a/qlib/workflow/expm.py b/qlib/workflow/expm.py index 80d471845..cfb0290fc 100644 --- a/qlib/workflow/expm.py +++ b/qlib/workflow/expm.py @@ -3,6 +3,7 @@ import mlflow from mlflow.exceptions import MlflowException +from mlflow.entities import ViewType import os from pathlib import Path from contextlib import contextmanager @@ -324,7 +325,7 @@ class MLflowExpManager(ExpManager): def list_experiments(self): # retrieve all the existing experiments - exps = self.client.list_experiments(view_type=1) + exps = self.client.list_experiments(view_type=ViewType.ACTIVE_ONLY) experiments = dict() for exp in exps: experiment = MLflowExperiment(exp.experiment_id, exp.name, self.uri)

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

Y};R9^f@e$$l6~y$dFKbkFdl_^w3Y8Mn59nwN8G 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%0DvZ zB1gFr>Qiy53Lxc<+Kvs#Dn2&!2eURqaIiE>jt=<K~51CC4%k>Ef2x_^@$nWy%H zaIALbv29^rH%fBA&Pz8i01l58Z{;1R0>upmH=c@3QK43G*qiXN$iT)tRGLL`FbeRjNQv7K?qbT}G>9oEY((;JVir!*n zZ(<>|!;kN8HG+EvIDGlW2RWlb0W1S8P^Z5D5rcVSxuz2y=0EbOYi-6`>yOzyaj}>5rppDS;nzHL=Agl?!FV~dO${8=x z&v!XBXz&oyx|SX~eVq}(ia@q@z+3NWSI?XMfTcI)UxeM^z7-c2_b&wwh`> z6@#VK!ctQi|4mb)Db#>S@9e9|&wuqfJp5Xzg4KRu)gIaSIT|-RKj>iTsOR_pCCM+R z%>i*uqeHJHZ`cp&$Xhyb_=?@Q{clQ0tO(%02{8UmlZIX1f8Fs~Gj}aLleu>-f5J*n z{vmiuO8rZQB_OlBy!=4+1IO!|f9a_HyFBYZRaV#EhJ`J#61hLptAAdC%@?j0`Olxs zc>fZd>}HlmVafkPBJ9Vndk+2UgFLm>aESp$_-F zr06*3Ql>QYcFEAzb*S&AcoutnrD2$Q-@t2A@k0ldG&N)%BU`$Xd&Mbrv46YNocq%I zTQ`OLYncsEN@6BnV&crJsiwB30aG90q^G3_dTLDF1?B*S?l{y=J9XV9TG|%k3?9u_ zxozmGktp~b+%%xac8I@&@~dIIF0rSL>P4Jo3TSX5DTbVipwjea!T7S_mg zeig=>cFzEcA^m`aO7nfqe4y_#ifDgTEZ6(Me)J_uKu+@fA}rRX4hiSFOcBvu{kRR? zZ+Wn^UCipb#Hb-ITY3hMifwW4vF<=Qy3wN|_t%%(le~^;y2ZTpDtP2lJI1Y*Kaa{xFy)51?M1^MY)4)E z9M5sm)F~?2+V4vHW-N(}EvRz$*XKA@(EW;_b8qaIZoz6^KL7rq(e+j8h~Yz5igd+v z8)sVTqsFqmXJe!OPe&Q^^>vO{+4GgXPKF+!Fr9NW_(PDVzmu$Ytzi*q_vgIM1*4s| zBRMZ5vL5SupCWcWDwo<~J81J6*5uPDd)bhEpmuqtv_}T+$IkhZDVEi;6|+KPPP*kB zuh(+WOx|9hvv;ulf$~$3c!rZ0h=ULtC>krR1$2<@qU&TCLDn}ne2`2P{@~Vz8ZxHN(De1 z9zsnIk?Cn6)P@qnjMcF4%=(pc0;FjLbi!lWb^!9z@aoJ*=gH+4Y=(tfiH(I?Ipqf( zrH#krl%eE%yDjnJIsq$VRpRY%7iv*&>d;FrpLpci_$M3e8MrRvMB7(Pb;>dq=TAc# ziUpt*GQAM!jAC-=B_WxYLoaNzjH+fV}|pU?A1_-l6dH!-ID2IZRl-xC(P(|co5 z=Uys0Kh-X!x=v=5`b8ejGhC#a<>faiK4^O~0H=~t)|HAV8y@Moz(WlDE~wviQofkN z9}r$HVzrv>fyBojrW}V~-FntBe^4Pi+wiG1$wPa=kTc)<@^;RQy1Qm6h6k^Mm92j2$a=uaY|ZA6&tuUxMQM`>B9@l} zWMKtxXN3Z(we2|{IK-<@X$om=-6Vg0dU$Av2VZYpD!-VnN}nAWxnN&jogF%LfJxP- zZ;Ht``uVB6B*g-$Kp1&VkQkkx$NGQ*!f2ALZ*^6=`WJ7CYNr3Q384)NF5s#E$%3@$ z*8N!@3OX^qezd5#A+Hh6=eKVi+u>6ra!R}N^sv#n4{0}oRDQQU{VmpiXTeIml1w36 z6BY!|h(SIV{OTgmK|^t)Vz7PF(LTeuU<1Qe2K%fGkG61AcUkf3D>oS5kvD9 z6NGRTAzj+xwUl!LQSW)f}TBF)f+0i)VvO9!(=%D&rSz4$G-vzt3t`>7)xQN<5Mm5 z$cGjqA8ShXAE^aX72%T2Oa<7JHhP=Ns=qr(j8N{wU=~ZPg^HW&eq~^f;BoQSo;)Zj z`1WXvmL! z26nU~f6m~18nTnARNQK2|+vhw;qtUb3CV_uHdG zf+%hWxu9v?i|8-JHA6=SGQmPHbrp0CUhM7jjuZC%-C9UGeQzf~-6NF}z zG35k&lTF$6eD*0wgrhR_0ipSHRUxG~WUr#h1Z~-nV_(y|)$DDHh!L({369=( zzRD&ALt#&K4#;T$L;)XrCwylYrnVKf$LBe$#xUIA$&+I6y0uaL;MQQc-Ttu%9wXar zs$2`L3-c@19N1UqyQ_ zvB;#@+Q7#QxVu#V-5&PgPa;~93&v0J_lQ{24vEg=b>0mUJmy3=>=6?NXVEhn`#{iAsUychl;OleMs4V4gzh9LrEGL%}ux!BJdC)Rq?k1 zJ#y3d5=;_qe=b&Kt*mT%0W+Wbek5{tYduSei)C81;HtV}h8|gMeZ{x{SJ8GyIC>fC zoT?NyNiLs0k{ALUudZHM%=DUD)!kF7E6#7?-*jws>zNi-xnpLwu8o{x(lN=M5B@qf z{p{r?g}+ilr#GbDdV0)#skue!7x0-HR!Scdw+kk_UsP}gg~_DT4~~x-GWLl_Zxerb zggQDoSq$d#ka0ZsmCZYM)o#Mwm|+#|qP=nX_|}bnKq`P3vQAj7utP)^r1A@H9@ILA z33`|B)Xuo_t1Rub)vnRg>=z&Nyy<+~sUHUeOZl0_1X|DFl*=@aeE_)DC)}QPoG!2SC&n7oo*e9oG?H%>;@=-)QKGdbX7lpo!@enWU$iooNlXnPDHvUh=^yi-1MPYI;z`B7)`0#C+A z*v>Dl+GSJ9NOY4vAbO6u(Zb*1wzl|NV5!llR)sGNc znnT+k-Uy};ji_#sh{L&unw6KJ`ns{bS7hZGDW!?G__i8iAJjmc!PRvu|KfW!fmHlw zt`hp4kI;I;3WpR}D#zOp8kz^rm6#_gL^Eiw zZ>N5IKn|8!qO#u!x3R6`9E4^nKd&b&s+MxH@ZF_cubWF5KtD~yCrTZCLajPJ%FC=O zVBdPt&n=<@JuluB#ZCC$UPm*>wI@6{h5DXTaF&wKGf>K@0rI@7Z+b(}BiF=u1|jfJ zxb(|PAR9+W?1Rkk^m~L-oYvYo4Gw%v77!KMJD(-U{KeBEmTb#$rJHAHys0(9 zV7oD1zs|a2DD%7$A#C`e8s$cFJC&ug^ZB!99as1sb_s))Ht@L`V?FcABUOK#RbLHM ze`uZA<6QGuL)P=XZQhisg~40+TNnN-yNK%Tz4Zv9nhQ4cn{6;%$+wM0wY4hFuw$QH zyf>{YHeuMkq;q@<92!Fz4&5YUStZ!&?0kl`{^S066VTkOD<;L~3 zJU`ycSrbxlKIpoIhZcK~o}I847$Ao;?5x(qf0|Hi*YjXMB%Simj)WLrTPf0B{7hui z51T$8n*xrD^o58BeMJxN4}*8dN==qg3cm;Ut@y>JR(k3@Aq_}_Z03}3XMJw&^IWLJ zR}%*#9HUo#^%K3xz(8Ua&sq%3IR*c26men}RNEUiGOo3DKmO}iT`$`yo7uWN9_#(Y zpQTE6+P0Q85BY=hbP0|9-VIR2-I;jK!PQ7NDT}<(nnk#ko-Q%}lDfb(=oYEhO*fu6 zoOkZdanSXFyzkq`zWC}M#i7|dO(MdVx+IDTqFdWns@rSD-dJx><$9WEWZySNy^AOs zCpDfHr|#q+#(DnFxd5LoB3xs+wsfoNjYiMak9BcAA7fer!LzlCl;S5$xOVrZ>`}E| z(v&Vfu5~V#gZz|V>~DY}I}-^T)o2d7Dz@7R)guWUA;e(=3dA|-m~)Ykz0_4y2(AJ3 zsMDQY`^{H?sa?Lv!FVQhv5J&ON&B^aX4{&P+}S(x{@!Op1;{pZNs087$&NFwvUc#=KT27UtSciG#+hqC{hF8t<-wCfl2CUj% zwR9%i>hAtK!#Pu+OH@7Oc*nBjdAk4e*^UJRLY%GAcYa#&63oXH&`7h@bvB8!9^{MU z#YNYke$iJm^W2y3RT6Dy&ti~R@r`dNmHF~#&o0RpLLB@q_8DV`1#q=JgCC)6#&&bt zy*QlozyV25-Poe!1}O4u@7;7`gSIXq-$s66-9#xxQ|;<=gq;6+j<^u5=WJyJV)>iB z3wu+;@?{_+(L{bx?}t0k=G|M-J)hGiWqfGqik(OU#l@I{+a>P@7Z(LS=J)vJtQqrgd4LYs!*oI~W-1z>MA8fwk?HXuZ}pzv~2jmyR+Cf&9I7pf`X?ahCG>X zw@2o+g=T?rcz-?5>uRoOyd<8+XJTYzXJ6Mw?1IiGYp024gmkA_{4O1Al6Y?Fc1!K8 z8h*k#(?=<|LYL#Jy1G~7P8&tX?l^qq78I^RlpO_iE}thQT`k`iCpC|KawIt~Wwrk% z^Hn2#2`9-&UX?vl1V;K+<=hJDsy5vl8C-~M7fBRRyL6-%dcXhlG5+fjTVQK0#ev5q zqWeS25QrKueZxUh&#{VCTT5qBMH2mqRRYwJ0%lfo18-h_LW>*6Zp^p&5LIy#51nA@ zhXv6ZI>%ib-uphm>FJ}Wv5fRZ-e+3l;$w9aK1)nUvuhIZm8%*x3E`@oG2vn7DEb!mA+K9t!{n; zvNLO1lIu;hRkJZGaj$R)4&d0%hwYcz5qK9WXZ;RwLE$Nb_;HGhMo&#fE zTX`Gqv+5GuNZUI%8qH;0w^KeftQfW*Cio}e2+yv56w{6h%om2$R33lml5}<9cCAXo zEpWR~{{6fDhixqO{@D0aTsaQ-rm2a!mu|gub_|)=rCgYq`w%H~|1xAe^ZRDfXKeLz z%!HjB73^^rP~@^&3-!!37lP!XP6TQ@+R4Zh6!Qws3O{rdad>c49y9`XFk0K)EwaE) z*BdG%*hVl9mQ%_ofncuc9Xf?qK2|3e-zMZJs~GVkB^@)Mf%N_`q2eZXTD{*YM4475 z{vbc1N?og}dQ15(kzIB2*HYZu>$L-Os0gKfyXNnbmys`hg#4T8B!NX3iPc`N%g7b* zb)UmyKM&-bE5^6sGPHoeiry)6n{6n1g2zA#7VfKqpAHfdXk9;p8{B*Q05`A$T*h1$ zDqU|Ie$c2peQ9waRbb8}TGCNIPP;C~Bq0jpF3#JUKPA7vqEilk94jxnIY5RIzvMK% zacLI1r>}XL-z<~J!c|80a+I>52Dk#7_~}zYemnk>@3h6pdBH!p6qK6=SY;6JEfA*q zX-i);2tRaC8Ni|%9XF({+{q|>3Lyc-XDJ7@;>I9t#69oNKk3a>mU4|v*}hlO?9HYF zbXgB8brsg|usF^_vvUA20}v!Q$8!T_8sD@??=~9SJoH@X13urT=3A6?LziL)FBC1b zy+=1K{xA-L(}*SXa8Hrz0|(5`Qri;fV!izs+mCK_WIt2UUalglHdUJ0o~P9` zNFPQ+XoaARA~KE<1)lepH{==Tk9^uCuc7cXt^`+mK<%7wUAb@1DQlcHOA|y6;n3mM zw6MB<{+)3IwDLS0erVr>q-BzR;Q_nu;nM+=X=tS(-D%Lj8{TNyELfpHviIYM zG{2km}WT zjZr9;GKoE=tOTD!p5!iTroB^v>Yl%jJg#P@ujJ^uwL!OWzrPM@_I6%1OYJ;ZaXdfT(hbz`eoIzErd&^C$+Mn?LtED zLNk^B9(#l6$f!}h1*`40-iLi7pWm3;Nw@kt%KE_-Kl1MHUrIP&@c^lBB@`6_p?2=d zr4VvAo4Jcd`4aX6pEw*Mat~seALUG_rYd-iTlibt?Wz&f|3{BxJ3=^Gl zRBycqjxx9p9D1TT@AK`goJ0SXbWvDNmW+SfyN?2+V*WV6o1QrLmE-56;hGXn7GwiV zmFRxz=ji^Wgwn-DdyYyw6OG!UJr6a*E0yU0uyWRSkJ-IFt=;a650J}m6Y~xzuNNP> zx)zlBBc0|jsnzsvIvee$HRqXqvDnlF(8GNo1oC_cRX`i0Qj)o4j!PdJ)|5Tjbb+=M zpIagdu6Ehx>a-{u^&prkq~fNS#946m5qC7LwzRv9^x3Z-AdG{iOAmQJ*YZ(x0j1vF z&r#)ER0PjXofVLc^!KaW%t_QrX7aoEyxRaq9u0!F0|ckk$>I#hj7K{%GaA3h&L1ntf<1-rf#tNta&Ac$%v zh?AJ>6U>iTmUvxSo4rmJX>A2TZ~Gl0XUB6J;^AknnUcq*1N5dS7e?DODv5}n$U1;8F5&CdH138_=}NDMPFH`qkeLT zhGNaaY=#vg^;k27tAca;mQu{d9<=S#_^vRLLHlZKfK$Iz$>8yEFTblfZ106NCI(Cv zg|9j;!o#L2P|Oi}obpno8zmQT4~4ojEyFvDQGy99f&i`)dyot1Vr84QSBlhk`R2aH z5L(e>w$Xu)%vE&79bn^Kl8t(az8l9LpP$XQ=++ss3Sm2Zu){F2rHjR-^(rYXd!Wuy z^E5qGedf~_-;0QH_+3`C$sqEoDxKyNd}43y$qsd6dz(z;OUH!qOl1RiR0k)0AngwB zsRu6_C+bf@bQ$at@4!PdUax-Y0=Pa*5K0cM+Z0>Xm>;eRjZj&U>Q#X56W9kbGEszY zPVp!Xtpo+7#1b+vkW>Z?Ou_pq#)Y3fj3ks2pDYcvwa0(KcT-sqW@lF`!n$Bo=Ph3x z;`D>__1vL$quVK_t+%c%f?75G+j|l|Mw$mDM+-U0UQEm~jR}2_-DguySpo-pHa2?9 zy}dl;e(-`N;-SIcQ5q45uz`W<8zVohvqM9jjzJ}hAnat85R)*S$V+Ph!*ABka+tmqD$2% z^hcTBpS&aqobsfGo=JLT>wCjq-07#$^2SRFbvRGPy74H;qPbV5{S8j#dy4fU{mN9Q zau<1_C*l`wlT>%o*vT1aD_F@08TRSLT?M&?yHI;iGywySq&82JTH*{>CKJba_;g~5 z!+&GyQAZPuem&80;}Q5hM@046&+|H9CCoj3r6!)TlaY@J$g^_bv8M&bTsj0QiN^U7 z{4{ZjU-m*~1UMF+rVq=}0N`&ZMpIJkK{*y53}~2n=N-1>M7=^=k{{5!iwOM`=+wT! zzBsq5#EExC&k-A|rV4w`cB#7fpmUp1N@D3`E>oW3%;T(kncd8@(=!Pd2PJ$DL+7?sRwRZ7DFvS_Mw&qQ7ggs}j2;OxfJwCfV z4b?-74A1q1;2e`lag9WejG=J-JnpPNyX~R>GkhrqIh74up{rMN-FZ~LtqnnoG#-Fr zh(6~w2F-NYuI9(S!GAn44t-f2B8NSR>M68sHDY549>^-H@yy?5VCXAQ!qa)z9g0oJ zmj&`)kY9Iu5RD{$?9**$@jIW{e7(rk6A+uD_lgM zsb7oA%~wy?Gn(}^FUz!WwFKWghu{?UG*fZJ@A9YnqQ1eKm_R@vmYUo^Yty1GRaR0< zG57ANRyWDPQajx9OE9ozx+7Sjtsf$EI~NsNP>^=xgb=o~;WdbsI9JVR{@9EBz~bA( z%C(p7@R#@ls|b z>c{e==4p0sA>Reu=No2tf^w%F$${tnVE^zv_r*AqTSK{o3q)lvuaiF^KkVdi+#{Ri zaDLRdiq<=={OtntGr+sHREZyB-q;l5Quw+-l5p1ieG}#gKDSGAgMdPVW*9}Oot1@W zZtc<8i}P+B?YgU{yGJh+gq`&+D`WeK_IDCJ1M~x-?v&+a+r^=1y51R;+og)f2DFPay2VRcaM?^X%mwr;Oy{yL5p$d!Tj#s~1`4)HL zn51aWWkUYiLm-kVGTk;nKBtSNev29f^+;TB$m|;es`@2=eu__@NQK$}d1QVto&bTp znd8j%@ARYEmX={6rO>II#j+~M!qC4NYe=QNdt{V*VFV zl=u+OAyBbrO7E&t6b~m|oo3So@_vB4{V!y#(ed+bnNKO8G#~jUD)o&FEVc4-Y<&dm z?ZHL0^BZw=QXr+9Yc#1jRaFBE%>j;;xe?#?z@`k*1k5qLyw)V%(lwOcBfkTsBJE3REkwyJ1J0tgKkUHwStrv-r^M($-+RPaZ&);h9+&nva9<%JpM`+oj(7@umI zB}@C{o+9Mt+^OJXiMVGCFD_=#B{OWUKJNbh`)B=?m)*~rn*ahRPsxRe6~g8xFBd+J zZM!QYDBM1Gc2%-VU=vu4P|407CZ25I1iB9gHDW20*-Byn)p~{> zE*~1P5eIUb8Si_evWDWdmfm9Z-!U)>O5czgBWr)LM3J2))}-1pIQ%{I1dfk?R$YNb zs`l`-D1TT~p#}*wv2?-ol?N`C4&2SjHUOF!+vm@>oYBL*^H^ zc-SN&oFePplLe)W!<6Lf#Vurn%6_2F`I(XN-how}+bX5hi`qrW2XbC0@t&bQli;rW zCv8EJ@kF^b^w`yLs*Ksk?spEfGAwR%dNZ|G%>O zvJQIL;H7lR_FU<)Dv^+J#~6EFN*-El_FmZ0CgZBVb#rv&W>xi7qSw!g=88N#+rzq# z-N)*i=<<4*K`L2+NE#$#Y6WQj@(917x!zAfK_3V#k*R;~C^rbI?UG9LB$Qv8J2iZf zR$W;zjB^@-h3eK$<&fhYDls_G@AFmPZD#C-DnXTVCp zo5K&~!1mYr`lB&5y$o>_7pSC8%%3*HKcqG3w;a|-)Lv4i@YshIMYOf${3LtO>=Yp2 z=)_lXQiY~?|M33$d6s>Yfo5~G2LaaSv-?Pm%30g&CQCx_;JuXxm6?kQ&lcbO-v8nT zEXg2a+{!t$DqN2^f7ke*2^DlMAw+p~%CO;8zPA68*;;d(g`6>`A3sV)jM=lc+ zj!3G$;`{x{9S3|W22XQ_2gWdU*N4m&g%VGZpinlY;;tWf%TwRIadkzG9jGAu@|#(T z;e6JrIhuH*sxqU3cYzcTSL6X`2~17Cw#+M>4GFn*9B+M_$HtWnh;HHM$(e!%WWH1# zoT~;+EUMl{<}b(eZ^4TvXB#8rWqL4A?;8D}36m47^IQs@@B5+h0|FSJiH9^fLsx~w zl?rON?yo{_$j5A*-u<}cwAuA3L4-Ai&GxVfa zf>dF=2(eoHZ$V#Bt5yU47@1elcMmq;L1xC@pJ7+cmbHLyPikD=Fz`uhFiO<37VV&x z7ElpeI~wi<@|WS?zIQRS$_n1?mzbM7tbEIdMzxuq_56?;+O(K3P;Pb^B_|wrC@*cn zbBp(2-ZqKsI;64{$x%W;Gn%DKi>|4rvKRtx;P!*)3O7^-)95BUKvhE17`i0>7$w<;eLeE7I?|CD54cqPf@ z`c>N@Y9C)nd~Xw$Q9z|ds|~_YpXMl3so__S@!mGzCieblMUG5WGiC|Gz`n?*UUv&` zy!*v~TCmLEkCIbfH$L+7Nh~lv;UmhabBaFK1!lY2OOE=zFP zsV~Ojt*U#=pi`m&%d`FjG3`Xg;?a!*fe zjHxT1K()5zAPtf}3tW7RKC{l3q%AH2{GOG>`V9}k>fqx6#F#cra?+)^vpi``%pUEh zPdrNY+i@!cpi@G1{fVF2%>N>O@!bgN>H2i5u7SlC`6Y)=)p+O%Pf-*u!!!A0Z>}^t zIe7fN^bfrSfD@hl4=Krc>HsqLMjcnZ4n9MBjo@&Pu0i}hn?$XB>$UA;k&8?X7~Z?3 z9Yr4D-VV_0xn=8Qa^*2II#J`p1hdtP@igzRX^@}$iSX6M;y_z0OCwLcc(?>$@9lyL zbZa|c_>S>}$#`4)PBJU_LC|Bl7uY6nwhuAH(x-npf*v>>3A^2{;%jl`-B@v zWQLs7t%p;V83r&1k4BTpO2|`PBehQ84Ln-76rTKp%36ATxV~ysTop~1eUIxz-v?2vO%FOA0w)kKK{k|`@F4G7IUljFp zAkt&$@NsfBLL!DC(oa@^+Kw`R!=)wnKaODE zJDbkVkO`0o?Sfq~l1X1jVul826`PGj)~GCaiZR^0Jwy=sarg6Qo`&Q9)iUEh)n<1m zg&qTk&&*^#Wx=5!rd*-h`8z0!9xs95c0BAd1)Wb%pVGJTjgg8eO;(kk74nNaEj9|b zb&C!>Ah5Temagat4_ETUhCv^=@6OJCyD6e;JJpFr!AZ&S@X!^GFWvaEDaZPIRUi6k zXeh|Vqp!NAQ<`^1>5ZC}c72FMHO)cMzhyf|MD~#Lja)GwCxw;0@A}?ezziIOuQY#B z=e%GF+fdv|gn5O(S$wsgR;@C|J9rD3NIH4zmdL>y+ak9abV^Fe-N}i^ z{Orj;WfMmi=1yyc_N{lHZ&|V}27cWZwY?X`Ie{8KBen!>jtzzS6;ti8ar}F?5O>`| z1sYJ(<}fovup-5*>^C-++ORZxvOCVKOL)Z@l9CO4wmQ>i6KScPhphrd;q%|#R%V`= zWIL=0EaE>CMPS^`$agBFF*7!ev1uRYy)IET`>#%_+wsVz&j+97YlKC|D*wD{eeck= zaN?uBm1K0qO?kiOqNE^y`;m^(wU*K2@58<02cPt=$mWCIRCM$WOd77~q)~OC@CJ)5 z)L}v@^iO#>9xBPKez)?_YG@W>WHePW&gjBlp1IbD;;-PFVtvcYHFJE$Ru)+b7KAfE z4F)e3@OtV!d_MpCvSFQ7k2Ni4^Sk6x+x5=Lte${&1CSQw=&0CS3AH+nb`20vPO3S* z*q$bz9p+Ql#KkB(=l=PF&N%3$-f)m`Zji9=^X!SD*0OEW3X7}Wt02qks7k*${i&^0 z`^VocC9CmsWDI#-Q_QOD_26$BN4$Y05g z;I#!Y$-7QX^b2$z16?$ARLke{l>wW7;DDwH zo_fIPN7F>Rw19XpOzMh#qW;cJtnlsdt1Fs$G727pP&Pb+pR~+jxgCr)RI`@gbO!H& zQJqa~6$rK4C4pq(n?&Q{Sg1*3Fkh5=POCy~AC;Od zEReKQ3E!?mumHhNAXU`4>jYc=J>T>-MvF|a%=XTZ;btM~CJH;#5(vNv0q4itA14-w zk?ECCg;Nk4a;N42gP5#6*yK+cq7X@`qdV{SQ5s4gh7_M0;_0vj`D}tA;>igj#$(jZ zPcq%923BIVP__B2C7*?PT5nb|qwDd8r$n5|3KMJaQ+$Bkren0p(}=;S$CVGIe*?Sk z+F+2AIzZ6YdmE3H&`-4Obl`M*LA?Lw1t>>i-trAS87+5*?%d_n%etX;KWd+A4#W4p zljEHkC#~zDEmZ0cRMdJV^idp7+(MC-cqtw1yAiC2&kN(-F>`zjwvChjI&2mXXuBzQ z?mFkZ^6@+`ts5|YbvhQO^i*~R(~8eJ*ffpeQ-7!3BeB`GdUqyOIy#;dE`WUck_8^1 zcFp-7Wir#m#E?gAY;aT}I@I!V*4lY_CC;kImscB=;ir2PUUxjDMR?%1m1B2>UtY@T z;IkwDIz38+6wd(5C(n#;NN~p!wrV@%;#&M24Iy}LP9l86P{C+oGA?bL>CtA_^w4UP z|I-jOqEo`r84d=1BL9)a-rs+$&ZUi>9k(WQ_;ws?RC&}^TKn7dGQa>!N{GC_>1^Mz z#J!UnRZt+bXT8hK4Wm)&vwQVK=okBo8@F)l=Z#NxImSMNy6|zi7+j~%u=_uDk?ID% zog$uEo9NpA(~s<(oBN#Ft3?C7_(#&IzRq?G(oFyG7K3291K;bGAyM(!w5DdOnqn6b z0etc5Y`v{*e+_va^TUVx8hd*z#=pH(*QAlUGqT9yB3jp_ny|gkpE;adHMyIVe!EF> zFzQYH{K}ePh8q2W6BC8I1YW4M zl51V_tIx2M^jtx0I4}z<3U1~*UIc+~hpJ_)k%pz#H-=18?eklkkNI?r!164OmD?14 z*tV+6+vkXM=!+$ItWaLi*8Qy}6u*6^i`4c{yDFi z{(Kh|7{l{74YQs>8}5&%LiRW1WdlE+BXG5#?F@GOyR`{B_Rw7Xqv@?X_?=gSBy*`6 zAq6tDrO{@UPbbUZ;jX47jUnHPn=XgW5hFZ7FqJ&i;(nTS_dxdi{AS5sTb=dvO+qR~ zv(mE53aWCE(~HG!+^+K|)V;Lg>UYb)P&p#1t`6{zK)HBSCS7RK0_5a44^zr+3_j&U zkkPr_6zK~7)6j)UTH9N!{lP3RCTva<=Vb1l19UBdUeQeSzZ3gmL#3U0tZSri!OLBK zwHHZXQN3Gc7`NnL@8i&a!_#)%+_Ki$0V*3H9}|J&Z zGED>)8(0+bhQ*AL}X|*sVx-DsIlI~oZEUTemrcHXxXx^_Xdwp}? zq7Uy{eqCyhG~nXoOmcJKb*qR!qn)u~_A`AtHarAWL=Y>G5)crI5Q^N9OQQGD+0chc z{Z%_h` zRP)uTo{wFPs&bWL4~ly13eYKck?Q%Se7qg}Y}pACBx5mqkKNsPBw^+{ZX6I7uUY?~ zg(m&gfTXN2I|hx0X#V__1RiRtyzk(1jB&>7vOdL z*}0i|pKxb&r4zmLQnSijmF85fNzsA}rQcBOw>PfQx-}Q`wXHaV6v&WVc_3OjUCbrY zn~x8wPQ26yYHJmOG&eUj&#QoN;LnKia*Hoes*pzn1a}?oI@}NB6gn&z(Q6&aK`Ny% z$Ag_(KL%d;JhL5I9A8cfl|7pY1;@XKziFY4=~`GHk9bZpJCvObw6^Q&%z4A1nDSZ$FgWT7+FP zo)eX_i?2B-QnhD|yoWSJOTAE7nln)WEcG>ZJU2TDP_<8S88BtYY)cEUG{RX7_2%!fuPEszfog=F3cDG>!7lSn6{*Bf-*pTMrTZzpWlL;`hgr*40bp?X zY-&>WMVJ|1LXJ;OmEVZhqDT7n$+UAP*aHgg*cClu2L{=P;2KJ@WDAqg<=xk#!=d~VZ{!2Pk5~jb%6WjX=fWmKGq6@celt%6*Ho}bY4 zk*h66BG^`7uH{yT8Pa^0F#C@mP zk;5EcIxd&Ldl%@Hqe-^;d|uasf@kMe9oo?$<(`tW!PKpcGsZ{Azep>Jma=L6t~WHh z8(}R;0kRXW03+PYma_Ic{E+`%PZVAJZ;Ppv*viAJ9kNj)`mR@&Bz#pN@<)1DJzgDJ{Kgs6;gW5U|M((($N1RhN zfrC^ovbd0r9|%2-jN*;&pHI(GuVp5APrlWGW<2s|5vucV)qFGd6@@&z;Ey%Yrb{+2 zrfHLm-|-lGivtl;9hIn}3nAxbR!hx|?T*aRz759u==t>*{~_#s#XrIpLOOC2JIqOJ zF-bW%t?!|MDW6Bm%LBt6U0Uk+^mP<#LMC&KX84#A^SmHv0c=0hM$QvBz%bVqWy)W< zpj`_8I+MRpRb3wV^=5F)ZXmq4`ItOeeA=7;e`SidTzHwj3eZqckogPW2fop%8*ph^ zI(;rcHPJ-8UQ_L(NAa^SzG1>a2cVnKC)=sXV=Oit=CnPHh%x=slmMn4(jj_#jZ5?y z+3M%zQL+=%D>G05(*q%UPiZQbMxnuRd%7y#c07U30WDaY)j?*<; zuq=a1l~$X_$JH_P@RlimPLSY2SB}E@=lRh-k4?JI{?2m%w$J5ia8cDnVcAK^*j>R~$ETz$ue#Lx zuLQJBl`^`!p{#J#I{4!ZDhkq!cyCf8cYV?DL9W1$iJrduzYiYSi^1Z+KSgI0B?Yp} z#JQvQw1J`-#w0R9Kw3Y78V`h$1)55Kd92{@aG~s%JHLoXrz*5Rwe;JXEYiOhDM?71-K0+kJkI(>hTl2gbf&-B z3)?c7-u3&H{Dt4HUvj}-IZ9(8Lf>YZ!9m@jSJ*>sK>6fk`%t}$nJI#1&5sIwd+7du zLPoq9FN8iWRpu|fyt>ZF%}?2rHS>ly53x!`0|1z>x-2fZrFq1uDm!#ff^KK$T<%b; zV`%979@T76&;MgS>WCe3z|bqjm_hJ58Fr(Tsy-qdJ?f_C8lF6RHE8Y%Zmu#w7J_k> zH%sjLT}>1ftNy6Tf5HvCA)xnpQX2OK?;~%;5+ixN)S;esIatlUBy`p*j<;kCQqz5G zMtw*dTn;yTEuy5qKQ%N2zSqj|UxvrZPl%1Dn!K=_Ic{+m42+$xJME5`K{b@Mp7K6= zgw$sI=;;~IH`JzaIsXq(1X=2K#DlC5yn=oHPZW&bYvbuc z&VJl;=)4}DMR~-()^^{tVr6}@EJJkjX}*-=(+s=NYq%3%i_%jc1eA8U$p!Y}zb(=B z2b0Q%+FIQ6g^KKT#{QnncJe}<;ycsRy zDa9wTm27R}90;3W;HS%i;V&%May;-REWz=d(ZM;u=+u9v+IdCWqzaw?Ki4p0l|NOR zpr#nnJMRD3Vb0LN<=n*ZkF$ZFW*y51lT@6gg=IB0v0ldzY8ZSKu<;XqryTgB=pN3$ zywv~WWxrt~9t!;f5~W;gH1&wB#nuG`vW#33@}>FNq5q)RKg$YxTR}zwKe$gs;Rs*CZ08^arYc95TFi;r)jD|8o_B5Xt*k6T5(@d+&C&t?^p@XW}gW-1+}3 zTFBC931QRsq($Ko+COjpgJu5y-y}8Oj{NbprL={#EUT5`3DeCt9^P9t^6PY#fnqiF>9 zWdH=uba-5?YE97B)a2`3AJSykyWAQ*@n_9GnZiB4Ow>F(^vkHbvK+d$^B=teTmvPo zF4-mH0|Hmh_U(jb?SxLSL!Q^VvT_HRLF1~H7yQ}1hX>LZAk8zuT+OSs8V{sj?!-2* zrbx&Ls%Tf^pE>4@Wy)yXAn}UJofv7d{Kb`f6{C4}g!T|r6Oudo9B@TH>U)0cY$`Wp zq6jCW(wQz49_Y8^+3m6J44zO@W3jajN+0va@IUW>{qhq&-%j0L=_8+*60^ID0VD?A z0yOUjw*7Ukmlgq19P_le=kI<2km(*f~YfB$kIe(a~&$*KtF)LYKe)@ z&Q?-fr!}?*dFgJboLl#y^SbE6rH_`hS}LFaM?N!Gen?!O$21ABw(TTB)6SnGpe`~g8H)2PlxrL`&38Zrj+$>4*vg;%#O zzp@WNPOO8L3X`rGy;$rz+9b`HkNJT{S(tJ&ml%K_HY=0*3giA^4h!VQ4dn6%H4Aw! ztiaNn;;A~{Ij>@_4WOB)%QGH_;XB*ZhTsOn0QrVoU)8cs9wBsXKwP~?U;Fd6-|++6 zl}2zYESBESiy_IYtkG<`SnG{*I=EHuM5fkP#&!whfh5{7VYtXB0w#NT_cH|LyXOQY zkNKW5NnTbp?Pj0q`gmQWbHQRP2XaQ~YpU|+U>&dR8a&t?S6}<`#x;Es+g@Df*pYJbNuddP18dEs>Ik^_UY=6GAW_V1v_%g}+ZIU24Gl2lmw z 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 literal 167121 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