概要
本記事では、Qlibを使用して、機械学習パイプライン環境を構築する第一歩について述べる。
はじめに
このブログの趣旨としては、当初は「戦略作成」→「戦略検証」→「戦略稼働」→「成果の評価」→「戦略へフィードバック」といったサイクルを管理できるような自動トレーディングシステムを作ることを考えていた。
最近、すこし株取引から離れていたのだが、最近になってまたやり始めようかなと思い、色々と現在の状況を調べはじめた。
その中で、MicrosoftのリポジトリにQlibというものがあるのを見つけた。これが2020年の8月から作られたもので、現在でもメンテされており、もしかするとこれがやりたいことにかなり近いことができるのではないかと感じた。
本記事では、そもそもQlibをどういう目的で使用できるのかを調査することを第一目標として、READMEにしたがってモデルの訓練・テストを自動で行い、その結果を図示するところをハンズオンとして実施している。
なお、Qlibについては論文になっているので、興味がある人は参考にしてほしい。
PDF: https://arxiv.org/pdf/2009.11189.pdf
Qlibの試用
動作条件
早速手引きに沿って動かしてみる。ただ普通になぞっても面白くないので、一部パラメータを変更してやることにする。
また、Docker環境+Pipenvを用意してその中で必要なパッケージ等はインストール済みの状態にする。
使用したrequirements.txt
当初はPipenvでやろうとしていましたが、色々と想定通りに動かなかったのと解決に時間がかかりそうだったので、素直に Python 3.8 を直接macに入れて、 pip install -r requirements.txt
で直接パッケージを入れることにした。
requirements.txtを表示する
matplotlib seaborn catboost xgboost numpy==1.23.5 japanize_matplotlib pyqlib qlib torch nbformat>=4.2.0
データの取得
公式の手順書と一部異なり、ホームディレクトリ以下ではなくプロジェクトルート以下にデータを置くようにした。
また、カレントディレクトリの ./qlib_original
に https://github.com/microsoft/qlib をクローンしたディレクトリを配置した。
python -m qlib.run.get_data qlib_data --target_dir ./qlib_data/cn_data --region cn
予測の実施
データの保存場所を変えたため、予め、データのパスを変更しておく。
qlib_init: # provider_uri: "~/.qlib/qlib_data/cn_data" provider_uri: "./qlib_data/cn_data"
YAMLファイル全体
qlib_init: # provider_uri: "~/.qlib/qlib_data/cn_data" provider_uri: "./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 kwargs: model: <MODEL> dataset: <DATASET> topk: 50 n_drop: 5 backtest: start_time: 2017-01-01 end_time: 2020-08-01 account: 100000000 benchmark: *benchmark exchange_kwargs: limit_threshold: 0.095 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.2 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: model: <MODEL> dataset: <DATASET> - 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
予測は下記のコマンドで実施できる。
qrun ./qlib_original/examples/benchmarks/LightGBM/workflow_config_lightgbm_Alpha158.yaml
その実行結果が以下。
Dockerで動かした時の出力を全て見る
$ qrun ./examples/benchmarks/LightGBM/workflow_config_lightgbm_Alpha158.yaml $ qrun qlib_original/examples/benchmarks/LightGBM/workflow_config_lightgbm_Alpha158.yaml [15744:MainThread](2023-05-28 19:22:40,109) INFO - qlib.Initialization - [config.py:416] - default_conf: client. [15744:MainThread](2023-05-28 19:22:40,114) INFO - qlib.Initialization - [__init__.py:74] - qlib successfully initialized based on client settings. [15744:MainThread](2023-05-28 19:22:40,114) INFO - qlib.Initialization - [__init__.py:76] - data_path={'__DEFAULT_FREQ': PosixPath('/workspaces/qlib_trading/qlib_data/cn_data')} [15744:MainThread](2023-05-28 19:22:40,146) WARNING - qlib.workflow - [expm.py:230] - No valid experiment found. Create a new experiment with name workflow. [15744:MainThread](2023-05-28 19:22:40,173) INFO - qlib.workflow - [exp.py:258] - Experiment 1 starts running ... [15744:MainThread](2023-05-28 19:22:40,382) INFO - qlib.workflow - [recorder.py:341] - Recorder 5aed6c0fc2d44d2ca39de90bf232e5f5 starts running under Experiment 1 ... Killed /home/vscode/.local/share/virtualenvs/qlib_trading-k3iKtIfp/lib/python3.8/site-packages/joblib/externals/loky/backend/resource_tracker.py:310: UserWarning: resource_tracker: There appear to be 1 leaked folder objects to clean up at shutdown warnings.warn( /home/vscode/.local/share/virtualenvs/qlib_trading-k3iKtIfp/lib/python3.8/site-packages/joblib/externals/loky/backend/resource_tracker.py:326: UserWarning: resource_tracker: /tmp/joblib_memmapping_folder_15744_014189611f674ea3af7965863fcdc02f_19ca69f69ecf414b98fc15c95140d315: FileNotFoundError(2, 'No such file or directory') warnings.warn(f'resource_tracker: {name}: {e!r}')
一時ファイルの削除に失敗したのか?
Docker内で動かすのをやめて、macネイティブで実行した場合はうまくいった。
ネイティブで動かした時の出力を全て見る
$ qrun qlib_original/examples/benchmarks/LightGBM/workflow_config_lightgbm_Alpha158.yaml [31202:MainThread](2023-05-28 21:41:23,258) INFO - qlib.Initialization - [config.py:416] - default_conf: client. [31202:MainThread](2023-05-28 21:41:23,261) INFO - qlib.Initialization - [__init__.py:74] - qlib successfully initialized based on client settings. [31202:MainThread](2023-05-28 21:41:23,262) INFO - qlib.Initialization - [__init__.py:76] - data_path={'__DEFAULT_FREQ': PosixPath('/Users/user/projects/github/qlib_trading/qlib_data/cn_data')} [31202:MainThread](2023-05-28 21:41:23,264) WARNING - qlib.workflow - [expm.py:230] - No valid experiment found. Create a new experiment with name workflow. [31202:MainThread](2023-05-28 21:41:23,267) INFO - qlib.workflow - [exp.py:258] - Experiment 1 starts running ... [31202:MainThread](2023-05-28 21:41:23,361) INFO - qlib.workflow - [recorder.py:341] - Recorder e46d7eb676da4bba92bfa9bb8b368ade starts running under Experiment 1 ... [31202:MainThread](2023-05-28 21:42:30,807) INFO - qlib.timer - [log.py:128] - Time cost: 53.933s | Loading data Done [31202:MainThread](2023-05-28 21:42:33,888) INFO - qlib.timer - [log.py:128] - Time cost: 0.676s | DropnaLabel Done /Users/user/.pyenv/versions/3.8.16/lib/python3.8/site-packages/qlib/data/dataset/processor.py:322: SettingWithCopyWarning: A value is trying to be set on a copy of a slice from a DataFrame. Try using .loc[row_indexer,col_indexer] = value instead See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy df[cols] = df[cols].groupby("datetime", group_keys=False).apply(self.zscore_func) [31202:MainThread](2023-05-28 21:42:39,297) INFO - qlib.timer - [log.py:128] - Time cost: 5.408s | CSZScoreNorm Done [31202:MainThread](2023-05-28 21:42:39,351) INFO - qlib.timer - [log.py:128] - Time cost: 8.543s | fit & process data Done [31202:MainThread](2023-05-28 21:42:39,352) INFO - qlib.timer - [log.py:128] - Time cost: 62.478s | Init data Done Training until validation scores don't improve for 50 rounds [20] train's l2: 0.981377 valid's l2: 0.993566 [40] train's l2: 0.973811 valid's l2: 0.993977 [60] train's l2: 0.967152 valid's l2: 0.994648 Early stopping, best iteration is: [18] train's l2: 0.982182 valid's l2: 0.993497 [31202:MainThread](2023-05-28 21:42:56,334) INFO - qlib.workflow - [record_temp.py:196] - Signal record 'pred.pkl' has been saved as the artifact of the Experiment 1 'The following are prediction results of the LGBModel model.' score datetime instrument 2017-01-03 SH600000 -0.027139 SH600008 0.007009 SH600009 0.009770 SH600010 0.012275 SH600015 -0.112909 {'IC': 0.04680587323833807, 'ICIR': 0.3815683918932706, 'Rank IC': 0.049049290457489736, 'Rank ICIR': 0.406748756941287} [31202:MainThread](2023-05-28 21:42:57,993) INFO - qlib.backtest caller - [__init__.py:93] - Create new exchange [31202:MainThread](2023-05-28 21:43:10,722) WARNING - qlib.online operator - [exchange.py:219] - $close field data contains nan. [31202:MainThread](2023-05-28 21:43:10,724) WARNING - qlib.online operator - [exchange.py:219] - $close field data contains nan. [31202:MainThread](2023-05-28 21:43:10,735) WARNING - qlib.online operator - [exchange.py:226] - factor.day.bin file not exists or factor contains `nan`. Order using adjusted_price. [31202:MainThread](2023-05-28 21:43:10,736) WARNING - qlib.online operator - [exchange.py:228] - trade unit 100 is not supported in adjusted_price mode. [31202:MainThread](2023-05-28 21:43:19,935) WARNING - qlib.data - [data.py:666] - load calendar error: freq=day, future=True; return current calendar! [31202:MainThread](2023-05-28 21:43:19,936) WARNING - qlib.data - [data.py:669] - You can get future calendar by referring to the following document: https://github.com/microsoft/qlib/blob/main/scripts/data_collector/contrib/README.md [31202:MainThread](2023-05-28 21:43:19,965) WARNING - qlib.BaseExecutor - [executor.py:121] - `common_infra` is not set for <qlib.backtest.executor.SimulatorExecutor object at 0x12fac2dc0> backtest loop: 0%| | 0/871 [00:00<?, ?it/s]/Users/user/.pyenv/versions/3.8.16/lib/python3.8/site-packages/qlib/utils/index_data.py:482: RuntimeWarning: Mean of empty slice return np.nanmean(self.data) backtest loop: 65%|████████████████████████████████████████████████████████████████████████████████▊ | 563/871 [00:08<00:04, 64.38it/s]/Users/user/.pyenv/versions/3.8.16/lib/python3.8/site-packages/qlib/utils/index_data.py:482: RuntimeWarning: Mean of empty slice return np.nanmean(self.data) /Users/user/.pyenv/versions/3.8.16/lib/python3.8/site-packages/qlib/utils/index_data.py:482: RuntimeWarning: Mean of empty slice return np.nanmean(self.data) backtest loop: 100%|█████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 871/871 [00:14<00:00, 61.96it/s] [31202:MainThread](2023-05-28 21:43:34,948) INFO - qlib.workflow - [record_temp.py:505] - Portfolio analysis record 'port_analysis_1day.pkl' has been saved as the artifact of the Experiment 1 'The following are analysis results of benchmark return(1day).' risk mean 0.000477 std 0.012295 annualized_return 0.113561 information_ratio 0.598699 max_drawdown -0.370479 'The following are analysis results of the excess return without cost(1day).' risk mean 0.000530 std 0.005718 annualized_return 0.126029 information_ratio 1.428574 max_drawdown -0.072310 'The following are analysis results of the excess return with cost(1day).' risk mean 0.000339 std 0.005717 annualized_return 0.080654 information_ratio 0.914486 max_drawdown -0.086083 [31202:MainThread](2023-05-28 21:43:34,964) INFO - qlib.workflow - [record_temp.py:530] - Indicator analysis record 'indicator_analysis_1day.pkl' has been saved as the artifact of the Experiment 1 'The following are analysis results of indicators(1day).' value ffr 1.0 pa 0.0 pos 0.0 [31202:MainThread](2023-05-28 21:43:34,979) INFO - qlib.timer - [log.py:128] - Time cost: 0.012s | waiting `async_log` Done
出力
次に1つずつ出力を見ていく。まずこれ。「1日のベンチマークリターンの解析結果」とある。
'The following are analysis results of benchmark return(1day).' risk mean 0.000477 std 0.012295 annualized_return 0.113561 information_ratio 0.598699 max_drawdown -0.370479
2つ目。これは「コストを考慮しない1日の超過収益の解析結果」になる。
'The following are analysis results of the excess return without cost(1day).' risk mean 0.000530 std 0.005718 annualized_return 0.126029 information_ratio 1.428574 max_drawdown -0.072310
3つ目。これは「コストを考慮した1日の超過収益の解析結果」である。
'The following are analysis results of the excess return with cost(1day).' risk mean 0.000339 std 0.005717 annualized_return 0.080654 information_ratio 0.914486 max_drawdown -0.086083
最後。これは「1日のインジケータの解析結果」とある。
'The following are analysis results of indicators(1day).' value ffr 1.0 pa 0.0 pos 0.0
図示
ソースコード
これだけ見てもさっぱりわからないのでグラフにする。 examples/workflow_by_code.ipynb
に視覚化するJupyterNotebook が置いてあるので、それを一部修正して使用する。
# examples/workflow_by_code.ipynb # Original: Copyright (c) Microsoft Corporation., Modified by dogwood008 # Licensed under the MIT License. import sys, site from pathlib import Path ################################# NOTE ################################# # Please be aware that if colab installs the latest numpy and pyqlib # # in this cell, users should RESTART the runtime in order to run the # # following cells successfully. # ######################################################################## try: import qlib except ImportError: # install qlib ! pip install --upgrade numpy ! pip install pyqlib if "google.colab" in sys.modules: # The Google colab environment is a little outdated. We have to downgrade the pyyaml to make it compatible with other packages ! pip install pyyaml==5.4.1 # reload site.main() scripts_dir = Path.cwd().parent.joinpath("scripts") if not scripts_dir.joinpath("get_data.py").exists(): # download get_data.py script scripts_dir = Path("~/tmp/qlib_code/scripts").expanduser().resolve() scripts_dir.mkdir(parents=True, exist_ok=True) import requests with requests.get("https://raw.githubusercontent.com/microsoft/qlib/main/scripts/get_data.py", timeout=10) as resp: with open(scripts_dir.joinpath("get_data.py"), "wb") as fp: fp.write(resp.content) import qlib import pandas as pd from qlib.constant 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 from qlib.utils import flatten_dict # use default data # NOTE: need to download data from remote: python scripts/get_data.py qlib_data_cn --target_dir ~/.qlib/qlib_data/cn_data provider_uri = "./qlib_data/cn_data" # target_dir <---- [MODIFIED] ≪ここを実際にデータが置かれたパスに変更≫ if not exists_qlib_data(provider_uri): print(f"Qlib data is not found in {provider_uri}") sys.path.append(str(scripts_dir)) 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" # [MODIFIED] この次にある2つのブロックをスキップ from qlib.contrib.report import analysis_model, analysis_position from qlib.data import D ba_rid = 'e200c6a4771a44bc8d6cf21fc44d643c' # <--- [MODIFIED] ここは mlruns/1/e200c6a4771a44bc8d6cf21fc44d643c のように、 qrun した際に出力されたディレクトリの下にあるUUIDっぽいものを指定 recorder = R.get_recorder(recorder_id=ba_rid, experiment_name="backtest_analysis") print(recorder) pred_df = recorder.load_object("pred.pkl") report_normal_df = recorder.load_object("portfolio_analysis/report_normal_1day.pkl") positions = recorder.load_object("portfolio_analysis/positions_normal_1day.pkl") analysis_df = recorder.load_object("portfolio_analysis/port_analysis_1day.pkl")
バックテストでのポートフォリオ分析
この状態で、
analysis_position.report_graph(report_normal_df)
を
実行すると数のような折れ線グラフを得られる。
それぞれの凡例はここにもあるが、日本語でも書いておく。(誤訳あれば教えてください)
cum bench
- ベンチマークの累積リターン系列
- Cumulative returns series of benchmark
cum return wo cost
- ポートフォリオの累積リターン系列(コスト含まず)
- Cumulative returns series of portfolio without cost
cum return w cost
- ポートフォリオの累積リターン系列(コスト含む)
- Cumulative returns series of portfolio with cost
return wo mdd
- 累積リターンの最大ドローダウン系列(コスト含まず)
- Maximum drawdown series of cumulative return without cost
return w cost mdd
- 累積リターンの最大ドローダウン系列(コスト含む)
- Maximum drawdown series of cumulative return with cost
cum ex return wo cost
- ベンチマークと比較したポートフォリオの累積異常リターン系列(コスト含まず)
- The CAR (cumulative abnormal return) series of the portfolio compared to the benchmark without cost.
cum ex return w cost
- ベンチマークと比較したポートフォリオの累積異常リターン系列(コスト含む)
- The CAR (cumulative abnormal return) series of the portfolio compared to the benchmark with cost.
turnover
- 売買回転率系列
- Turnover rate series
cum ex return wo cost mdd
- 累積異常リターンのドローダウン系列(コスト含まず)
- Drawdown series of CAR (cumulative abnormal return) without cost
cum ex return w cost mdd
- 累積異常リターンのドローダウン系列(コスト含む)
- Drawdown series of CAR (cumulative abnormal return) with cost
リスク分析、分析モデル
他にも、次のような図を得られる。先ずはリスク分析結果。
analysis_position.risk_analysis_graph(analysis_df, report_normal_df)
続いて、分析モデル。
# [ADDED] FROM import yaml # read yaml and convert it into dict path_to_config_yaml = './qlib_original/examples/benchmarks/LightGBM/workflow_config_lightgbm_Alpha158.yaml' with open(path_to_config_yaml, 'r') as stream: try: config = yaml.safe_load(stream) except yaml.YAMLError as exc: print(exc) # [ADDED] UNTIL dataset = init_instance_by_config(task["task"]["dataset"]) # <--- [MODIFIED] label_df = dataset.prepare("test", col_set="label") label_df.columns = ["label"]
pred_label = pd.concat([label_df, pred_df], axis=1, sort=True).reindex(label_df.index) analysis_position.score_ic_graph(pred_label)
analysis_model.model_performance_graph(pred_label)
おわりに
本記事では、Qlibを使ってYAMLで設定したデータセットやアルゴリズム等にしたがって自動的にモデルを訓練・テストする方法を説明した。また、それによって出力される結果をグラフによって図示する方法を紹介した。
多くの図や数値を結果として受け取ることはできたものの、それらをどう読み解くかが重要である。次回以降ではそれぞれの示す意味を理解して説明できるようになるところまでを1つの記事にしたい。