1
0
mirror of https://github.com/microsoft/qlib.git synced 2026-07-26 21:42:40 +08:00

Compare commits

...

66 Commits

Author SHA1 Message Date
you-n-g
61f2433c9b Debug MacOS CI 2022-04-27 19:28:31 +08:00
Hubedge
84ff662a26 Fixed pandas FutureWarning (#1073)
* Fixed pandas FutureWarning

`FutureWarning: Passing a set as an indexer is deprecated and will raise in a future version. Use a list instead.`

* fixed another pandas FutureWarning

```
scripts/data_collector/index.py:228: FutureWarning: The frame.append method is deprecated and will be removed from pandas in a future version. Use pandas.concat instead.
  new_df = new_df.append(_tmp_df, sort=False)
```

* fixed more pandas futurewarnings
2022-04-27 18:43:26 +08:00
金戈
00e40e775b Fixed typos in workflow.rst (#1068)
* Update workflow.rst

Fixed a typo. `please refer to Qlib Model` should be `please refer to Qlib Data` in Dataset section.

* Fix typo. `preprossing` should be `preprocessing`

* Update data.rst

Remove extra `of`.
2022-04-27 18:36:47 +08:00
code-review-doctor
45fe5e6974 Fix issue probably-meant-fstring found at https://codereview.doctor (#1072) 2022-04-25 16:12:40 +08:00
you-n-g
366a9c33f3 Bump to Dev Version 2022-04-25 16:11:47 +08:00
Young
982e0da715 Update Version 2022-04-25 00:08:08 +08:00
you-n-g
cd5e5d5235 fast fillna (#1074)
* fast fillna

* fix TSDataSampler bug
2022-04-24 23:24:32 +08:00
you-n-g
caea495f40 Update handler.py (#1044) 2022-04-22 09:16:06 +08:00
Linlang
d934c8caba fix_issue_1019_1026 (#1046)
Co-authored-by: Linlang Lv (iSoftStone) <v-linlanglv@microsoft.com>
2022-04-22 09:15:53 +08:00
wuzhe1234
a139986f4e Change Power to a NpPairOperator (#1052)
* Change Power to a NpPairOperator

* Change Power to pair operator and use black to format
2022-04-21 20:50:16 +08:00
you-n-g
12c3de42d0 Update Tutorial Notebook 2022-04-21 10:08:47 +08:00
Chia-hung Tai
fe0f9427f2 Use the region in qlib.config for FileCalendarStorage. (#1049)
* Use the region in qlib.config for FileCalendarStorage.

* Fix black.

* Make region as an optional parameter.
2022-04-20 19:20:43 +08:00
Wangwuyi123
a973e4fb66 Update test_macos.yml (#1055) 2022-04-15 18:17:16 +08:00
Wangwuyi123
c60366addd update ci with test doc (#1054) 2022-04-15 18:16:45 +08:00
you-n-g
41447f320b fix tra dataset bug (#1050) 2022-04-15 17:15:44 +08:00
you-n-g
e1271a83f7 Update setup.py (#1048) 2022-04-14 20:41:06 +08:00
Tuozhen Liu
30b531086c Fixed issue #943 about TCTS init_fore_model (#1047)
p.init_fore_model = False -> p.requires_grad = False
2022-04-14 11:23:08 +08:00
Wentao Xu
87926513cb Add the HIST and IGMTF model on Alpha360 (#1040)
* Commit the code of HIST and IGMTF on Alpha360

* add stock index

* Update README.md

* delete useless code

* fix the bug of code format with black

* fix pylint bugs

* fix the bugs of pylint

* fix pylint bugs

* fix flake8
2022-04-14 01:45:49 +08:00
plpycoin
7bfc7e1797 chore: bug-fix for crypto data collector (#1038) 2022-04-13 22:22:31 +08:00
Wangwuyi123
85e7cdcac3 Update setup.py (#1043) 2022-04-12 16:26:55 +08:00
Chao Wang
08fd1d3f42 update cli.py (#1008)
* update cli.py

update cli.py so that one can specify exp_manager uri in "qlib_init" and "experiment_name" in *.yaml file.

* black cli.py

* Resolving pre-commit-hook changes
2022-04-12 08:58:28 +08:00
you-n-g
defd6758f6 Update README.md 2022-04-11 16:06:35 +08:00
Qin Molei
61cc1a3867 Update README.md (#1039) 2022-04-10 20:57:12 +08:00
Yuchen Fang
655ed982cf Add high-frequency feature engineering code (#1022)
* highfreq data processing

* lint

* lint

* lint
2022-04-10 10:41:22 +08:00
you-n-g
2952c443ca Add Qlib notebook tutorial (#1037)
* Add Qlib notebook tutorial

* Update tutorial
2022-04-08 21:29:41 +08:00
you-n-g
7f1293ec34 Update PIT.rst 2022-04-06 22:17:27 +08:00
you-n-g
73438807f9 Add docs for CSRankNorm (#1032) 2022-04-06 19:57:27 +08:00
you-n-g
962751c72d Update README.md 2022-04-06 10:19:41 +08:00
igor17400
56cfa480dc Ibovespa index support (#990)
* feat: download ibovespa index historic composition

ibovespa(ibov) is the largest index in Brazil's stocks exchange.
The br_index folder has support for downloading new companies for the current index composition.
And has support, as well, for downloading companies from historic composition of ibov index.

Partially resolves issue #956

* fix: typo error instead of end_date, it was written end_ate

* feat: adds support for downloading stocks historic prices from Brazil's stocks exchange (B3)

Together with commit c2f933 it resolves issue #956

* fix: code formatted with black.

* wip: Creating code logic for brazils stock market data normalization

* docs: brazils stock market data normalization code documentation

* fix: code formatted the with black

* docs: fixed typo

* docs: more info about python version used to generate requirements.txt file

* docs: added BeautifulSoup requirements

* feat: removed debug prints

* feat: added ibov_index_composition variable as a class attribute of IBOVIndex

* feat: added increment to generate the four month period used by the ibov index

* refactor: Added get_instruments() method inside utils.py for better code usability.

Message in the PR request to understand the context of the change

In the course of reviewing this PR we found two issues.

    1. there are multiple places where the get_instruments() method is used,
	and we feel that scripts.index.py is the best place for the
	get_instruments() method to go.
    2. data_collector.utils has some very generic stuff put inside it.

* refactor: improve brazils stocks download speed

The reason to use retry=2 is due to the fact that
Yahoo Finance unfortunately does not keep track of the majority
of Brazilian stocks.

Therefore, the decorator deco_retry with retry argument
set to 5 will keep trying to get the stock data 5 times,
which makes the code to download Brazilians stocks very slow.

In future, this may change, but for now
I suggest to leave retry argument to 1 or 2 in
order to improve download speed.

In order to achieve this code logic an argument called retry_config
was added into YahooCollectorBR1d and YahooCollectorBR1min

* fix: added __main__ at the bottom of the script

* refactor: changed interface inside each index

Using partial as `fire.Fire(partial(get_instruments, market_index="br_index" ))`
will make the interface easier for the user to execute the script.
Then all the collector.py CLI in each folder can remove a redundant arguments.

* refactor: implemented  class interface retry into YahooCollectorBR

* docs: added BR as a possible region into the documentation

* refactor: make retry attribute part of the interface

This way we don't have to use hasattr to access the retry attribute as previously done
2022-04-06 09:01:29 +08:00
you-n-g
6edd0bf298 fix ddgda run all bug & pylint (#1031) 2022-04-03 20:43:02 +08:00
Chao Wang
fe155703b0 update doc for TopK-Drop (#1015)
updated doc for TopK-Drop.
2022-03-29 09:18:37 +08:00
Chaoying
3c4f4bfd44 Fix Chinese punctuation regex comment (#1012) 2022-03-29 09:16:21 +08:00
Linlang Lv (iSoftStone)
5200ff520a fix_download_data_for_CI 2022-03-25 16:56:02 +08:00
Linlang Lv (iSoftStone)
30e457119c add_pre-commit_and_flake8_to_CI 2022-03-25 16:56:02 +08:00
Young
243e516cf1 Add pre-commit 2022-03-25 16:56:02 +08:00
Chaoying
e229b567ad Support feature names contain Chinese punctuation (#1003) 2022-03-24 19:49:25 +08:00
you-n-g
f129bfef5d Update README.md 2022-03-24 16:21:52 +08:00
Chaoying
9dd5e07819 Add PRef operator (#988) (#1000)
* Add PRef operator (#988)

* Fix type annotations

* Add test_pref_operator test case field

* Add note to PITProvider

* Add period parameter comment
2022-03-24 15:29:08 +08:00
you-n-g
00ed35fc1b Update README.md 2022-03-23 10:47:53 +08:00
Chaoying
3f53a097b0 Fix format for PULL_REQUEST_TEMPLATE.md (#1001) 2022-03-22 18:40:30 +08:00
you-n-g
fb230a8097 Update README.md 2022-03-22 09:22:30 +08:00
you-n-g
ff4724e248 Known Limitations In Recroder (#999) 2022-03-22 09:21:48 +08:00
Chia-hung Tai
73d90f7f44 Add lightgbm min version. (#995)
See https://github.com/microsoft/LightGBM/pull/4604
2022-03-21 08:00:28 +08:00
you-n-g
b7988e6428 Add backtest example to online simulation (#984) 2022-03-19 01:53:14 +08:00
Chauncey
8efc8b92ef Optimize the pit collector script (#982)
* Optimize the pit collector script

* Add copyright notice to collector.py

* Remove unnecessary parameters for test_pit.py

* Update test_pit.py

* Update test_pit.py
2022-03-18 21:51:36 +08:00
Chauncey
f2a5ecd98a Fix comment typo (#987) 2022-03-17 19:25:15 +08:00
Linlang
705354cc28 fix-issue948 (#986)
Co-authored-by: Linlang Lv (iSoftStone) <v-linlanglv@microsoft.com>
2022-03-17 19:24:17 +08:00
you-n-g
1b5d0d4d6d Update report.rst (#980) 2022-03-15 21:30:50 +08:00
you-n-g
f4a481945b Update README.md (#981) 2022-03-15 20:40:52 +08:00
Chauncey
5f18ba7970 Fix pit download_data script TypeError (#978) (#979)
* Fix pit download_data script TypeError (#978)

* Format pit collector with black

* Format pit collector with black
2022-03-15 14:02:14 +08:00
you-n-g
2681c61c60 Fix log object bug (#977) 2022-03-14 17:33:13 +08:00
Chia-hung Tai
776b0c5bb4 Skip idx.is_lexsorted() when pandas version is larger than 1.3.0. (#973)
* Skip idx.is_lexsorted() when pandas version is larger than 1.3.0. The future warning is annoying.

* Skip idx.is_lexsorted() when pandas version is larger than 1.3.0. The future warning is annoying.

* Rewrite code.
2022-03-13 23:24:54 +08:00
Chia-hung Tai
829ad9f5e9 Use callback in LGBM.train. (#974) 2022-03-13 11:20:18 +08:00
you-n-g
921c13cc90 safe remove file and more friendly log (#967)
* save remove file and more friendly log

* fix pylint
2022-03-13 11:11:41 +08:00
Wangwuyi123
0f519f6053 Update yahooquery marked words (#966)
* Update collector.py

* Update collector.py

* Update collector.py

* Update collector.py

* Update collector.py

* Update collector.py

* Update collector.py

* Update collector.py

* Update collector.py

* Update scripts/data_collector/yahoo/collector.py

Co-authored-by: you-n-g <you-n-g@users.noreply.github.com>

* Update collector.py

* Update collector.py

* Update collector.py

Co-authored-by: you-n-g <you-n-g@users.noreply.github.com>
2022-03-12 20:49:38 +08:00
you-n-g
2ed806c846 Remove redundant import [fix pylint] (#962) 2022-03-11 12:15:37 +08:00
Jiabao Qu
d2f0bebf60 feat: add instrument context to inst_processor (#959)
* feat: add context to data loader

* refactor: add instrument to interface of InstProcessor

Co-authored-by: Jiabao Qu <qujiabao@logiocean.com>
2022-03-11 12:15:13 +08:00
you-n-g
615a381038 Merge pull request #938 from SunsetWolf/fix-csi500
Fix csi500
2022-03-11 12:09:22 +08:00
bxdd
568a88fddb fix cn annotation in PIT script (#958) 2022-03-11 10:15:20 +08:00
Chauncey
058f976727 Fix pit docs format (#957)
* Fix pit docs list format

* Fix pit docs format
2022-03-11 10:13:31 +08:00
bxdd
faa99f30fa Support Point-in-time Data Operation (#343)
* add period ops class

* black format

* add pit data read

* fix bug in period ops

* update ops runnable

* update PIT test example

* black format

* update PIT test

* update tets_PIT

* update code format

* add check_feature_exist

* black format

* optimize the PIT Algorithm

* fix bug

* update example

* update test_PIT name

* add pit collector

* black format

* fix bugs

* fix try

* fix bug & add dump_pit.py

* Successfully run and understand PIT

* Add some docs and remove a bug

* mv crypto collector

* black format

* Run succesfully after merging master

* Pass test and fix code

* remove useless PIT code

* fix PYlint

* Rename

Co-authored-by: Young <afe.young@gmail.com>
2022-03-10 14:27:52 +08:00
Linlang Lv (iSoftStone)
837067b9e1 fix-csi500 2022-03-09 23:03:28 +08:00
Chia-hung Tai
3a911bc09b Add REG_TW. (#955) 2022-03-08 23:48:27 +08:00
you-n-g
90be21bb40 Change to Dev Version 2022-03-08 22:32:28 +08:00
Linlang Lv (iSoftStone)
40dd84857c update-csi500 2022-02-28 03:48:07 +08:00
BigTreei
74cc21fc2c add CSI500 data collector 2022-02-28 03:33:36 +08:00
117 changed files with 5831 additions and 439 deletions

View File

@@ -8,7 +8,7 @@
<!--- Why is this change required? What problem does it solve? --> <!--- Why is this change required? What problem does it solve? -->
## How Has This Been Tested? ## How Has This Been Tested?
<! --- Put an `x` in all the boxes that apply: ---> <!--- Put an `x` in all the boxes that apply: --->
- [ ] Pass the test by running: `pytest qlib/tests/test_all_pipeline.py` under upper directory of `qlib`. - [ ] Pass the test by running: `pytest qlib/tests/test_all_pipeline.py` under upper directory of `qlib`.
- [ ] If you are adding a new feature, test on your own test scripts. - [ ] If you are adding a new feature, test on your own test scripts.

View File

@@ -35,6 +35,15 @@ jobs:
pip install numpy==1.19.5 ruamel.yaml pip install numpy==1.19.5 ruamel.yaml
pip install pyqlib --ignore-installed pip install pyqlib --ignore-installed
- name: Make html with sphnix
run: |
pip install -U sphinx
pip install sphinx_rtd_theme readthedocs_sphinx_ext
pip install --exists-action=w --no-cache-dir -r docs/requirements.txt
cd docs
sphinx-build -b html . build
cd ..
# Check Qlib with pylint # Check Qlib with pylint
# TODO: These problems we will solve in the future. Important among them are: W0221, W0223, W0237, E1102 # TODO: These problems we will solve in the future. Important among them are: W0221, W0223, W0237, E1102
# C0103: invalid-name # C0103: invalid-name
@@ -65,9 +74,35 @@ jobs:
pip install pylint pip install pylint
pylint --disable=C0104,C0114,C0115,C0116,C0301,C0302,C0411,C0413,C1802,R0201,R0401,R0801,R0902,R0903,R0911,R0912,R0913,R0914,R0915,R1720,W0105,W0123,W0201,W0511,W0613,W1113,W1514,E0401,E1121,C0103,C0209,R0402,R1705,R1710,R1725,R1735,W0102,W0212,W0221,W0223,W0231,W0237,W0612,W0621,W0622,W0703,W1309,E1102,E1136 --const-rgx='[a-z_][a-z0-9_]{2,30}$' qlib --init-hook "import astroid; astroid.context.InferenceContext.max_inferred = 500" pylint --disable=C0104,C0114,C0115,C0116,C0301,C0302,C0411,C0413,C1802,R0201,R0401,R0801,R0902,R0903,R0911,R0912,R0913,R0914,R0915,R1720,W0105,W0123,W0201,W0511,W0613,W1113,W1514,E0401,E1121,C0103,C0209,R0402,R1705,R1710,R1725,R1735,W0102,W0212,W0221,W0223,W0231,W0237,W0612,W0621,W0622,W0703,W1309,E1102,E1136 --const-rgx='[a-z_][a-z0-9_]{2,30}$' qlib --init-hook "import astroid; astroid.context.InferenceContext.max_inferred = 500"
# The following flake8 error codes were ignored:
# E501 line too long
# Description: We have used black to limit the length of each line to 120.
# F541 f-string is missing placeholders
# Description: The same thing is done when using pylint for detection.
# E266 too many leading '#' for block comment
# Description: To make the code more readable, a lot of "#" is used.
# This error code appears centrally in:
# qlib/backtest/executor.py
# qlib/data/ops.py
# qlib/utils/__init__.py
# E402 module level import not at top of file
# Description: There are times when module level import is not available at the top of the file.
# W503 line break before binary operator
# Description: Since black formats the length of each line of code, it has to perform a line break when a line of arithmetic is too long.
# E731 do not assign a lambda expression, use a def
# Description: Restricts the use of lambda expressions, but at some point lambda expressions are required.
# E203 whitespace before ':'
# Description: If there is whitespace before ":", it cannot pass the black check.
- name: Check Qlib with flake8
run: |
pip install --upgrade pip
pip install flake8
flake8 --ignore=E501,F541,E266,E402,W503,E731,E203 qlib
- name: Test data downloads - name: Test data downloads
run: | run: |
python scripts/get_data.py qlib_data --target_dir ~/.qlib/qlib_data/cn_data --interval 1d --region cn python scripts/get_data.py qlib_data --name qlib_data_simple --target_dir ~/.qlib/qlib_data/cn_data_simple --interval 1d --region cn
python -c "import os; userpath=os.path.expanduser('~'); os.rename(userpath + '/.qlib/qlib_data/cn_data_simple', userpath + '/.qlib/qlib_data/cn_data')"
- name: Test workflow by config (install from pip) - name: Test workflow by config (install from pip)
run: | run: |

12
.pre-commit-config.yaml Normal file
View File

@@ -0,0 +1,12 @@
repos:
- repo: https://github.com/psf/black
rev: 22.1.0
hooks:
- id: black
args: ["qlib", "-l 120"]
- repo: https://github.com/PyCQA/flake8
rev: 4.0.1
hooks:
- id: flake8
args: ["--ignore=E501,F541,E266,E402,W503,E731,E203"]

View File

@@ -11,7 +11,11 @@
Recent released features Recent released features
| Feature | Status | | Feature | Status |
| -- | ------ | | -- | ------ |
| Arctic Provider Backend & Orderbook data example | :hammer: [Rleased](https://github.com/microsoft/qlib/pull/744) on Jan 17, 2022 | | HIST and IGMTF models | :chart_with_upwards_trend: [Released](https://github.com/microsoft/qlib/pull/1040) on Apr 10, 2022 |
| Qlib [notebook tutorial](https://github.com/microsoft/qlib/tree/main/examples/tutorial) | 📖 [Released](https://github.com/microsoft/qlib/pull/1037) on Apr 7, 2022 |
| Ibovespa index data | :rice: [Released](https://github.com/microsoft/qlib/pull/990) on Apr 6, 2022 |
| Point-in-Time database | :hammer: [Released](https://github.com/microsoft/qlib/pull/343) on Mar 10, 2022 |
| Arctic Provider Backend & Orderbook data example | :hammer: [Released](https://github.com/microsoft/qlib/pull/744) on Jan 17, 2022 |
| Meta-Learning-based framework & DDG-DA | :chart_with_upwards_trend: :hammer: [Released](https://github.com/microsoft/qlib/pull/743) on Jan 10, 2022 | | Meta-Learning-based framework & DDG-DA | :chart_with_upwards_trend: :hammer: [Released](https://github.com/microsoft/qlib/pull/743) on Jan 10, 2022 |
| Planning-based portfolio optimization | :hammer: [Released](https://github.com/microsoft/qlib/pull/754) on Dec 28, 2021 | | Planning-based portfolio optimization | :hammer: [Released](https://github.com/microsoft/qlib/pull/754) on Dec 28, 2021 |
| Release Qlib v0.8.0 | :octocat: [Released](https://github.com/microsoft/qlib/releases/tag/v0.8.0) on Dec 8, 2021 | | Release Qlib v0.8.0 | :octocat: [Released](https://github.com/microsoft/qlib/releases/tag/v0.8.0) on Dec 8, 2021 |
@@ -95,9 +99,8 @@ For more details, please refer to our paper ["Qlib: An AI-oriented Quantitative
# Plans # Plans
New features under development(order by estimated release time). New features under development(order by estimated release time).
Your feedbacks about the features are very important. Your feedbacks about the features are very important.
| Feature | Status | <!-- | Feature | Status | -->
| -- | ------ | <!-- | -- | ------ | -->
| Point-in-Time database | Under review: https://github.com/microsoft/qlib/pull/343 |
# Framework of Qlib # Framework of Qlib
@@ -105,7 +108,6 @@ Your feedbacks about the features are very important.
<img src="docs/_static/img/framework.svg" /> <img src="docs/_static/img/framework.svg" />
</div> </div>
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. 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 | | Name | Description |
@@ -117,6 +119,8 @@ At the module level, Qlib is a platform that consists of the above components. T
* The modules with hand-drawn style are under development and will be released in the future. * 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. * The modules with dashed borders are highly user-customizable and extendible.
(p.s. framework image is created with https://draw.io/)
# Quick Start # Quick Start
@@ -336,6 +340,8 @@ Here is a list of models built on `Qlib`.
- [TCN based on pytorch (Shaojie Bai, et al. 2018)](examples/benchmarks/TCN/) - [TCN based on pytorch (Shaojie Bai, et al. 2018)](examples/benchmarks/TCN/)
- [ADARNN based on pytorch (YunTao Du, et al. 2021)](examples/benchmarks/ADARNN/) - [ADARNN based on pytorch (YunTao Du, et al. 2021)](examples/benchmarks/ADARNN/)
- [ADD based on pytorch (Hongshun Tang, et al.2020)](examples/benchmarks/ADD/) - [ADD based on pytorch (Hongshun Tang, et al.2020)](examples/benchmarks/ADD/)
- [IGMTF based on pytorch (Wentao Xu, et al.2021)](examples/benchmarks/IGMTF/)
- [HIST based on pytorch (Wentao Xu, et al.2021)](examples/benchmarks/HIST/)
Your PR of new Quant models is highly welcomed. Your PR of new Quant models is highly welcomed.
@@ -384,6 +390,8 @@ Dataset plays a very important role in Quant. Here is a list of the datasets bui
Your PR to build new Quant dataset is highly welcomed. Your PR to build new Quant dataset is highly welcomed.
# More About Qlib # More About Qlib
If you want to have a quick glance at the most frequently used components of qlib, you can try notebooks [here](examples/tutorial/).
The detailed documents are organized in [docs](docs/). The detailed documents are organized in [docs](docs/).
[Sphinx](http://www.sphinx-doc.org) and the readthedocs theme is required to build the documentation in html formats. [Sphinx](http://www.sphinx-doc.org) and the readthedocs theme is required to build the documentation in html formats.
```bash ```bash
@@ -468,7 +476,11 @@ If you don't know how to start to contribute, you can refer to the following exa
| Dataset | [Add a dataset](https://github.com/microsoft/qlib/pull/733) | | Dataset | [Add a dataset](https://github.com/microsoft/qlib/pull/733) |
| Models | [Implement a new model](https://github.com/microsoft/qlib/pull/689) | | Models | [Implement a new model](https://github.com/microsoft/qlib/pull/689) |
If you would like to become one of Qlib's maintainers to contribute more (e.g. help merge PR, triage issues), please contact us by email([qlib@microsoft.com](mailto:qlib@microsoft.com)). We are glad to help you to set the right permission. [Good first issues](https://github.com/microsoft/qlib/labels/good%20first%20issue) are labelled to indicate that they are easy to start your contributions.
You can find some impefect implementation in Qlib by `rg 'TODO|FIXME' qlib`
If you would like to become one of Qlib's maintainers to contribute more (e.g. help merge PR, triage issues), please contact us by email([qlib@microsoft.com](mailto:qlib@microsoft.com)). We are glad to help to upgrade your permission.
## Licence ## Licence
Most contributions require you to agree to a Most contributions require you to agree to a

136
docs/advanced/PIT.rst Normal file
View File

@@ -0,0 +1,136 @@
.. _pit:
===========================
(P)oint-(I)n-(T)ime Database
===========================
.. currentmodule:: qlib
Introduction
------------
Point-in-time data is a very important consideration when performing any sort of historical market analysis.
For example, lets say we are backtesting a trading strategy and we are using the past five years of historical data as our input.
Our model is assumed to trade once a day, at the market close, and well say we are calculating the trading signal for 1 January 2020 in our backtest. At that point, we should only have data for 1 January 2020, 31 December 2019, 30 December 2019 etc.
In financial data (especially financial reports), the same piece of data may be amended for multiple times overtime. If we only use the latest version for historical backtesting, data leakage will happen.
Point-in-time database is designed for solving this problem to make sure user get the right version of data at any historical timestamp. It will keep the performance of online trading and historical backtesting the same.
Data Preparation
----------------
Qlib provides a crawler to help users to download financial data and then a converter to dump the data in Qlib format.
Please follow `scripts/data_collector/pit/README.md <https://github.com/microsoft/qlib/tree/main/scripts/data_collector/pit/>`_ to download and convert data.
Besides, you can find some additional usage examples there.
File-based design for PIT data
------------------------------
Qlib provides a file-based storage for PIT data.
For each feature, it contains 4 columns, i.e. date, period, value, _next.
Each row corresponds to a statement.
The meaning of each feature with filename like `XXX_a.data`:
- `date`: the statement's date of publication.
- `period`: the period of the statement. (e.g. it will be quarterly frequency in most of the markets)
- If it is an annual period, it will be an integer corresponding to the year
- If it is an quarterly periods, it will be an integer like `<year><index of quarter>`. The last two decimal digits represents the index of quarter. Others represent the year.
- `value`: the described value
- `_next`: the byte index of the next occurance of the field.
Besides the feature data, an index `XXX_a.index` is included to speed up the querying performance
The statements are soted by the `date` in ascending order from the beginning of the file.
.. code-block:: python
# the data format from XXXX.data
array([(20070428, 200701, 0.090219 , 4294967295),
(20070817, 200702, 0.13933 , 4294967295),
(20071023, 200703, 0.24586301, 4294967295),
(20080301, 200704, 0.3479 , 80),
(20080313, 200704, 0.395989 , 4294967295),
(20080422, 200801, 0.100724 , 4294967295),
(20080828, 200802, 0.24996801, 4294967295),
(20081027, 200803, 0.33412001, 4294967295),
(20090325, 200804, 0.39011699, 4294967295),
(20090421, 200901, 0.102675 , 4294967295),
(20090807, 200902, 0.230712 , 4294967295),
(20091024, 200903, 0.30072999, 4294967295),
(20100402, 200904, 0.33546099, 4294967295),
(20100426, 201001, 0.083825 , 4294967295),
(20100812, 201002, 0.200545 , 4294967295),
(20101029, 201003, 0.260986 , 4294967295),
(20110321, 201004, 0.30739301, 4294967295),
(20110423, 201101, 0.097411 , 4294967295),
(20110831, 201102, 0.24825101, 4294967295),
(20111018, 201103, 0.318919 , 4294967295),
(20120323, 201104, 0.4039 , 420),
(20120411, 201104, 0.403925 , 4294967295),
(20120426, 201201, 0.112148 , 4294967295),
(20120810, 201202, 0.26484701, 4294967295),
(20121026, 201203, 0.370487 , 4294967295),
(20130329, 201204, 0.45004699, 4294967295),
(20130418, 201301, 0.099958 , 4294967295),
(20130831, 201302, 0.21044201, 4294967295),
(20131016, 201303, 0.30454299, 4294967295),
(20140325, 201304, 0.394328 , 4294967295),
(20140425, 201401, 0.083217 , 4294967295),
(20140829, 201402, 0.16450299, 4294967295),
(20141030, 201403, 0.23408499, 4294967295),
(20150421, 201404, 0.319612 , 4294967295),
(20150421, 201501, 0.078494 , 4294967295),
(20150828, 201502, 0.137504 , 4294967295),
(20151023, 201503, 0.201709 , 4294967295),
(20160324, 201504, 0.26420501, 4294967295),
(20160421, 201601, 0.073664 , 4294967295),
(20160827, 201602, 0.136576 , 4294967295),
(20161029, 201603, 0.188062 , 4294967295),
(20170415, 201604, 0.244385 , 4294967295),
(20170425, 201701, 0.080614 , 4294967295),
(20170728, 201702, 0.15151 , 4294967295),
(20171026, 201703, 0.25416601, 4294967295),
(20180328, 201704, 0.32954201, 4294967295),
(20180428, 201801, 0.088887 , 4294967295),
(20180802, 201802, 0.170563 , 4294967295),
(20181029, 201803, 0.25522 , 4294967295),
(20190329, 201804, 0.34464401, 4294967295),
(20190425, 201901, 0.094737 , 4294967295),
(20190713, 201902, 0. , 1040),
(20190718, 201902, 0.175322 , 4294967295),
(20191016, 201903, 0.25581899, 4294967295)],
dtype=[('date', '<u4'), ('period', '<u4'), ('value', '<f8'), ('_next', '<u4')])
# - each row contains 20 byte
# The data format from XXXX.index. It consists of two parts
# 1) the start index of the data. So the first part of the info will be like
2007
# 2) the remain index data will be like information below
# - The data indicate the **byte index** of first data update of a period.
# - e.g. Because the info at both byte 80 and 100 corresponds to 200704. The byte index of first occurance (i.e. 100) is recorded in the data.
array([ 0, 20, 40, 60, 100,
120, 140, 160, 180, 200,
220, 240, 260, 280, 300,
320, 340, 360, 380, 400,
440, 460, 480, 500, 520,
540, 560, 580, 600, 620,
640, 660, 680, 700, 720,
740, 760, 780, 800, 820,
840, 860, 880, 900, 920,
940, 960, 980, 1000, 1020,
1060, 4294967295], dtype=uint32)
Known limitations:
- Currently, the PIT database is designed for quarterly or annually factors, which can handle fundamental data of financial reports in most markets.
- Qlib leverage the file name to identify the type of the data. File with name like `XXX_q.data` corresponds to quarterly data. File with name like `XXX_a.data` corresponds to annual data.
- The caclulation of PIT is not performed in the optimal way. There is great potential to boost the performance of PIT data calcuation.

View File

@@ -437,7 +437,7 @@ Dataset
The ``Dataset`` module in ``Qlib`` aims to prepare data for model training and inferencing. 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 flexibility 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 motivation of this module is that we want to maximize the flexibility of different models to handle data that are suitable for themselves. This module gives the model the flexibility 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.
If user's model need process its data in a different way, user could implement his own ``Dataset`` class. If the model's If user's model need process its data in a different way, user could implement his own ``Dataset`` class. If the model's
data processing is not special, ``DatasetH`` can be used directly. data processing is not special, ``DatasetH`` can be used directly.

View File

@@ -143,3 +143,9 @@ Here is a simple exampke of what is done in ``PortAnaRecord``, which users can r
print(analysis_df) print(analysis_df)
For more information about the APIs, please refer to `Record Template API <../reference/api.html#module-qlib.workflow.record_temp>`_. For more information about the APIs, please refer to `Record Template API <../reference/api.html#module-qlib.workflow.record_temp>`_.
Known Limitations
=================
- The Python objects are saved based on pickle, which may results in issues when the environment dumping objects and loading objects are different.

View File

@@ -20,6 +20,9 @@ Introduction
- model_performance_graph - model_performance_graph
All of the accumulated profit metrics(e.g. return, max drawdown) in Qlib are calculated by summation.
This avoids the metrics or the plots being skewed exponentially over time.
Graphical Reports Graphical Reports
=================== ===================

View File

@@ -24,11 +24,8 @@ BaseStrategy
Qlib provides a base class ``qlib.strategy.base.BaseStrategy``. All strategy classes need to inherit the base class and implement its interface. Qlib provides a base class ``qlib.strategy.base.BaseStrategy``. All strategy classes need to inherit the base class and implement its interface.
- `get_risk_degree` - `generate_trade_decision`
Return the proportion of your total value you will use in investment. Dynamically risk_degree will result in Market timing. generate_trade_decision is a key interface that generates trade decisions in each trading bar.
- `generate_order_list`
Return the order list.
The frequency to call this method depends on the executor frequency("time_per_step"="day" by default). But the trading frequency can be decided by users' implementation. The frequency to call this method depends on the executor frequency("time_per_step"="day" by default). But the trading frequency can be decided by users' implementation.
For example, if the user wants to trading in weekly while the `time_per_step` is "day" in executor, user can return non-empty TradeDecision weekly(otherwise return empty like `this <https://github.com/microsoft/qlib/blob/main/qlib/contrib/strategy/signal_strategy.py#L132>`_ ). For example, if the user wants to trading in weekly while the `time_per_step` is "day" in executor, user can return non-empty TradeDecision weekly(otherwise return empty like `this <https://github.com/microsoft/qlib/blob/main/qlib/contrib/strategy/signal_strategy.py#L132>`_ ).
@@ -69,18 +66,24 @@ TopkDropoutStrategy
- Adopt the ``Topk-Drop`` algorithm to calculate the target amount of each stock - Adopt the ``Topk-Drop`` algorithm to calculate the target amount of each stock
.. note:: .. note::
``Topk-Drop`` algorithm There are two parameters for the ``Topk-Drop`` algorithm
- `Topk`: The number of stocks held - `Topk`: The number of stocks held
- `Drop`: The number of stocks sold on each trading day - `Drop`: The number of stocks sold on each trading day
Currently, the number of held stocks is `Topk`. In general, the number of stocks currently held is `Topk`, with the exception of being zero at the beginning period of trading.
On each trading day, the `Drop` number of held stocks with the worst `prediction score` will be sold, and the same number of unheld stocks with the best `prediction score` will be bought. For each trading day, let $d$ be the number of the instruments currently held and with a rank $\gt K$ when ranked by the prediction scores from high to low.
Then `d` number of stocks currently held with the worst `prediction score` will be sold, and the same number of unheld stocks with the best `prediction score` will be bought.
In general, $d=$`Drop`, especially when the pool of the candidate instruments is large, $K$ is large, and `Drop` is small.
In most cases, ``TopkDrop`` algorithm sells and buys `Drop` stocks every trading day, which yields a turnover rate of 2$\times$`Drop`/$K$.
The following images illustrate a typical scenario.
.. image:: ../_static/img/topk_drop.png .. image:: ../_static/img/topk_drop.png
:alt: Topk-Drop :alt: Topk-Drop
``TopkDrop`` algorithm sells `Drop` stocks every trading day, which guarantees a fixed turnover rate.
- Generate the order list from the target amount - Generate the order list from the target amount
@@ -164,12 +167,9 @@ Running backtest
start_time="2017-01-01", end_time="2020-08-01", strategy=strategy_obj start_time="2017-01-01", end_time="2020-08-01", strategy=strategy_obj
) )
analysis = dict() analysis = dict()
analysis["excess_return_without_cost"] = risk_analysis( # default frequency will be daily (i.e. "day")
report_normal["return"] - report_normal["bench"], freq=analysis_freq 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["excess_return_with_cost"] = risk_analysis(
report_normal["return"] - report_normal["bench"] - report_normal["cost"], freq=analysis_freq
)
analysis_df = pd.concat(analysis) # type: pd.DataFrame analysis_df = pd.concat(analysis) # type: pd.DataFrame
pprint(analysis_df) pprint(analysis_df)

View File

@@ -233,7 +233,7 @@ The meaning of each field is as follows:
Dataset Section 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 `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 Data <../component/data.html#dataset>`_.
The keywords arguments configuration of the ``DataHandler`` is as follows: The keywords arguments configuration of the ``DataHandler`` is as follows:
@@ -248,7 +248,7 @@ The keywords arguments configuration of the ``DataHandler`` is as follows:
Users can refer to the document of `DataHandler <../component/data.html#datahandler>`_ for more information about the meaning of each field in the configuration. 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. Here is the configuration for the ``Dataset`` module which will take care of data preprocessing and slicing during the training and testing phase.
.. code-block:: YAML .. code-block:: YAML

View File

@@ -17,7 +17,7 @@ When you submit a PR request, you can check whether your code passes the CI test
1. Qlib will check the code format with black. The PR will raise error if your code does not align to the standard of Qlib(e.g. a common error is the mixed use of space and tab). 1. Qlib will check the code format with black. The PR will raise error if your code does not align to the standard of Qlib(e.g. a common error is the mixed use of space and tab).
You can fix the bug by inputing the following code in the command line. You can fix the bug by inputing the following code in the command line.
.. code-block:: python .. code-block:: bash
pip install black pip install black
python -m black . -l 120 python -m black . -l 120
@@ -30,3 +30,19 @@ When you submit a PR request, you can check whether your code passes the CI test
return -ICLoss()(pred, target, index) # pylint: disable=E1130 return -ICLoss()(pred, target, index) # pylint: disable=E1130
3. Qlib will check your code style flake8. The checking command is implemented in [github action workflow](https://github.com/microsoft/qlib/blob/0e8b94a552f1c457cfa6cd2c1bb3b87ebb3fb279/.github/workflows/test.yml#L73).
You can fix the bug by inputing the following code in the command line.
.. code-block:: bash
flake8 --ignore E501,F541,E402,F401,W503,E741,E266,E203,E302,E731,E262,F523,F821,F811,F841,E713,E265,W291,E712,E722,W293 qlib
4. Qlib has integrated pre-commit, which will make it easier for developers to format their code.
Just run the following two commands, and the code will be automatically formatted using black and flake8 when the git commit command is executed.
.. code-block:: bash
pip install -e .[dev]
pre-commit install

View File

@@ -53,6 +53,7 @@ Document Structure
Online & Offline mode <advanced/server.rst> Online & Offline mode <advanced/server.rst>
Serialization <advanced/serial.rst> Serialization <advanced/serial.rst>
Task Management <advanced/task_management.rst> Task Management <advanced/task_management.rst>
Point-In-Time database <advanced/PIT.rst>
.. toctree:: .. toctree::
:maxdepth: 3 :maxdepth: 3

View File

@@ -0,0 +1,3 @@
# HIST
* Code: [https://github.com/Wentao-Xu/HIST](https://github.com/Wentao-Xu/HIST)
* Paper: [HIST: A Graph-based Framework for Stock Trend Forecasting via Mining Concept-Oriented Shared InformationAdaRNN: Adaptive Learning and Forecasting for Time Series](https://arxiv.org/abs/2110.13716).

Binary file not shown.

View File

@@ -0,0 +1,4 @@
pandas==1.1.2
numpy==1.21.0
scikit_learn==0.23.2
torch==1.7.0

View File

@@ -0,0 +1,92 @@
qlib_init:
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
kwargs:
signal:
- <MODEL>
- <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: HIST
module_path: qlib.contrib.model.pytorch_hist
kwargs:
d_feat: 6
hidden_size: 64
num_layers: 2
dropout: 0
n_epochs: 200
lr: 1e-4
early_stop: 20
metric: ic
loss: mse
base_model: LSTM
model_path: "benchmarks/LSTM/model_lstm_csi300.pkl"
stock2concept: "benchmarks/HIST/qlib_csi300_stock2concept.npy"
stock_index: "benchmarks/HIST/qlib_csi300_stock_index.npy"
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:
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

View File

@@ -0,0 +1,4 @@
# IGMTF
* Code: [https://github.com/Wentao-Xu/IGMTF](https://github.com/Wentao-Xu/IGMTF)
* Paper: [IGMTF: An Instance-wise Graph-based Framework for
Multivariate Time Series Forecasting](https://arxiv.org/abs/2109.06489).

View File

@@ -0,0 +1,4 @@
pandas==1.1.2
numpy==1.21.0
scikit_learn==0.23.2
torch==1.7.0

View File

@@ -0,0 +1,89 @@
qlib_init:
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
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: IGMTF
module_path: qlib.contrib.model.pytorch_igmtf
kwargs:
d_feat: 6
hidden_size: 64
num_layers: 2
dropout: 0
n_epochs: 200
lr: 1e-4
early_stop: 20
metric: ic
loss: mse
base_model: LSTM
model_path: "benchmarks/LSTM/model_lstm_csi300.pkl"
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:
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

View File

@@ -65,6 +65,9 @@ The numbers shown below demonstrate the performance of the entire `workflow` of
| GATs (Petar Velickovic, et al.) | Alpha360 | 0.0476±0.00 | 0.3508±0.02 | 0.0598±0.00 | 0.4604±0.01 | 0.0824±0.02 | 1.1079±0.26 | -0.0894±0.03 | | GATs (Petar Velickovic, et al.) | Alpha360 | 0.0476±0.00 | 0.3508±0.02 | 0.0598±0.00 | 0.4604±0.01 | 0.0824±0.02 | 1.1079±0.26 | -0.0894±0.03 |
| TCTS(Xueqing Wu, et al.) | Alpha360 | 0.0508±0.00 | 0.3931±0.04 | 0.0599±0.00 | 0.4756±0.03 | 0.0893±0.03 | 1.2256±0.36 | -0.0857±0.02 | | TCTS(Xueqing Wu, et al.) | Alpha360 | 0.0508±0.00 | 0.3931±0.04 | 0.0599±0.00 | 0.4756±0.03 | 0.0893±0.03 | 1.2256±0.36 | -0.0857±0.02 |
| TRA(Hengxu Lin, et al.) | Alpha360 | 0.0485±0.00 | 0.3787±0.03 | 0.0587±0.00 | 0.4756±0.03 | 0.0920±0.03 | 1.2789±0.42 | -0.0834±0.02 | | TRA(Hengxu Lin, et al.) | Alpha360 | 0.0485±0.00 | 0.3787±0.03 | 0.0587±0.00 | 0.4756±0.03 | 0.0920±0.03 | 1.2789±0.42 | -0.0834±0.02 |
| IGMTF(Wentao Xu, et al.) | Alpha360 | 0.0480±0.00 | 0.3589±0.02 | 0.0606±0.00 | 0.4773±0.01 | 0.0946±0.02 | 1.3509±0.25 | -0.0716±0.02 |
| HIST(Wentao Xu, et al.) | Alpha360 | 0.0522±0.00 | 0.3530±0.01 | 0.0667±0.00 | 0.4576±0.01 | 0.0987±0.02 | 1.3726±0.27 | -0.0681±0.01 |
- The selected 20 features are based on the feature importance of a lightgbm-based model. - The selected 20 features are based on the feature importance of a lightgbm-based model.
- The base model of DoubleEnsemble is LGBM. - The base model of DoubleEnsemble is LGBM.

View File

@@ -6,8 +6,7 @@ import torch
import numpy as np import numpy as np
import pandas as pd import pandas as pd
from qlib.utils import init_instance_by_config from qlib.data.dataset import DatasetH
from qlib.data.dataset import DatasetH, DataHandler
device = "cuda" if torch.cuda.is_available() else "cpu" device = "cuda" if torch.cuda.is_available() else "cpu"
@@ -95,7 +94,7 @@ class MTSDatasetH(DatasetH):
shuffle=True, shuffle=True,
pin_memory=False, pin_memory=False,
drop_last=False, drop_last=False,
**kwargs **kwargs,
): ):
assert horizon > 0, "please specify `horizon` to avoid data leakage" assert horizon > 0, "please specify `horizon` to avoid data leakage"
@@ -150,8 +149,15 @@ class MTSDatasetH(DatasetH):
def _prepare_seg(self, slc, **kwargs): def _prepare_seg(self, slc, **kwargs):
fn = _get_date_parse_fn(self._index[0][1]) fn = _get_date_parse_fn(self._index[0][1])
start_date = fn(slc.start)
end_date = fn(slc.stop) if isinstance(slc, slice):
start, stop = slc.start, slc.stop
elif isinstance(slc, (list, tuple)):
start, stop = slc
else:
raise NotImplementedError(f"This type of input is not supported")
start_date = fn(start)
end_date = fn(stop)
obj = copy.copy(self) # shallow copy obj = copy.copy(self) # shallow copy
# NOTE: Seriable will disable copy `self._data` so we manually assign them here # NOTE: Seriable will disable copy `self._data` so we manually assign them here
obj._data = self._data obj._data = self._data

View File

@@ -9,13 +9,10 @@ from qlib.data.dataset.handler import DataHandlerLP
import pandas as pd import pandas as pd
import fire import fire
import sys import sys
from tqdm.auto import tqdm
import yaml
import pickle import pickle
from qlib import auto_init from qlib import auto_init
from qlib.model.trainer import TrainerR, task_train from qlib.model.trainer import TrainerR
from qlib.utils import init_instance_by_config from qlib.utils import init_instance_by_config
from qlib.workflow.task.gen import RollingGen, task_generator
from qlib.workflow import R from qlib.workflow import R
from qlib.tests.data import GetData from qlib.tests.data import GetData
@@ -47,9 +44,10 @@ class DDGDA:
rb = RollingBenchmark(model_type="gbdt") rb = RollingBenchmark(model_type="gbdt")
task = rb.basic_task() task = rb.basic_task()
model = init_instance_by_config(task["model"]) with R.start(experiment_name="feature_importance"):
dataset = init_instance_by_config(task["dataset"]) model = init_instance_by_config(task["model"])
model.fit(dataset) dataset = init_instance_by_config(task["dataset"])
model.fit(dataset)
fi = model.get_feature_importance() fi = model.get_feature_importance()

View File

@@ -2,7 +2,7 @@
# Licensed under the MIT License. # Licensed under the MIT License.
""" """
This example is about how can simulate the OnlineManager based on rolling tasks. This example is about how can simulate the OnlineManager based on rolling tasks.
""" """
from pprint import pprint from pprint import pprint
@@ -15,6 +15,10 @@ from qlib.workflow.online.strategy import RollingStrategy
from qlib.workflow.task.gen import RollingGen from qlib.workflow.task.gen import RollingGen
from qlib.workflow.task.manage import TaskManager from qlib.workflow.task.manage import TaskManager
from qlib.tests.config import CSI100_RECORD_LGB_TASK_CONFIG_ONLINE, CSI100_RECORD_XGBOOST_TASK_CONFIG_ONLINE from qlib.tests.config import CSI100_RECORD_LGB_TASK_CONFIG_ONLINE, CSI100_RECORD_XGBOOST_TASK_CONFIG_ONLINE
import pandas as pd
from qlib.contrib.evaluate import backtest_daily
from qlib.contrib.evaluate import risk_analysis
from qlib.contrib.strategy import TopkDropoutStrategy
class OnlineSimulationExample: class OnlineSimulationExample:
@@ -30,6 +34,7 @@ class OnlineSimulationExample:
start_time="2018-09-10", start_time="2018-09-10",
end_time="2018-10-31", end_time="2018-10-31",
tasks=None, tasks=None,
trainer="TrainerR",
): ):
""" """
Init OnlineManagerExample. Init OnlineManagerExample.
@@ -60,7 +65,13 @@ class OnlineSimulationExample:
self.rolling_gen = RollingGen( self.rolling_gen = RollingGen(
step=rolling_step, rtype=RollingGen.ROLL_SD, ds_extra_mod_func=None step=rolling_step, rtype=RollingGen.ROLL_SD, ds_extra_mod_func=None
) # The rolling tasks generator, ds_extra_mod_func is None because we just need to simulate to 2018-10-31 and needn't change the handler end time. ) # The rolling tasks generator, ds_extra_mod_func is None because we just need to simulate to 2018-10-31 and needn't change the handler end time.
self.trainer = TrainerRM(self.exp_name, self.task_pool) # Also can be TrainerR, TrainerRM, DelayTrainerR if trainer == "TrainerRM":
self.trainer = TrainerRM(self.exp_name, self.task_pool)
elif trainer == "TrainerR":
self.trainer = TrainerR(self.exp_name)
else:
# TODO: support all the trainers: TrainerR, TrainerRM, DelayTrainerR
raise NotImplementedError(f"This type of input is not supported")
self.rolling_online_manager = OnlineManager( self.rolling_online_manager = OnlineManager(
RollingStrategy(exp_name, task_template=tasks, rolling_gen=self.rolling_gen), RollingStrategy(exp_name, task_template=tasks, rolling_gen=self.rolling_gen),
trainer=self.trainer, trainer=self.trainer,
@@ -70,7 +81,8 @@ class OnlineSimulationExample:
# Reset all things to the first status, be careful to save important data # Reset all things to the first status, be careful to save important data
def reset(self): def reset(self):
TaskManager(self.task_pool).remove() if isinstance(self.trainer, TrainerRM):
TaskManager(self.task_pool).remove()
exp = R.get_exp(experiment_name=self.exp_name) exp = R.get_exp(experiment_name=self.exp_name)
for rid in exp.list_recorders(): for rid in exp.list_recorders():
exp.delete_recorder(rid) exp.delete_recorder(rid)
@@ -84,7 +96,30 @@ class OnlineSimulationExample:
print("========== collect results ==========") print("========== collect results ==========")
print(self.rolling_online_manager.get_collector()()) print(self.rolling_online_manager.get_collector()())
print("========== signals ==========") print("========== signals ==========")
print(self.rolling_online_manager.get_signals()) signals = self.rolling_online_manager.get_signals()
print(signals)
# Backtesting
# - the code is based on this example https://qlib.readthedocs.io/en/latest/component/strategy.html
CSI300_BENCH = "SH000903"
STRATEGY_CONFIG = {
"topk": 30,
"n_drop": 3,
"signal": signals.to_frame("score"),
}
strategy_obj = TopkDropoutStrategy(**STRATEGY_CONFIG)
report_normal, positions_normal = backtest_daily(
start_time=signals.index.get_level_values("datetime").min(),
end_time=signals.index.get_level_values("datetime").max(),
strategy=strategy_obj,
)
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
pprint(analysis_df)
def worker(self): def worker(self):
# train tasks by other progress or machines for multiprocessing # train tasks by other progress or machines for multiprocessing

File diff suppressed because it is too large Load Diff

View File

@@ -256,7 +256,6 @@
"recorder = R.get_recorder(recorder_id=ba_rid, experiment_name=\"backtest_analysis\")\n", "recorder = R.get_recorder(recorder_id=ba_rid, experiment_name=\"backtest_analysis\")\n",
"print(recorder)\n", "print(recorder)\n",
"pred_df = recorder.load_object(\"pred.pkl\")\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_1day.pkl\")\n", "report_normal_df = recorder.load_object(\"portfolio_analysis/report_normal_1day.pkl\")\n",
"positions = recorder.load_object(\"portfolio_analysis/positions_normal_1day.pkl\")\n", "positions = recorder.load_object(\"portfolio_analysis/positions_normal_1day.pkl\")\n",
"analysis_df = recorder.load_object(\"portfolio_analysis/port_analysis_1day.pkl\")" "analysis_df = recorder.load_object(\"portfolio_analysis/port_analysis_1day.pkl\")"

View File

@@ -2,7 +2,7 @@
# Licensed under the MIT License. # Licensed under the MIT License.
from pathlib import Path from pathlib import Path
__version__ = "0.8.4" __version__ = "0.8.5.99"
__version__bak = __version__ # This version is backup for QlibConfig.reset_qlib_version __version__bak = __version__ # This version is backup for QlibConfig.reset_qlib_version
import os import os
from typing import Union from typing import Union
@@ -12,6 +12,7 @@ import platform
import subprocess import subprocess
from .log import get_module_logger from .log import get_module_logger
# init qlib # init qlib
def init(default_conf="client", **kwargs): def init(default_conf="client", **kwargs):
""" """

View File

@@ -1,5 +1,6 @@
# Copyright (c) Microsoft Corporation. # Copyright (c) Microsoft Corporation.
# Licensed under the MIT License. # Licensed under the MIT License.
from __future__ import annotations from __future__ import annotations
import copy import copy
from typing import List, Tuple, Union, TYPE_CHECKING from typing import List, Tuple, Union, TYPE_CHECKING
@@ -323,3 +324,6 @@ def format_decisions(
last_dec_idx = i last_dec_idx = i
res[1].append((decisions[last_dec_idx], format_decisions(decisions[last_dec_idx + 1 :]))) res[1].append((decisions[last_dec_idx], format_decisions(decisions[last_dec_idx + 1 :])))
return res return res
__all__ = ["Order"]

View File

@@ -242,7 +242,7 @@ class BaseExecutor:
if self.track_data: if self.track_data:
yield trade_decision yield trade_decision
atomic = not issubclass(self.__class__, NestedExecutor) # issubclass(A, A) is True atomic = not issubclass(self.__class__, NestedExecutor) # issubclass(A, A) is True
if atomic and trade_decision.get_range_limit(default_value=None) is not None: if atomic and trade_decision.get_range_limit(default_value=None) is not None:
raise ValueError("atomic executor doesn't support specify `range_limit`") raise ValueError("atomic executor doesn't support specify `range_limit`")

View File

@@ -28,7 +28,6 @@ class Signal(metaclass=abc.ABCMeta):
Union[pd.Series, pd.DataFrame, None]: Union[pd.Series, pd.DataFrame, None]:
returns None if no signal in the specific day returns None if no signal in the specific day
""" """
...
class SignalWCache(Signal): class SignalWCache(Signal):

View File

@@ -22,7 +22,7 @@ from pathlib import Path
from typing import Callable, Optional, Union from typing import Callable, Optional, Union
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from qlib.constant import REG_CN, REG_US from qlib.constant import REG_CN, REG_US, REG_TW
if TYPE_CHECKING: if TYPE_CHECKING:
from qlib.utils.time import Freq from qlib.utils.time import Freq
@@ -92,6 +92,7 @@ _default_config = {
"calendar_provider": "LocalCalendarProvider", "calendar_provider": "LocalCalendarProvider",
"instrument_provider": "LocalInstrumentProvider", "instrument_provider": "LocalInstrumentProvider",
"feature_provider": "LocalFeatureProvider", "feature_provider": "LocalFeatureProvider",
"pit_provider": "LocalPITProvider",
"expression_provider": "LocalExpressionProvider", "expression_provider": "LocalExpressionProvider",
"dataset_provider": "LocalDatasetProvider", "dataset_provider": "LocalDatasetProvider",
"provider": "LocalProvider", "provider": "LocalProvider",
@@ -108,7 +109,6 @@ _default_config = {
"provider_uri": "", "provider_uri": "",
# cache # cache
"expression_cache": None, "expression_cache": None,
"dataset_cache": None,
"calendar_cache": None, "calendar_cache": None,
# for simple dataset cache # for simple dataset cache
"local_cache_path": None, "local_cache_path": None,
@@ -171,6 +171,18 @@ _default_config = {
"default_exp_name": "Experiment", "default_exp_name": "Experiment",
}, },
}, },
"pit_record_type": {
"date": "I", # uint32
"period": "I", # uint32
"value": "d", # float64
"index": "I", # uint32
},
"pit_record_nan": {
"date": 0,
"period": 0,
"value": float("NAN"),
"index": 0xFFFFFFFF,
},
# Default config for MongoDB # Default config for MongoDB
"mongo": { "mongo": {
"task_url": "mongodb://localhost:27017/", "task_url": "mongodb://localhost:27017/",
@@ -184,20 +196,12 @@ _default_config = {
MODE_CONF = { MODE_CONF = {
"server": { "server": {
# data provider config
"calendar_provider": "LocalCalendarProvider",
"instrument_provider": "LocalInstrumentProvider",
"feature_provider": "LocalFeatureProvider",
"expression_provider": "LocalExpressionProvider",
"dataset_provider": "LocalDatasetProvider",
"provider": "LocalProvider",
# config it in qlib.init() # config it in qlib.init()
"provider_uri": "", "provider_uri": "",
# redis # redis
"redis_host": "127.0.0.1", "redis_host": "127.0.0.1",
"redis_port": 6379, "redis_port": 6379,
"redis_task_db": 1, "redis_task_db": 1,
"kernels": NUM_USABLE_CPU,
# cache # cache
"expression_cache": DISK_EXPRESSION_CACHE, "expression_cache": DISK_EXPRESSION_CACHE,
"dataset_cache": DISK_DATASET_CACHE, "dataset_cache": DISK_DATASET_CACHE,
@@ -205,25 +209,15 @@ MODE_CONF = {
"mount_path": None, "mount_path": None,
}, },
"client": { "client": {
# data provider config
"calendar_provider": "LocalCalendarProvider",
"instrument_provider": "LocalInstrumentProvider",
"feature_provider": "LocalFeatureProvider",
"expression_provider": "LocalExpressionProvider",
"dataset_provider": "LocalDatasetProvider",
"provider": "LocalProvider",
# config it in user's own code # config it in user's own code
"provider_uri": "~/.qlib/qlib_data/cn_data", "provider_uri": "~/.qlib/qlib_data/cn_data",
# cache # cache
# Using parameter 'remote' to announce the client is using server_cache, and the writing access will be disabled. # Using parameter 'remote' to announce the client is using server_cache, and the writing access will be disabled.
# Disable cache by default. Avoid introduce advanced features for beginners # Disable cache by default. Avoid introduce advanced features for beginners
"expression_cache": None,
"dataset_cache": None, "dataset_cache": None,
# SimpleDatasetCache directory # SimpleDatasetCache directory
"local_cache_path": Path("~/.cache/qlib_simple_cache").expanduser().resolve(), "local_cache_path": Path("~/.cache/qlib_simple_cache").expanduser().resolve(),
"calendar_cache": None,
# client config # client config
"kernels": NUM_USABLE_CPU,
"mount_path": None, "mount_path": None,
"auto_mount": False, # The nfs is already mounted on our server[auto_mount: False]. "auto_mount": False, # The nfs is already mounted on our server[auto_mount: False].
# The nfs should be auto-mounted by qlib on other # The nfs should be auto-mounted by qlib on other
@@ -257,6 +251,11 @@ _default_region_config = {
"limit_threshold": None, "limit_threshold": None,
"deal_price": "close", "deal_price": "close",
}, },
REG_TW: {
"trade_unit": 1000,
"limit_threshold": 0.1,
"deal_price": "close",
},
} }

View File

@@ -4,6 +4,7 @@
# REGION CONST # REGION CONST
REG_CN = "cn" REG_CN = "cn"
REG_US = "us" REG_US = "us"
REG_TW = "tw"
# Epsilon for avoiding division by zero. # Epsilon for avoiding division by zero.
EPS = 1e-12 EPS = 1e-12

View File

@@ -0,0 +1,164 @@
from qlib.data.dataset.handler import DataHandler, DataHandlerLP
EPSILON = 1e-4
class HighFreqHandler(DataHandlerLP):
def __init__(
self,
instruments="csi300",
start_time=None,
end_time=None,
infer_processors=[],
learn_processors=[],
fit_start_time=None,
fit_end_time=None,
drop_raw=True,
):
def check_transform_proc(proc_l):
new_l = []
for p in proc_l:
p["kwargs"].update(
{
"fit_start_time": fit_start_time,
"fit_end_time": fit_end_time,
}
)
new_l.append(p)
return new_l
infer_processors = check_transform_proc(infer_processors)
learn_processors = check_transform_proc(learn_processors)
data_loader = {
"class": "QlibDataLoader",
"kwargs": {
"config": self.get_feature_config(),
"swap_level": False,
"freq": "1min",
},
}
super().__init__(
instruments=instruments,
start_time=start_time,
end_time=end_time,
data_loader=data_loader,
infer_processors=infer_processors,
learn_processors=learn_processors,
drop_raw=drop_raw,
)
def get_feature_config(self):
fields = []
names = []
template_if = "If(IsNull({1}), {0}, {1})"
template_paused = "Select(Gt($hx_paused_num, 1.001), {0})"
def get_normalized_price_feature(price_field, shift=0):
# norm with the close price of 237th minute of yesterday.
if shift == 0:
template_norm = "{0}/DayLast(Ref({1}, 243))"
else:
template_norm = "Ref({0}, " + str(shift) + ")/DayLast(Ref({1}, 243))"
template_fillnan = "FFillNan({0})"
# calculate -> ffill -> remove paused
feature_ops = template_paused.format(
template_fillnan.format(
template_norm.format(template_if.format("$close", price_field), template_fillnan.format("$close"))
)
)
return feature_ops
fields += [get_normalized_price_feature("$open", 0)]
fields += [get_normalized_price_feature("$high", 0)]
fields += [get_normalized_price_feature("$low", 0)]
fields += [get_normalized_price_feature("$close", 0)]
fields += [get_normalized_price_feature("$vwap", 0)]
names += ["$open", "$high", "$low", "$close", "$vwap"]
fields += [get_normalized_price_feature("$open", 240)]
fields += [get_normalized_price_feature("$high", 240)]
fields += [get_normalized_price_feature("$low", 240)]
fields += [get_normalized_price_feature("$close", 240)]
fields += [get_normalized_price_feature("$vwap", 240)]
names += ["$open_1", "$high_1", "$low_1", "$close_1", "$vwap_1"]
# calculate and fill nan with 0
template_gzero = "If(Ge({0}, 0), {0}, 0)"
fields += [
template_gzero.format(
template_paused.format(
"If(IsNull({0}), 0, {0})".format("{0}/Ref(DayLast(Mean({0}, 7200)), 240)".format("$volume"))
)
)
]
names += ["$volume"]
fields += [
template_gzero.format(
template_paused.format(
"If(IsNull({0}), 0, {0})".format(
"Ref({0}, 240)/Ref(DayLast(Mean({0}, 7200)), 240)".format("$volume")
)
)
)
]
names += ["$volume_1"]
return fields, names
class HighFreqBacktestHandler(DataHandler):
def __init__(
self,
instruments="csi300",
start_time=None,
end_time=None,
):
data_loader = {
"class": "QlibDataLoader",
"kwargs": {
"config": self.get_feature_config(),
"swap_level": False,
"freq": "1min",
},
}
super().__init__(
instruments=instruments,
start_time=start_time,
end_time=end_time,
data_loader=data_loader,
)
def get_feature_config(self):
fields = []
names = []
template_if = "If(IsNull({1}), {0}, {1})"
template_paused = "Select(Gt($hx_paused_num, 1.001), {0})"
# template_paused = "{0}"
template_fillnan = "FFillNan({0})"
fields += [
template_fillnan.format(template_paused.format("$close")),
]
names += ["$close0"]
fields += [
template_paused.format(
template_if.format(
template_fillnan.format("$close"),
"$vwap",
)
)
]
names += ["$vwap0"]
fields += [template_paused.format("If(IsNull({0}), 0, {0})".format("$volume"))]
names += ["$volume0"]
fields += [template_paused.format("If(IsNull({0}), 0, {0})".format("$factor"))]
names += ["$factor0"]
return fields, names

View File

@@ -0,0 +1,81 @@
import os
import numpy as np
import pandas as pd
from qlib.data.dataset.processor import Processor
from qlib.data.dataset.utils import fetch_df_by_index
from typing import Dict
class HighFreqTrans(Processor):
def __init__(self, dtype: str = "bool"):
self.dtype = dtype
def fit(self, df_features):
pass
def __call__(self, df_features):
if self.dtype == "bool":
return df_features.astype(np.int8)
else:
return df_features.astype(np.float32)
class HighFreqNorm(Processor):
def __init__(
self,
fit_start_time: pd.Timestamp,
fit_end_time: pd.Timestamp,
feature_save_dir: str,
norm_groups: Dict[str, int],
):
self.fit_start_time = fit_start_time
self.fit_end_time = fit_end_time
self.feature_save_dir = feature_save_dir
self.norm_groups = norm_groups
def fit(self, df_features) -> None:
if os.path.exists(self.feature_save_dir) and len(os.listdir(self.feature_save_dir)) != 0:
return
os.makedirs(self.feature_save_dir)
fetch_df = fetch_df_by_index(df_features, slice(self.fit_start_time, self.fit_end_time), level="datetime")
del df_features
index = 0
names = {}
for name, dim in self.norm_groups.items():
names[name] = slice(index, index + dim)
index += dim
for name, name_val in names.items():
df_values = fetch_df.iloc(axis=1)[name_val].values
if name.endswith("volume"):
df_values = np.log1p(df_values)
self.feature_mean = np.nanmean(df_values)
np.save(self.feature_save_dir + name + "_mean.npy", self.feature_mean)
df_values = df_values - self.feature_mean
self.feature_std = np.nanstd(np.absolute(df_values))
np.save(self.feature_save_dir + name + "_std.npy", self.feature_std)
df_values = df_values / self.feature_std
np.save(self.feature_save_dir + name + "_vmax.npy", np.nanmax(df_values))
np.save(self.feature_save_dir + name + "_vmin.npy", np.nanmin(df_values))
return
def __call__(self, df_features):
if "date" in df_features:
df_features.droplevel("date", inplace=True)
df_values = df_features.values
index = 0
names = {}
for name, dim in self.norm_groups.items():
names[name] = slice(index, index + dim)
index += dim
for name, name_val in names.items():
feature_mean = np.load(self.feature_save_dir + name + "_mean.npy")
feature_std = np.load(self.feature_save_dir + name + "_std.npy")
if name.endswith("volume"):
df_values[:, name_val] = np.log1p(df_values[:, name_val])
df_values[:, name_val] -= feature_mean
df_values[:, name_val] /= feature_std
df_features = pd.DataFrame(data=df_values, index=df_features.index, columns=df_features.columns)
return df_features.fillna(0)

View File

@@ -0,0 +1,301 @@
import os
import time
import datetime
from typing import Optional
import qlib
from qlib.data import D
from qlib.config import REG_CN
from qlib.utils import init_instance_by_config
from qlib.data.dataset.handler import DataHandlerLP
from qlib.data.data import Cal
from qlib.contrib.ops.high_freq import get_calendar_day, DayLast, FFillNan, BFillNan, Date, Select, IsNull, IsInf, Cut
import pickle as pkl
from joblib import Parallel, delayed
from utilsd.logging import print_log
class HighFreqProvider:
def __init__(
self,
start_time: str,
end_time: str,
train_end_time: str,
valid_start_time: str,
valid_end_time: str,
test_start_time: str,
qlib_conf: dict,
feature_conf: dict,
label_conf: Optional[dict] = None,
backtest_conf: dict = None,
**kwargs,
) -> None:
self.start_time = start_time
self.end_time = end_time
self.test_start_time = test_start_time
self.train_end_time = train_end_time
self.valid_start_time = valid_start_time
self.valid_end_time = valid_end_time
self._init_qlib(qlib_conf)
self.feature_conf = feature_conf
self.label_conf = label_conf
self.backtest_conf = backtest_conf
self.qlib_conf = qlib_conf
def get_pre_datasets(self):
"""Generate the training, validation and test datasets for prediction
Returns:
Tuple[BaseDataset, BaseDataset, BaseDataset]: The training and test datasets
"""
dict_feature_path = self.feature_conf["path"]
train_feature_path = dict_feature_path[:-4] + "_train.pkl"
valid_feature_path = dict_feature_path[:-4] + "_valid.pkl"
test_feature_path = dict_feature_path[:-4] + "_test.pkl"
dict_label_path = self.label_conf["path"]
train_label_path = dict_label_path[:-4] + "_train.pkl"
valid_label_path = dict_label_path[:-4] + "_valid.pkl"
test_label_path = dict_label_path[:-4] + "_test.pkl"
if (
not os.path.isfile(train_feature_path)
or not os.path.isfile(valid_feature_path)
or not os.path.isfile(test_feature_path)
):
xtrain, xvalid, xtest = self._gen_data(self.feature_conf)
xtrain.to_pickle(train_feature_path)
xvalid.to_pickle(valid_feature_path)
xtest.to_pickle(test_feature_path)
del xtrain, xvalid, xtest
if (
not os.path.isfile(train_label_path)
or not os.path.isfile(valid_label_path)
or not os.path.isfile(test_label_path)
):
ytrain, yvalid, ytest = self._gen_data(self.label_conf)
ytrain.to_pickle(train_label_path)
yvalid.to_pickle(valid_label_path)
ytest.to_pickle(test_label_path)
del ytrain, yvalid, ytest
feature = {
"train": train_feature_path,
"valid": valid_feature_path,
"test": test_feature_path,
}
label = {
"train": train_label_path,
"valid": valid_label_path,
"test": test_label_path,
}
return feature, label
def get_backtest(self, **kwargs) -> None:
self._gen_data(self.backtest_conf)
def _init_qlib(self, qlib_conf):
"""initialize qlib"""
qlib.init(
region=REG_CN,
auto_mount=False,
custom_ops=[DayLast, FFillNan, BFillNan, Date, Select, IsNull, IsInf, Cut],
expression_cache=None,
**qlib_conf,
)
def _prepare_calender_cache(self):
"""preload the calendar for cache"""
# This code used the copy-on-write feature of Linux
# to avoid calculating the calendar multiple times in the subprocess.
# This code may accelerate, but may be not useful on Windows and Mac Os
Cal.calendar(freq="1min")
get_calendar_day(freq="1min")
def _gen_dataframe(self, config, datasets=["train", "valid", "test"]):
try:
path = config.pop("path")
except KeyError as e:
raise ValueError("Must specify the path to save the dataset.") from e
if os.path.isfile(path):
start = time.time()
print_log("Dataset exists, load from disk.", __name__)
# res = dataset.prepare(['train', 'valid', 'test'])
with open(path, "rb") as f:
data = pkl.load(f)
if isinstance(data, dict):
res = [data[i] for i in datasets]
else:
res = data.prepare(datasets)
print_log(f"Data loaded, time cost: {time.time() - start:.2f}", __name__)
else:
if not os.path.exists(os.path.dirname(path)):
os.makedirs(os.path.dirname(path))
print_log("Generating dataset", __name__)
start_time = time.time()
self._prepare_calender_cache()
dataset = init_instance_by_config(config)
trainset, validset, testset = dataset.prepare(["train", "valid", "test"])
data = {
"train": trainset,
"valid": validset,
"test": testset,
}
with open(path, "wb") as f:
pkl.dump(data, f)
with open(path[:-4] + "train.pkl", "wb") as f:
pkl.dump(trainset, f)
with open(path[:-4] + "valid.pkl", "wb") as f:
pkl.dump(validset, f)
with open(path[:-4] + "test.pkl", "wb") as f:
pkl.dump(testset, f)
res = [data[i] for i in datasets]
print_log(f"Data generated, time cost: {(time.time() - start_time):.2f}", __name__)
return res
def _gen_data(self, config, datasets=["train", "valid", "test"]):
try:
path = config.pop("path")
except KeyError as e:
raise ValueError("Must specify the path to save the dataset.") from e
if os.path.isfile(path):
start = time.time()
print_log("Dataset exists, load from disk.", __name__)
# res = dataset.prepare(['train', 'valid', 'test'])
with open(path, "rb") as f:
data = pkl.load(f)
if isinstance(data, dict):
res = [data[i] for i in datasets]
else:
res = data.prepare(datasets)
print_log(f"Data loaded, time cost: {time.time() - start:.2f}", __name__)
else:
if not os.path.exists(os.path.dirname(path)):
os.makedirs(os.path.dirname(path))
print_log("Generating dataset", __name__)
start_time = time.time()
self._prepare_calender_cache()
dataset = init_instance_by_config(config)
dataset.config(dump_all=True, recursive=True)
dataset.to_pickle(path)
res = dataset.prepare(datasets)
print_log(f"Data generated, time cost: {(time.time() - start_time):.2f}", __name__)
return res
def _gen_dataset(self, config):
try:
path = config.pop("path")
except KeyError as e:
raise ValueError("Must specify the path to save the dataset.") from e
if os.path.isfile(path):
start = time.time()
print_log("Dataset exists, load from disk.", __name__)
with open(path, "rb") as f:
dataset = pkl.load(f)
print_log(f"Data loaded, time cost: {time.time() - start:.2f}", __name__)
else:
start = time.time()
if not os.path.exists(os.path.dirname(path)):
os.makedirs(os.path.dirname(path))
print_log("Generating dataset", __name__)
self._prepare_calender_cache()
dataset = init_instance_by_config(config)
print_log(f"Dataset init, time cost: {time.time() - start:.2f}", __name__)
dataset.prepare(["train", "valid", "test"])
print_log(f"Dataset prepared, time cost: {time.time() - start:.2f}", __name__)
dataset.config(dump_all=True, recursive=True)
dataset.to_pickle(path)
return dataset
def _gen_day_dataset(self, config, conf_type):
try:
path = config.pop("path")
except KeyError as e:
raise ValueError("Must specify the path to save the dataset.") from e
if os.path.isfile(path + "tmp_dataset.pkl"):
start = time.time()
print_log("Dataset exists, load from disk.", __name__)
else:
start = time.time()
if not os.path.exists(os.path.dirname(path)):
os.makedirs(os.path.dirname(path))
print_log("Generating dataset", __name__)
self._prepare_calender_cache()
dataset = init_instance_by_config(config)
print_log(f"Dataset init, time cost: {time.time() - start:.2f}", __name__)
dataset.config(dump_all=False, recursive=True)
dataset.to_pickle(path + "tmp_dataset.pkl")
with open(path + "tmp_dataset.pkl", "rb") as f:
new_dataset = pkl.load(f)
time_list = D.calendar(start_time=self.start_time, end_time=self.end_time, freq="1min")[::240]
def generate_dataset(times):
if os.path.isfile(path + times.strftime("%Y-%m-%d") + ".pkl"):
print("exist " + times.strftime("%Y-%m-%d"))
return
self._init_qlib(self.qlib_conf)
end_times = times + datetime.timedelta(days=1)
new_dataset.handler.config(**{"start_time": times, "end_time": end_times})
if conf_type == "backtest":
new_dataset.handler.setup_data()
else:
new_dataset.handler.setup_data(init_type=DataHandlerLP.IT_LS)
new_dataset.config(dump_all=True, recursive=True)
new_dataset.to_pickle(path + times.strftime("%Y-%m-%d") + ".pkl")
Parallel(n_jobs=8)(delayed(generate_dataset)(times) for times in time_list)
def _gen_stock_dataset(self, config, conf_type):
try:
path = config.pop("path")
except KeyError as e:
raise ValueError("Must specify the path to save the dataset.") from e
if os.path.isfile(path + "tmp_dataset.pkl"):
start = time.time()
print_log("Dataset exists, load from disk.", __name__)
else:
start = time.time()
if not os.path.exists(os.path.dirname(path)):
os.makedirs(os.path.dirname(path))
print_log("Generating dataset", __name__)
self._prepare_calender_cache()
dataset = init_instance_by_config(config)
print_log(f"Dataset init, time cost: {time.time() - start:.2f}", __name__)
dataset.config(dump_all=False, recursive=True)
dataset.to_pickle(path + "tmp_dataset.pkl")
with open(path + "tmp_dataset.pkl", "rb") as f:
new_dataset = pkl.load(f)
instruments = D.instruments(market="all")
stock_list = D.list_instruments(
instruments=instruments, start_time=self.start_time, end_time=self.end_time, freq="1min", as_list=True
)
def generate_dataset(stock):
if os.path.isfile(path + stock + ".pkl"):
print("exist " + stock)
return
self._init_qlib(self.qlib_conf)
new_dataset.handler.config(**{"instruments": [stock]})
if conf_type == "backtest":
new_dataset.handler.setup_data()
else:
new_dataset.handler.setup_data(init_type=DataHandlerLP.IT_LS)
new_dataset.config(dump_all=True, recursive=True)
new_dataset.to_pickle(path + stock + ".pkl")
Parallel(n_jobs=32)(delayed(generate_dataset)(stock) for stock in stock_list)

View File

@@ -164,14 +164,14 @@ import builtins
def _isinstance(instance, cls): def _isinstance(instance, cls):
if isinstance_orig(instance, SepDataFrame): # pylint: disable=E0602 if isinstance_orig(instance, SepDataFrame): # pylint: disable=E0602 # noqa: F821
if isinstance(cls, Iterable): if isinstance(cls, Iterable):
for c in cls: for c in cls:
if c is pd.DataFrame: if c is pd.DataFrame:
return True return True
elif cls is pd.DataFrame: elif cls is pd.DataFrame:
return True return True
return isinstance_orig(instance, cls) # pylint: disable=E0602 return isinstance_orig(instance, cls) # pylint: disable=E0602 # noqa: F821
builtins.isinstance_orig = builtins.isinstance builtins.isinstance_orig = builtins.isinstance

View File

@@ -123,7 +123,7 @@ def pred_autocorr(pred: pd.Series, lag=1, inst_col="instrument", date_col="datet
""" """
if isinstance(pred, pd.DataFrame): if isinstance(pred, pd.DataFrame):
pred = pred.iloc[:, 0] pred = pred.iloc[:, 0]
get_module_logger("pred_autocorr").warning("Only the first column in {pred.columns} of `pred` is kept") get_module_logger("pred_autocorr").warning(f"Only the first column in {pred.columns} of `pred` is kept")
pred_ustk = pred.sort_index().unstack(inst_col) pred_ustk = pred.sort_index().unstack(inst_col)
corr_s = {} corr_s = {}
for (idx, cur), (_, prev) in zip(pred_ustk.iterrows(), pred_ustk.shift(lag).iterrows()): for (idx, cur), (_, prev) in zip(pred_ustk.iterrows(), pred_ustk.shift(lag).iterrows()):

View File

@@ -2,3 +2,6 @@
# Licensed under the MIT License. # Licensed under the MIT License.
from .data_selection import MetaTaskDS, MetaDatasetDS, MetaModelDS from .data_selection import MetaTaskDS, MetaDatasetDS, MetaModelDS
__all__ = ["MetaTaskDS", "MetaDatasetDS", "MetaModelDS"]

View File

@@ -3,3 +3,6 @@
from .dataset import MetaDatasetDS, MetaTaskDS from .dataset import MetaDatasetDS, MetaTaskDS
from .model import MetaModelDS from .model import MetaModelDS
__all__ = ["MetaDatasetDS", "MetaTaskDS", "MetaModelDS"]

View File

@@ -10,7 +10,6 @@ from tqdm.auto import tqdm
import copy import copy
from typing import Union, List from typing import Union, List
from ....data.dataset.weight import Reweighter
from ....model.meta.dataset import MetaTaskDataset from ....model.meta.dataset import MetaTaskDataset
from ....model.meta.model import MetaTaskModel from ....model.meta.model import MetaTaskModel
from ....workflow import R from ....workflow import R
@@ -18,8 +17,8 @@ from .utils import ICLoss
from .dataset import MetaDatasetDS from .dataset import MetaDatasetDS
from qlib.log import get_module_logger from qlib.log import get_module_logger
from qlib.data.dataset.weight import Reweighter
from qlib.model.meta.task import MetaTask from qlib.model.meta.task import MetaTask
from qlib.data.dataset.weight import Reweighter
from qlib.contrib.meta.data_selection.net import PredNet from qlib.contrib.meta.data_selection.net import PredNet
logger = get_module_logger("data selection") logger = get_module_logger("data selection")
@@ -98,7 +97,6 @@ class MetaModelDS(MetaTaskModel):
if phase == "train": if phase == "train":
opt.zero_grad() opt.zero_grad()
norm_loss = nn.MSELoss()
loss.backward() loss.backward()
opt.step() opt.step()
elif phase == "test": elif phase == "test":

View File

@@ -10,17 +10,19 @@ try:
from .gbdt import LGBModel from .gbdt import LGBModel
except ModuleNotFoundError: except ModuleNotFoundError:
DEnsembleModel, LGBModel = None, None DEnsembleModel, LGBModel = None, None
print("Please install necessary libs for DEnsembleModel and LGBModel, such as lightgbm.") print(
"ModuleNotFoundError. DEnsembleModel and LGBModel are skipped. (optional: maybe installing lightgbm can fix it.)"
)
try: try:
from .xgboost import XGBModel from .xgboost import XGBModel
except ModuleNotFoundError: except ModuleNotFoundError:
XGBModel = None XGBModel = None
print("Please install necessary libs for XGBModel, such as xgboost.") print("ModuleNotFoundError. XGBModel is skipped(optional: maybe installing xgboost can fix it).")
try: try:
from .linear import LinearModel from .linear import LinearModel
except ModuleNotFoundError: except ModuleNotFoundError:
LinearModel = None LinearModel = None
print("Please install necessary libs for LinearModel, such as scipy and sklearn.") print("ModuleNotFoundError. LinearModel is skipped(optional: maybe installing scipy and sklearn can fix it).")
# import pytorch models # import pytorch models
try: try:
from .pytorch_alstm import ALSTM from .pytorch_alstm import ALSTM
@@ -36,6 +38,6 @@ try:
pytorch_classes = (ALSTM, GATs, GRU, LSTM, DNNModelPytorch, TabnetModel, SFM_Model, TCN, ADD) pytorch_classes = (ALSTM, GATs, GRU, LSTM, DNNModelPytorch, TabnetModel, SFM_Model, TCN, ADD)
except ModuleNotFoundError: except ModuleNotFoundError:
pytorch_classes = () pytorch_classes = ()
print("Please install necessary libs for PyTorch models.") print("ModuleNotFoundError. PyTorch models are skipped (optional: maybe installing pytorch can fix it).")
all_model_classes = (CatBoostModel, DEnsembleModel, LGBModel, XGBModel, LinearModel) + pytorch_classes all_model_classes = (CatBoostModel, DEnsembleModel, LGBModel, XGBModel, LinearModel) + pytorch_classes

View File

@@ -249,7 +249,7 @@ class DEnsembleModel(Model, FeatureInt):
return pred return pred
def predict_sub(self, submodel, df_data, features): def predict_sub(self, submodel, df_data, features):
x_data, y_data = df_data["feature"].loc[:, features], df_data["label"] x_data = df_data["feature"].loc[:, features]
pred_sub = pd.Series(submodel.predict(x_data.values), index=x_data.index) pred_sub = pd.Series(submodel.predict(x_data.values), index=x_data.index)
return pred_sub return pred_sub

View File

@@ -68,17 +68,19 @@ class LGBModel(ModelFT, LightGBMFInt):
evals_result = {} # in case of unsafety of Python default values evals_result = {} # in case of unsafety of Python default values
ds_l = self._prepare_data(dataset, reweighter) ds_l = self._prepare_data(dataset, reweighter)
ds, names = list(zip(*ds_l)) ds, names = list(zip(*ds_l))
early_stopping_callback = lgb.early_stopping(
self.early_stopping_rounds if early_stopping_rounds is None else early_stopping_rounds
)
# NOTE: if you encounter error here. Please upgrade your lightgbm
verbose_eval_callback = lgb.log_evaluation(period=verbose_eval)
evals_result_callback = lgb.record_evaluation(evals_result)
self.model = lgb.train( self.model = lgb.train(
self.params, self.params,
ds[0], # training dataset ds[0], # training dataset
num_boost_round=self.num_boost_round if num_boost_round is None else num_boost_round, num_boost_round=self.num_boost_round if num_boost_round is None else num_boost_round,
valid_sets=ds, valid_sets=ds,
valid_names=names, valid_names=names,
early_stopping_rounds=( callbacks=[early_stopping_callback, verbose_eval_callback, evals_result_callback],
self.early_stopping_rounds if early_stopping_rounds is None else early_stopping_rounds
),
verbose_eval=verbose_eval,
evals_result=evals_result,
**kwargs, **kwargs,
) )
for k in names: for k in names:
@@ -110,6 +112,7 @@ class LGBModel(ModelFT, LightGBMFInt):
dtrain, _ = self._prepare_data(dataset, reweighter) # pylint: disable=W0632 dtrain, _ = self._prepare_data(dataset, reweighter) # pylint: disable=W0632
if dtrain.empty: if dtrain.empty:
raise ValueError("Empty data from dataset, please check your dataset config.") raise ValueError("Empty data from dataset, please check your dataset config.")
verbose_eval_callback = lgb.log_evaluation(period=verbose_eval)
self.model = lgb.train( self.model = lgb.train(
self.params, self.params,
dtrain, dtrain,
@@ -117,5 +120,5 @@ class LGBModel(ModelFT, LightGBMFInt):
init_model=self.model, init_model=self.model,
valid_sets=[dtrain], valid_sets=[dtrain],
valid_names=["train"], valid_names=["train"],
verbose_eval=verbose_eval, callbacks=[verbose_eval_callback],
) )

View File

@@ -110,18 +110,21 @@ class HFLGBModel(ModelFT, LightGBMFInt):
num_boost_round=1000, num_boost_round=1000,
early_stopping_rounds=50, early_stopping_rounds=50,
verbose_eval=20, verbose_eval=20,
evals_result=dict(), evals_result=None,
): ):
if evals_result is None:
evals_result = dict()
dtrain, dvalid = self._prepare_data(dataset) dtrain, dvalid = self._prepare_data(dataset)
early_stopping_callback = lgb.early_stopping(early_stopping_rounds)
verbose_eval_callback = lgb.log_evaluation(period=verbose_eval)
evals_result_callback = lgb.record_evaluation(evals_result)
self.model = lgb.train( self.model = lgb.train(
self.params, self.params,
dtrain, dtrain,
num_boost_round=num_boost_round, num_boost_round=num_boost_round,
valid_sets=[dtrain, dvalid], valid_sets=[dtrain, dvalid],
valid_names=["train", "valid"], valid_names=["train", "valid"],
early_stopping_rounds=early_stopping_rounds, callbacks=[early_stopping_callback, verbose_eval_callback, evals_result_callback],
verbose_eval=verbose_eval,
evals_result=evals_result,
) )
evals_result["train"] = list(evals_result["train"].values())[0] evals_result["train"] = list(evals_result["train"].values())[0]
evals_result["valid"] = list(evals_result["valid"].values())[0] evals_result["valid"] = list(evals_result["valid"].values())[0]
@@ -147,6 +150,7 @@ class HFLGBModel(ModelFT, LightGBMFInt):
""" """
# Based on existing model and finetune by train more rounds # Based on existing model and finetune by train more rounds
dtrain, _ = self._prepare_data(dataset) dtrain, _ = self._prepare_data(dataset)
verbose_eval_callback = lgb.log_evaluation(period=verbose_eval)
self.model = lgb.train( self.model = lgb.train(
self.params, self.params,
dtrain, dtrain,
@@ -154,5 +158,5 @@ class HFLGBModel(ModelFT, LightGBMFInt):
init_model=self.model, init_model=self.model,
valid_sets=[dtrain], valid_sets=[dtrain],
valid_names=["train"], valid_names=["train"],
verbose_eval=verbose_eval, callbacks=[verbose_eval_callback],
) )

View File

@@ -0,0 +1,501 @@
# 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
from typing import Text, Union
import urllib.request
import copy
from ...utils import get_or_create_path
from ...log import get_module_logger
import torch
import torch.nn as nn
import torch.optim as optim
from .pytorch_utils import count_parameters
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 HIST(Model):
"""HIST Model
Parameters
----------
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
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="",
early_stop=20,
loss="mse",
base_model="GRU",
model_path=None,
stock2concept=None,
stock_index=None,
optimizer="adam",
GPU=0,
seed=None,
**kwargs
):
# Set logger.
self.logger = get_module_logger("HIST")
self.logger.info("HIST 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.model_path = model_path
self.stock2concept = stock2concept
self.stock_index = stock_index
self.device = torch.device("cuda:%d" % (GPU) if torch.cuda.is_available() and GPU >= 0 else "cpu")
self.seed = seed
self.logger.info(
"HIST parameters setting:"
"\nd_feat : {}"
"\nhidden_size : {}"
"\nnum_layers : {}"
"\ndropout : {}"
"\nn_epochs : {}"
"\nlr : {}"
"\nmetric : {}"
"\nearly_stop : {}"
"\noptimizer : {}"
"\nloss_type : {}"
"\nbase_model : {}"
"\nmodel_path : {}"
"\nstock2concept : {}"
"\nstock_index : {}"
"\nuse_GPU : {}"
"\nseed : {}".format(
d_feat,
hidden_size,
num_layers,
dropout,
n_epochs,
lr,
metric,
early_stop,
optimizer.lower(),
loss,
base_model,
model_path,
stock2concept,
stock_index,
GPU,
seed,
)
)
if self.seed is not None:
np.random.seed(self.seed)
torch.manual_seed(self.seed)
self.HIST_model = HISTModel(
d_feat=self.d_feat,
hidden_size=self.hidden_size,
num_layers=self.num_layers,
dropout=self.dropout,
base_model=self.base_model,
)
self.logger.info("model:\n{:}".format(self.HIST_model))
self.logger.info("model size: {:.4f} MB".format(count_parameters(self.HIST_model)))
if optimizer.lower() == "adam":
self.train_optimizer = optim.Adam(self.HIST_model.parameters(), lr=self.lr)
elif optimizer.lower() == "gd":
self.train_optimizer = optim.SGD(self.HIST_model.parameters(), lr=self.lr)
else:
raise NotImplementedError("optimizer {} is not supported!".format(optimizer))
self.fitted = False
self.HIST_model.to(self.device)
@property
def use_gpu(self):
return self.device != torch.device("cpu")
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":
x = pred[mask]
y = label[mask]
vx = x - torch.mean(x)
vy = y - torch.mean(y)
return torch.sum(vx * vy) / (torch.sqrt(torch.sum(vx**2)) * torch.sqrt(torch.sum(vy**2)))
if 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 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 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, stock_index):
stock2concept_matrix = np.load(self.stock2concept)
x_train_values = x_train.values
y_train_values = np.squeeze(y_train.values)
stock_index = stock_index.values
stock_index[np.isnan(stock_index)] = 733
self.HIST_model.train()
# 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):
batch = slice(idx, idx + count)
feature = torch.from_numpy(x_train_values[batch]).float().to(self.device)
concept_matrix = torch.from_numpy(stock2concept_matrix[stock_index[batch]]).float().to(self.device)
label = torch.from_numpy(y_train_values[batch]).float().to(self.device)
pred = self.HIST_model(feature, concept_matrix)
loss = self.loss_fn(pred, label)
self.train_optimizer.zero_grad()
loss.backward()
torch.nn.utils.clip_grad_value_(self.HIST_model.parameters(), 3.0)
self.train_optimizer.step()
def test_epoch(self, data_x, data_y, stock_index):
# prepare training data
stock2concept_matrix = np.load(self.stock2concept)
x_values = data_x.values
y_values = np.squeeze(data_y.values)
stock_index = stock_index.values
stock_index[np.isnan(stock_index)] = 733
self.HIST_model.eval()
scores = []
losses = []
# 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):
batch = slice(idx, idx + count)
feature = torch.from_numpy(x_values[batch]).float().to(self.device)
concept_matrix = torch.from_numpy(stock2concept_matrix[stock_index[batch]]).float().to(self.device)
label = torch.from_numpy(y_values[batch]).float().to(self.device)
with torch.no_grad():
pred = self.HIST_model(feature, concept_matrix)
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(),
save_path=None,
):
df_train, df_valid, df_test = dataset.prepare(
["train", "valid", "test"],
col_set=["feature", "label"],
data_key=DataHandlerLP.DK_L,
)
if df_train.empty or df_valid.empty:
raise ValueError("Empty data from dataset, please check your dataset config.")
if not os.path.exists(self.stock2concept):
url = "http://fintech.msra.cn/stock_data/downloads/qlib_csi300_stock2concept.npy"
urllib.request.urlretrieve(url, self.stock2concept)
stock_index = np.load(self.stock_index, allow_pickle=True).item()
df_train["stock_index"] = 733
df_train["stock_index"] = df_train.index.get_level_values("instrument").map(stock_index)
df_valid["stock_index"] = 733
df_valid["stock_index"] = df_valid.index.get_level_values("instrument").map(stock_index)
x_train, y_train, stock_index_train = df_train["feature"], df_train["label"], df_train["stock_index"]
x_valid, y_valid, stock_index_valid = df_valid["feature"], df_valid["label"], df_valid["stock_index"]
save_path = get_or_create_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.base_model == "LSTM":
pretrained_model = LSTMModel()
elif self.base_model == "GRU":
pretrained_model = GRUModel()
else:
raise ValueError("unknown base model name `%s`" % self.base_model)
if self.model_path is not None:
self.logger.info("Loading pretrained model...")
pretrained_model.load_state_dict(torch.load(self.model_path))
model_dict = self.HIST_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.HIST_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, stock_index_train)
self.logger.info("evaluating...")
train_loss, train_score = self.test_epoch(x_train, y_train, stock_index_train)
val_loss, val_score = self.test_epoch(x_valid, y_valid, stock_index_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.HIST_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.HIST_model.load_state_dict(best_param)
torch.save(best_param, save_path)
def predict(self, dataset: DatasetH, segment: Union[Text, slice] = "test"):
if not self.fitted:
raise ValueError("model is not fitted yet!")
stock2concept_matrix = np.load(self.stock2concept)
stock_index = np.load(self.stock_index, allow_pickle=True).item()
df_test = dataset.prepare(segment, col_set="feature", data_key=DataHandlerLP.DK_I)
df_test["stock_index"] = 733
df_test["stock_index"] = df_test.index.get_level_values("instrument").map(stock_index)
stock_index_test = df_test["stock_index"].values
stock_index_test[np.isnan(stock_index_test)] = 733
stock_index_test = stock_index_test.astype("int")
df_test = df_test.drop(["stock_index"], axis=1)
index = df_test.index
self.HIST_model.eval()
x_values = df_test.values
preds = []
# organize the data into daily batches
daily_index, daily_count = self.get_daily_inter(df_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().to(self.device)
concept_matrix = torch.from_numpy(stock2concept_matrix[stock_index_test[batch]]).float().to(self.device)
with torch.no_grad():
pred = self.HIST_model(x_batch, concept_matrix).detach().cpu().numpy()
preds.append(pred)
return pd.Series(np.concatenate(preds), index=index)
class HISTModel(nn.Module):
def __init__(self, d_feat=6, hidden_size=64, num_layers=2, dropout=0.0, base_model="GRU"):
super().__init__()
self.d_feat = d_feat
self.hidden_size = hidden_size
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.fc_es = nn.Linear(hidden_size, hidden_size)
torch.nn.init.xavier_uniform_(self.fc_es.weight)
self.fc_is = nn.Linear(hidden_size, hidden_size)
torch.nn.init.xavier_uniform_(self.fc_is.weight)
self.fc_es_middle = nn.Linear(hidden_size, hidden_size)
torch.nn.init.xavier_uniform_(self.fc_es_middle.weight)
self.fc_is_middle = nn.Linear(hidden_size, hidden_size)
torch.nn.init.xavier_uniform_(self.fc_is_middle.weight)
self.fc_es_fore = nn.Linear(hidden_size, hidden_size)
torch.nn.init.xavier_uniform_(self.fc_es_fore.weight)
self.fc_is_fore = nn.Linear(hidden_size, hidden_size)
torch.nn.init.xavier_uniform_(self.fc_is_fore.weight)
self.fc_indi_fore = nn.Linear(hidden_size, hidden_size)
torch.nn.init.xavier_uniform_(self.fc_indi_fore.weight)
self.fc_es_back = nn.Linear(hidden_size, hidden_size)
torch.nn.init.xavier_uniform_(self.fc_es_back.weight)
self.fc_is_back = nn.Linear(hidden_size, hidden_size)
torch.nn.init.xavier_uniform_(self.fc_is_back.weight)
self.fc_indi = nn.Linear(hidden_size, hidden_size)
torch.nn.init.xavier_uniform_(self.fc_indi.weight)
self.leaky_relu = nn.LeakyReLU()
self.softmax_s2t = torch.nn.Softmax(dim=0)
self.softmax_t2s = torch.nn.Softmax(dim=1)
self.fc_out_es = nn.Linear(hidden_size, 1)
self.fc_out_is = nn.Linear(hidden_size, 1)
self.fc_out_indi = nn.Linear(hidden_size, 1)
self.fc_out = nn.Linear(hidden_size, 1)
def cal_cos_similarity(self, x, y): # the 2nd dimension of x and y are the same
xy = x.mm(torch.t(y))
x_norm = torch.sqrt(torch.sum(x * x, dim=1)).reshape(-1, 1)
y_norm = torch.sqrt(torch.sum(y * y, dim=1)).reshape(-1, 1)
cos_similarity = xy / (x_norm.mm(torch.t(y_norm)) + 1e-6)
return cos_similarity
def forward(self, x, concept_matrix):
device = torch.device(torch.get_device(x))
x_hidden = x.reshape(len(x), self.d_feat, -1) # [N, F, T]
x_hidden = x_hidden.permute(0, 2, 1) # [N, T, F]
x_hidden, _ = self.rnn(x_hidden)
x_hidden = x_hidden[:, -1, :]
# Predefined Concept Module
stock_to_concept = concept_matrix
stock_to_concept_sum = torch.sum(stock_to_concept, 0).reshape(1, -1).repeat(stock_to_concept.shape[0], 1)
stock_to_concept_sum = stock_to_concept_sum.mul(concept_matrix)
stock_to_concept_sum = stock_to_concept_sum + (
torch.ones(stock_to_concept.shape[0], stock_to_concept.shape[1]).to(device)
)
stock_to_concept = stock_to_concept / stock_to_concept_sum
hidden = torch.t(stock_to_concept).mm(x_hidden)
hidden = hidden[hidden.sum(1) != 0]
concept_to_stock = self.cal_cos_similarity(x_hidden, hidden)
concept_to_stock = self.softmax_t2s(concept_to_stock)
e_shared_info = concept_to_stock.mm(hidden)
e_shared_info = self.fc_es(e_shared_info)
e_shared_back = self.fc_es_back(e_shared_info)
output_es = self.fc_es_fore(e_shared_info)
output_es = self.leaky_relu(output_es)
# Hidden Concept Module
i_shared_info = x_hidden - e_shared_back
hidden = i_shared_info
i_stock_to_concept = self.cal_cos_similarity(i_shared_info, hidden)
dim = i_stock_to_concept.shape[0]
diag = i_stock_to_concept.diagonal(0)
i_stock_to_concept = i_stock_to_concept * (torch.ones(dim, dim) - torch.eye(dim)).to(device)
row = torch.linspace(0, dim - 1, dim).to(device).long()
column = i_stock_to_concept.max(1)[1].long()
value = i_stock_to_concept.max(1)[0]
i_stock_to_concept[row, column] = 10
i_stock_to_concept[i_stock_to_concept != 10] = 0
i_stock_to_concept[row, column] = value
i_stock_to_concept = i_stock_to_concept + torch.diag_embed((i_stock_to_concept.sum(0) != 0).float() * diag)
hidden = torch.t(i_shared_info).mm(i_stock_to_concept).t()
hidden = hidden[hidden.sum(1) != 0]
i_concept_to_stock = self.cal_cos_similarity(i_shared_info, hidden)
i_concept_to_stock = self.softmax_t2s(i_concept_to_stock)
i_shared_info = i_concept_to_stock.mm(hidden)
i_shared_info = self.fc_is(i_shared_info)
i_shared_back = self.fc_is_back(i_shared_info)
output_is = self.fc_is_fore(i_shared_info)
output_is = self.leaky_relu(output_is)
# Individual Information Module
individual_info = x_hidden - e_shared_back - i_shared_back
output_indi = individual_info
output_indi = self.fc_indi(output_indi)
output_indi = self.leaky_relu(output_indi)
# Stock Trend Prediction
all_info = output_es + output_is + output_indi
pred_all = self.fc_out(all_info).squeeze()
return pred_all

View File

@@ -0,0 +1,446 @@
# 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
from typing import Text, Union
import copy
from ...utils import get_or_create_path
from ...log import get_module_logger
import torch
import torch.nn as nn
import torch.optim as optim
from .pytorch_utils import count_parameters
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 IGMTF(Model):
"""IGMTF 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.0,
n_epochs=200,
lr=0.001,
metric="",
early_stop=20,
loss="mse",
base_model="GRU",
model_path=None,
optimizer="adam",
GPU=0,
seed=None,
**kwargs
):
# Set logger.
self.logger = get_module_logger("IGMTF")
self.logger.info("IMGTF 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.model_path = model_path
self.device = torch.device("cuda:%d" % (GPU) if torch.cuda.is_available() and GPU >= 0 else "cpu")
self.seed = seed
self.logger.info(
"IGMTF parameters setting:"
"\nd_feat : {}"
"\nhidden_size : {}"
"\nnum_layers : {}"
"\ndropout : {}"
"\nn_epochs : {}"
"\nlr : {}"
"\nmetric : {}"
"\nearly_stop : {}"
"\noptimizer : {}"
"\nloss_type : {}"
"\nbase_model : {}"
"\nmodel_path : {}"
"\nvisible_GPU : {}"
"\nuse_GPU : {}"
"\nseed : {}".format(
d_feat,
hidden_size,
num_layers,
dropout,
n_epochs,
lr,
metric,
early_stop,
optimizer.lower(),
loss,
base_model,
model_path,
GPU,
self.use_gpu,
seed,
)
)
if self.seed is not None:
np.random.seed(self.seed)
torch.manual_seed(self.seed)
self.igmtf_model = IGMTFModel(
d_feat=self.d_feat,
hidden_size=self.hidden_size,
num_layers=self.num_layers,
dropout=self.dropout,
base_model=self.base_model,
)
self.logger.info("model:\n{:}".format(self.igmtf_model))
self.logger.info("model size: {:.4f} MB".format(count_parameters(self.igmtf_model)))
if optimizer.lower() == "adam":
self.train_optimizer = optim.Adam(self.igmtf_model.parameters(), lr=self.lr)
elif optimizer.lower() == "gd":
self.train_optimizer = optim.SGD(self.igmtf_model.parameters(), lr=self.lr)
else:
raise NotImplementedError("optimizer {} is not supported!".format(optimizer))
self.fitted = False
self.igmtf_model.to(self.device)
@property
def use_gpu(self):
return self.device != torch.device("cpu")
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":
x = pred[mask]
y = label[mask]
vx = x - torch.mean(x)
vy = y - torch.mean(y)
return torch.sum(vx * vy) / (torch.sqrt(torch.sum(vx**2)) * torch.sqrt(torch.sum(vy**2)))
if 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 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 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 get_train_hidden(self, x_train):
x_train_values = x_train.values
daily_index, daily_count = self.get_daily_inter(x_train, shuffle=True)
self.igmtf_model.eval()
train_hidden = []
train_hidden_day = []
for idx, count in zip(daily_index, daily_count):
batch = slice(idx, idx + count)
feature = torch.from_numpy(x_train_values[batch]).float().to(self.device)
out = self.igmtf_model(feature, get_hidden=True)
train_hidden.append(out.detach().cpu())
train_hidden_day.append(out.detach().cpu().mean(dim=0).unsqueeze(dim=0))
train_hidden = np.asarray(train_hidden, dtype=object)
train_hidden_day = torch.cat(train_hidden_day)
return train_hidden, train_hidden_day
def train_epoch(self, x_train, y_train, train_hidden, train_hidden_day):
x_train_values = x_train.values
y_train_values = np.squeeze(y_train.values)
self.igmtf_model.train()
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().to(self.device)
label = torch.from_numpy(y_train_values[batch]).float().to(self.device)
pred = self.igmtf_model(feature, train_hidden=train_hidden, train_hidden_day=train_hidden_day)
loss = self.loss_fn(pred, label)
self.train_optimizer.zero_grad()
loss.backward()
torch.nn.utils.clip_grad_value_(self.igmtf_model.parameters(), 3.0)
self.train_optimizer.step()
def test_epoch(self, data_x, data_y, train_hidden, train_hidden_day):
# prepare training data
x_values = data_x.values
y_values = np.squeeze(data_y.values)
self.igmtf_model.eval()
scores = []
losses = []
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().to(self.device)
label = torch.from_numpy(y_values[batch]).float().to(self.device)
pred = self.igmtf_model(feature, train_hidden=train_hidden, train_hidden_day=train_hidden_day)
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(),
save_path=None,
):
df_train, df_valid = dataset.prepare(
["train", "valid"],
col_set=["feature", "label"],
data_key=DataHandlerLP.DK_L,
)
if df_train.empty or df_valid.empty:
raise ValueError("Empty data from dataset, please check your dataset config.")
x_train, y_train = df_train["feature"], df_train["label"]
x_valid, y_valid = df_valid["feature"], df_valid["label"]
save_path = get_or_create_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.base_model == "LSTM":
pretrained_model = LSTMModel()
elif self.base_model == "GRU":
pretrained_model = GRUModel()
else:
raise ValueError("unknown base model name `%s`" % self.base_model)
if self.model_path is not None:
self.logger.info("Loading pretrained model...")
pretrained_model.load_state_dict(torch.load(self.model_path, map_location=self.device))
model_dict = self.igmtf_model.state_dict()
pretrained_dict = {
k: v for k, v in pretrained_model.state_dict().items() if k in model_dict # pylint: disable=E1135
}
model_dict.update(pretrained_dict)
self.igmtf_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...")
train_hidden, train_hidden_day = self.get_train_hidden(x_train)
self.train_epoch(x_train, y_train, train_hidden, train_hidden_day)
self.logger.info("evaluating...")
train_loss, train_score = self.test_epoch(x_train, y_train, train_hidden, train_hidden_day)
val_loss, val_score = self.test_epoch(x_valid, y_valid, train_hidden, train_hidden_day)
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.igmtf_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.igmtf_model.load_state_dict(best_param)
torch.save(best_param, save_path)
if self.use_gpu:
torch.cuda.empty_cache()
def predict(self, dataset: DatasetH, segment: Union[Text, slice] = "test"):
if not self.fitted:
raise ValueError("model is not fitted yet!")
x_train = dataset.prepare("train", col_set="feature", data_key=DataHandlerLP.DK_L)
train_hidden, train_hidden_day = self.get_train_hidden(x_train)
x_test = dataset.prepare(segment, col_set="feature", data_key=DataHandlerLP.DK_I)
index = x_test.index
self.igmtf_model.eval()
x_values = x_test.values
preds = []
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().to(self.device)
with torch.no_grad():
pred = (
self.igmtf_model(x_batch, train_hidden=train_hidden, train_hidden_day=train_hidden_day)
.detach()
.cpu()
.numpy()
)
preds.append(pred)
return pd.Series(np.concatenate(preds), index=index)
class IGMTFModel(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.lins = nn.Sequential()
for i in range(2):
self.lins.add_module("linear" + str(i), nn.Linear(hidden_size, hidden_size))
self.lins.add_module("leakyrelu" + str(i), nn.LeakyReLU())
self.fc_output = nn.Linear(hidden_size * 2, hidden_size * 2)
self.project1 = nn.Linear(hidden_size, hidden_size, bias=False)
self.project2 = nn.Linear(hidden_size, hidden_size, bias=False)
self.fc_out_pred = nn.Linear(hidden_size * 2, 1)
self.leaky_relu = nn.LeakyReLU()
self.d_feat = d_feat
def cal_cos_similarity(self, x, y): # the 2nd dimension of x and y are the same
xy = x.mm(torch.t(y))
x_norm = torch.sqrt(torch.sum(x * x, dim=1)).reshape(-1, 1)
y_norm = torch.sqrt(torch.sum(y * y, dim=1)).reshape(-1, 1)
cos_similarity = xy / (x_norm.mm(torch.t(y_norm)) + 1e-6)
return cos_similarity
def sparse_dense_mul(self, s, d):
i = s._indices()
v = s._values()
dv = d[i[0, :], i[1, :]] # get values from relevant entries of dense matrix
return torch.sparse.FloatTensor(i, v * dv, s.size())
def forward(self, x, get_hidden=False, train_hidden=None, train_hidden_day=None, k_day=10, n_neighbor=10):
# x: [N, F*T]
device = x.device
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)
out = out[:, -1, :]
out = self.lins(out)
mini_batch_out = out
if get_hidden is True:
return mini_batch_out
mini_batch_out_day = torch.mean(mini_batch_out, dim=0).unsqueeze(0)
day_similarity = self.cal_cos_similarity(mini_batch_out_day, train_hidden_day.to(device))
day_index = torch.topk(day_similarity, k_day, dim=1)[1]
sample_train_hidden = train_hidden[day_index.long().cpu()].squeeze()
sample_train_hidden = torch.cat(list(sample_train_hidden)).to(device)
sample_train_hidden = self.lins(sample_train_hidden)
cos_similarity = self.cal_cos_similarity(self.project1(mini_batch_out), self.project2(sample_train_hidden))
row = (
torch.linspace(0, x.shape[0] - 1, x.shape[0])
.reshape([-1, 1])
.repeat(1, n_neighbor)
.reshape(1, -1)
.to(device)
)
column = torch.topk(cos_similarity, n_neighbor, dim=1)[1].reshape(1, -1)
mask = torch.sparse_coo_tensor(
torch.cat([row, column]),
torch.ones([row.shape[1]]).to(device) / n_neighbor,
(x.shape[0], sample_train_hidden.shape[0]),
)
cos_similarity = self.sparse_dense_mul(mask, cos_similarity)
agg_out = torch.sparse.mm(cos_similarity, self.project2(sample_train_hidden))
# out = self.fc_out(out).squeeze()
out = self.fc_out_pred(torch.cat([mini_batch_out, agg_out], axis=1)).squeeze()
return out

View File

@@ -84,7 +84,7 @@ class SFM_Model(nn.Module):
if len(self.states) == 0: # hasn't initialized yet if len(self.states) == 0: # hasn't initialized yet
self.init_states(x) self.init_states(x)
self.get_constants(x) self.get_constants(x)
p_tm1 = self.states[0] p_tm1 = self.states[0] # noqa: F841
h_tm1 = self.states[1] h_tm1 = self.states[1]
S_re_tm1 = self.states[2] S_re_tm1 = self.states[2]
S_im_tm1 = self.states[3] S_im_tm1 = self.states[3]

View File

@@ -477,10 +477,10 @@ class TabNet(nn.Module):
sparse_loss = [] sparse_loss = []
out = torch.zeros(x.size(0), self.n_d).to(x.device) out = torch.zeros(x.size(0), self.n_d).to(x.device)
for step in self.steps: for step in self.steps:
x_te, l = step(x, x_a, priors) x_te, loss = step(x, x_a, priors)
out += F.relu(x_te[:, : self.n_d]) # split the feature from feat_transformer out += F.relu(x_te[:, : self.n_d]) # split the feature from feat_transformer
x_a = x_te[:, self.n_d :] x_a = x_te[:, self.n_d :]
sparse_loss.append(l) sparse_loss.append(loss)
return self.fc(out), sum(sparse_loss) return self.fc(out), sum(sparse_loss)

View File

@@ -145,7 +145,7 @@ class TCTS(Model):
init_fore_model = copy.deepcopy(self.fore_model) init_fore_model = copy.deepcopy(self.fore_model)
for p in init_fore_model.parameters(): for p in init_fore_model.parameters():
p.init_fore_model = False p.requires_grad = False
self.fore_model.train() self.fore_model.train()
self.weight_model.train() self.weight_model.train()

View File

@@ -1,4 +1,5 @@
# pylint: skip-file # pylint: skip-file
# flake8: noqa
''' '''
TODO: TODO:

View File

@@ -2,6 +2,7 @@
# Licensed under the MIT License. # Licensed under the MIT License.
# pylint: skip-file # pylint: skip-file
# flake8: noqa
import yaml import yaml
import pathlib import pathlib

View File

@@ -2,6 +2,7 @@
# Licensed under the MIT License. # Licensed under the MIT License.
# pylint: skip-file # pylint: skip-file
# flake8: noqa
import random import random
import pandas as pd import pandas as pd

View File

@@ -2,6 +2,7 @@
# Licensed under the MIT License. # Licensed under the MIT License.
# pylint: skip-file # pylint: skip-file
# flake8: noqa
import fire import fire
import pandas as pd import pandas as pd

View File

@@ -2,6 +2,7 @@
# Licensed under the MIT License. # Licensed under the MIT License.
# pylint: skip-file # pylint: skip-file
# flake8: noqa
import logging import logging

View File

@@ -2,6 +2,7 @@
# Licensed under the MIT License. # Licensed under the MIT License.
# pylint: skip-file # pylint: skip-file
# flake8: noqa
import pathlib import pathlib
import pickle import pickle

View File

@@ -1,11 +1,12 @@
# Copyright (c) Microsoft Corporation. # Copyright (c) Microsoft Corporation.
# Licensed under the MIT License. # Licensed under the MIT License.
import numpy as np import numpy as np
import pandas as pd
from datetime import datetime from datetime import datetime
from qlib.data.cache import H from qlib.data.cache import H
from qlib.data.data import Cal from qlib.data.data import Cal
from qlib.data.ops import ElemOperator from qlib.data.ops import ElemOperator, PairOperator
from qlib.utils.time import time_to_day_index from qlib.utils.time import time_to_day_index
@@ -35,6 +36,17 @@ def get_calendar_day(freq="1min", future=False):
return _calendar return _calendar
def get_calendar_minute(freq="day", future=False):
"""Load High-Freq Calendar Minute Using Memcache"""
flag = f"{freq}_future_{future}_day"
if flag in H["c"]:
_calendar = H["c"][flag]
else:
_calendar = np.array(list(map(lambda x: x.minute // 30, Cal.load_calendar(freq, future))))
H["c"][flag] = _calendar
return _calendar
class DayCumsum(ElemOperator): class DayCumsum(ElemOperator):
"""DayCumsum Operator during start time and end time. """DayCumsum Operator during start time and end time.
@@ -83,3 +95,181 @@ class DayCumsum(ElemOperator):
_calendar = get_calendar_day(freq=freq) _calendar = get_calendar_day(freq=freq)
series = self.feature.load(instrument, start_index, end_index, freq) series = self.feature.load(instrument, start_index, end_index, freq)
return series.groupby(_calendar[series.index]).transform(self.period_cusum) return series.groupby(_calendar[series.index]).transform(self.period_cusum)
class DayLast(ElemOperator):
"""DayLast Operator
Parameters
----------
feature : Expression
feature instance
Returns
----------
feature:
a series of that each value equals the last value of its day
"""
def _load_internal(self, instrument, start_index, end_index, freq):
_calendar = get_calendar_day(freq=freq)
series = self.feature.load(instrument, start_index, end_index, freq)
return series.groupby(_calendar[series.index]).transform("last")
class FFillNan(ElemOperator):
"""FFillNan Operator
Parameters
----------
feature : Expression
feature instance
Returns
----------
feature:
a forward fill nan feature
"""
def _load_internal(self, instrument, start_index, end_index, freq):
series = self.feature.load(instrument, start_index, end_index, freq)
return series.fillna(method="ffill")
class BFillNan(ElemOperator):
"""BFillNan Operator
Parameters
----------
feature : Expression
feature instance
Returns
----------
feature:
a backfoward fill nan feature
"""
def _load_internal(self, instrument, start_index, end_index, freq):
series = self.feature.load(instrument, start_index, end_index, freq)
return series.fillna(method="bfill")
class Date(ElemOperator):
"""Date Operator
Parameters
----------
feature : Expression
feature instance
Returns
----------
feature:
a series of that each value is the date corresponding to feature.index
"""
def _load_internal(self, instrument, start_index, end_index, freq):
_calendar = get_calendar_day(freq=freq)
series = self.feature.load(instrument, start_index, end_index, freq)
return pd.Series(_calendar[series.index], index=series.index)
class Select(PairOperator):
"""Select Operator
Parameters
----------
feature_left : Expression
feature instance, select condition
feature_right : Expression
feature instance, select value
Returns
----------
feature:
value(feature_right) that meets the condition(feature_left)
"""
def _load_internal(self, instrument, start_index, end_index, freq):
series_condition = self.feature_left.load(instrument, start_index, end_index, freq)
series_feature = self.feature_right.load(instrument, start_index, end_index, freq)
return series_feature.loc[series_condition]
class IsNull(ElemOperator):
"""IsNull Operator
Parameters
----------
feature : Expression
feature instance
Returns
----------
feature:
A series indicating whether the feature is nan
"""
def _load_internal(self, instrument, start_index, end_index, freq):
series = self.feature.load(instrument, start_index, end_index, freq)
return series.isnull()
class IsInf(ElemOperator):
"""IsInf Operator
Parameters
----------
feature : Expression
feature instance
Returns
----------
feature:
A series indicating whether the feature is inf
"""
def _load_internal(self, instrument, start_index, end_index, freq):
series = self.feature.load(instrument, start_index, end_index, freq)
return np.isinf(series)
class Cut(ElemOperator):
"""Cut Operator
Parameters
----------
feature : Expression
feature instance
l : int
l > 0, delete the first l elements of feature (default is None, which means 0)
r : int
r < 0, delete the last -r elements of feature (default is None, which means 0)
Returns
----------
feature:
A series with the first l and last -r elements deleted from the feature.
Note: It is deleted from the raw data, not the sliced data
"""
def __init__(self, feature, left=None, right=None):
self.left = left
self.right = right
if (self.left is not None and self.left <= 0) or (self.right is not None and self.right >= 0):
raise ValueError("Cut operator l shoud > 0 and r should < 0")
super(Cut, self).__init__(feature)
def _load_internal(self, instrument, start_index, end_index, freq):
series = self.feature.load(instrument, start_index, end_index, freq)
return series.iloc[self.left : self.right]
def get_extended_window_size(self):
ll = 0 if self.left is None else self.left
rr = 0 if self.right is None else abs(self.right)
lft_etd, rght_etd = self.feature.get_extended_window_size()
lft_etd = lft_etd + ll
rght_etd = rght_etd + rr
return lft_etd, rght_etd

View File

@@ -2,3 +2,6 @@
# Licensed under the MIT License. # Licensed under the MIT License.
from .analysis_model_performance import model_performance_graph from .analysis_model_performance import model_performance_graph
__all__ = ["model_performance_graph"]

View File

@@ -6,3 +6,6 @@ from .score_ic import score_ic_graph
from .report import report_graph from .report import report_graph
from .rank_label import rank_label_graph from .rank_label import rank_label_graph
from .risk_analysis import risk_analysis_graph from .risk_analysis import risk_analysis_graph
__all__ = ["cumulative_return_graph", "score_ic_graph", "report_graph", "rank_label_graph", "risk_analysis_graph"]

View File

@@ -68,9 +68,9 @@ def parse_position(position: dict = None) -> pd.DataFrame:
if not _trading_day_sell_df.empty: if not _trading_day_sell_df.empty:
_trading_day_sell_df["status"] = -1 _trading_day_sell_df["status"] = -1
_trading_day_sell_df["date"] = _trading_date _trading_day_sell_df["date"] = _trading_date
_trading_day_df = _trading_day_df.append(_trading_day_sell_df, sort=False) _trading_day_df = pd.concat([_trading_day_df, _trading_day_sell_df], sort=False)
result_df = result_df.append(_trading_day_df, sort=True) result_df = pd.concat([result_df, _trading_day_df], sort=True)
previous_data = dict( previous_data = dict(
date=_trading_date, date=_trading_date,

View File

@@ -85,7 +85,7 @@ def _get_monthly_risk_analysis_with_report(report_normal_df: pd.DataFrame) -> pd
# _m_report_long_short, # _m_report_long_short,
pd.Timestamp(year=gp_m[0], month=gp_m[1], day=month_days), pd.Timestamp(year=gp_m[0], month=gp_m[1], day=month_days),
) )
_monthly_df = _monthly_df.append(_temp_df, sort=False) _monthly_df = pd.concat([_monthly_df, _temp_df], sort=False)
return _monthly_df return _monthly_df

View File

@@ -15,3 +15,14 @@ from .rule_strategy import (
) )
from .cost_control import SoftTopkStrategy from .cost_control import SoftTopkStrategy
__all__ = [
"TopkDropoutStrategy",
"WeightStrategyBase",
"EnhancedIndexingStrategy",
"TWAPStrategy",
"SBBStrategyBase",
"SBBStrategyEMA",
"SoftTopkStrategy",
]

View File

@@ -4,3 +4,6 @@
from .base import BaseOptimizer from .base import BaseOptimizer
from .optimizer import PortfolioOptimizer from .optimizer import PortfolioOptimizer
from .enhanced_indexing import EnhancedIndexingOptimizer from .enhanced_indexing import EnhancedIndexingOptimizer
__all__ = ["BaseOptimizer", "PortfolioOptimizer", "EnhancedIndexingOptimizer"]

View File

@@ -131,10 +131,10 @@ class TopkDropoutStrategy(BaseSignalStrategy):
if self.only_tradable: if self.only_tradable:
# If The strategy only consider tradable stock when make decision # If The strategy only consider tradable stock when make decision
# It needs following actions to filter stocks # It needs following actions to filter stocks
def get_first_n(l, n, reverse=False): def get_first_n(li, n, reverse=False):
cur_n = 0 cur_n = 0
res = [] res = []
for si in reversed(l) if reverse else l: for si in reversed(li) if reverse else li:
if self.trade_exchange.is_stock_tradable( if self.trade_exchange.is_stock_tradable(
stock_id=si, start_time=trade_start_time, end_time=trade_end_time stock_id=si, start_time=trade_start_time, end_time=trade_end_time
): ):
@@ -144,13 +144,13 @@ class TopkDropoutStrategy(BaseSignalStrategy):
break break
return res[::-1] if reverse else res return res[::-1] if reverse else res
def get_last_n(l, n): def get_last_n(li, n):
return get_first_n(l, n, reverse=True) return get_first_n(li, n, reverse=True)
def filter_stock(l): def filter_stock(li):
return [ return [
si si
for si in l for si in li
if self.trade_exchange.is_stock_tradable( if self.trade_exchange.is_stock_tradable(
stock_id=si, start_time=trade_start_time, end_time=trade_end_time stock_id=si, start_time=trade_start_time, end_time=trade_end_time
) )
@@ -158,14 +158,14 @@ class TopkDropoutStrategy(BaseSignalStrategy):
else: else:
# Otherwise, the stock will make decision with out the stock tradable info # Otherwise, the stock will make decision with out the stock tradable info
def get_first_n(l, n): def get_first_n(li, n):
return list(l)[:n] return list(li)[:n]
def get_last_n(l, n): def get_last_n(li, n):
return list(l)[-n:] return list(li)[-n:]
def filter_stock(l): def filter_stock(li):
return l return li
current_temp = copy.deepcopy(self.trade_position) current_temp = copy.deepcopy(self.trade_position)
# generate order list for this adjust date # generate order list for this adjust date
@@ -203,7 +203,7 @@ class TopkDropoutStrategy(BaseSignalStrategy):
candi = filter_stock(last) candi = filter_stock(last)
try: try:
sell = pd.Index(np.random.choice(candi, self.n_drop, replace=False) if len(last) else []) sell = pd.Index(np.random.choice(candi, self.n_drop, replace=False) if len(last) else [])
except ValueError: # No enough candidates except ValueError: # No enough candidates
sell = candi sell = candi
else: else:
raise NotImplementedError(f"This type of input is not supported") raise NotImplementedError(f"This type of input is not supported")

View File

@@ -1 +1,2 @@
# pylint: skip-file # pylint: skip-file
# flake8: noqa

View File

@@ -2,6 +2,7 @@
# Licensed under the MIT License. # Licensed under the MIT License.
# pylint: skip-file # pylint: skip-file
# flake8: noqa
import yaml import yaml
import copy import copy

View File

@@ -2,6 +2,7 @@
# Licensed under the MIT License. # Licensed under the MIT License.
# pylint: skip-file # pylint: skip-file
# flake8: noqa
# coding=utf-8 # coding=utf-8

View File

@@ -2,6 +2,7 @@
# Licensed under the MIT License. # Licensed under the MIT License.
# pylint: skip-file # pylint: skip-file
# flake8: noqa
import os import os
import json import json

View File

@@ -2,6 +2,7 @@
# Licensed under the MIT License. # Licensed under the MIT License.
# pylint: skip-file # pylint: skip-file
# flake8: noqa
from hyperopt import hp from hyperopt import hp

View File

@@ -2,6 +2,7 @@
# Licensed under the MIT License. # Licensed under the MIT License.
# pylint: skip-file # pylint: skip-file
# flake8: noqa
import os import os
import yaml import yaml

View File

@@ -2,3 +2,6 @@
# Licensed under the MIT License. # Licensed under the MIT License.
from .record_temp import MultiSegRecord from .record_temp import MultiSegRecord
from .record_temp import SignalMseRecord from .record_temp import SignalMseRecord
__all__ = ["MultiSegRecord", "SignalMseRecord"]

View File

@@ -15,6 +15,7 @@ from .data import (
LocalCalendarProvider, LocalCalendarProvider,
LocalInstrumentProvider, LocalInstrumentProvider,
LocalFeatureProvider, LocalFeatureProvider,
LocalPITProvider,
LocalExpressionProvider, LocalExpressionProvider,
LocalDatasetProvider, LocalDatasetProvider,
ClientCalendarProvider, ClientCalendarProvider,
@@ -34,3 +35,32 @@ from .cache import (
DatasetURICache, DatasetURICache,
MemoryCalendarCache, MemoryCalendarCache,
) )
__all__ = [
"D",
"CalendarProvider",
"InstrumentProvider",
"FeatureProvider",
"ExpressionProvider",
"DatasetProvider",
"LocalCalendarProvider",
"LocalInstrumentProvider",
"LocalFeatureProvider",
"LocalPITProvider",
"LocalExpressionProvider",
"LocalDatasetProvider",
"ClientCalendarProvider",
"ClientInstrumentProvider",
"ClientDatasetProvider",
"BaseProvider",
"LocalProvider",
"ClientProvider",
"ExpressionCache",
"DatasetCache",
"DiskExpressionCache",
"DiskDatasetCache",
"SimpleDatasetCache",
"DatasetURICache",
"MemoryCalendarCache",
]

View File

@@ -6,12 +6,20 @@ from __future__ import division
from __future__ import print_function from __future__ import print_function
import abc import abc
import pandas as pd
from ..log import get_module_logger from ..log import get_module_logger
class Expression(abc.ABC): class Expression(abc.ABC):
"""Expression base class""" """
Expression base class
Expression is designed to handle the calculation of data with the format below
data with two dimension for each instrument,
- feature
- time: it could be observation time or period time.
- period time is designed for Point-in-time database. For example, the period time maybe 2014Q4, its value can observed for multiple times(different value may be observed at different time due to amendment).
"""
def __str__(self): def __str__(self):
return type(self).__name__ return type(self).__name__
@@ -104,6 +112,11 @@ class Expression(abc.ABC):
return Power(self, other) return Power(self, other)
def __rpow__(self, other):
from .ops import Power # pylint: disable=C0415
return Power(other, self)
def __and__(self, other): def __and__(self, other):
from .ops import And # pylint: disable=C0415 from .ops import And # pylint: disable=C0415
@@ -124,8 +137,18 @@ class Expression(abc.ABC):
return Or(other, self) return Or(other, self)
def load(self, instrument, start_index, end_index, freq): def load(self, instrument, start_index, end_index, *args):
"""load feature """load feature
This function is responsible for loading feature/expression based on the expression engine.
The concrete implementation will be separated into two parts:
1) caching data, handle errors.
- This part is shared by all the expressions and implemented in Expression
2) processing and calculating data based on the specific expression.
- This part is different in each expression and implemented in each expression
Expression Engine is shared by different data.
Different data will have different extra information for `args`.
Parameters Parameters
---------- ----------
@@ -135,8 +158,18 @@ class Expression(abc.ABC):
feature start index [in calendar]. feature start index [in calendar].
end_index : str end_index : str
feature end index [in calendar]. feature end index [in calendar].
freq : str
feature frequency. *args may contain following information:
1) if it is used in basic expression engine data, it contains following arguments
freq: str
feature frequency.
2) if is used in PIT data, it contains following arguments
cur_pit:
it is designed for the point-in-time data.
period: int
This is used for query specific period.
The period is represented with int in Qlib. (e.g. 202001 may represent the first quarter in 2020)
Returns Returns
---------- ----------
@@ -146,26 +179,26 @@ class Expression(abc.ABC):
from .cache import H # pylint: disable=C0415 from .cache import H # pylint: disable=C0415
# cache # cache
args = str(self), instrument, start_index, end_index, freq cache_key = str(self), instrument, start_index, end_index, *args
if args in H["f"]: if cache_key in H["f"]:
return H["f"][args] return H["f"][cache_key]
if start_index is not None and end_index is not None and start_index > end_index: if start_index is not None and end_index is not None and start_index > end_index:
raise ValueError("Invalid index range: {} {}".format(start_index, end_index)) raise ValueError("Invalid index range: {} {}".format(start_index, end_index))
try: try:
series = self._load_internal(instrument, start_index, end_index, freq) series = self._load_internal(instrument, start_index, end_index, *args)
except Exception as e: except Exception as e:
get_module_logger("data").debug( get_module_logger("data").debug(
f"Loading data error: instrument={instrument}, expression={str(self)}, " f"Loading data error: instrument={instrument}, expression={str(self)}, "
f"start_index={start_index}, end_index={end_index}, freq={freq}. " f"start_index={start_index}, end_index={end_index}, args={args}. "
f"error info: {str(e)}" f"error info: {str(e)}"
) )
raise raise
series.name = str(self) series.name = str(self)
H["f"][args] = series H["f"][cache_key] = series
return series return series
@abc.abstractmethod @abc.abstractmethod
def _load_internal(self, instrument, start_index, end_index, freq): def _load_internal(self, instrument, start_index, end_index, *args) -> pd.Series:
raise NotImplementedError("This function must be implemented in your newly defined feature") raise NotImplementedError("This function must be implemented in your newly defined feature")
@abc.abstractmethod @abc.abstractmethod
@@ -225,6 +258,16 @@ class Feature(Expression):
return 0, 0 return 0, 0
class PFeature(Feature):
def __str__(self):
return "$$" + self._name
def _load_internal(self, instrument, start_index, end_index, cur_time, period=None):
from .data import PITD # pylint: disable=C0415
return PITD.period_feature(instrument, str(self), start_index, end_index, cur_time, period)
class ExpressionOps(Expression): class ExpressionOps(Expression):
"""Operator Expression """Operator Expression

View File

@@ -33,7 +33,7 @@ from ..utils import (
from ..log import get_module_logger from ..log import get_module_logger
from .base import Feature from .base import Feature
from .ops import Operators # pylint: disable=W0611 from .ops import Operators # pylint: disable=W0611 # noqa: F401
class QlibCacheException(RuntimeError): class QlibCacheException(RuntimeError):
@@ -528,7 +528,7 @@ class DiskExpressionCache(ExpressionCache):
CacheUtils.visit(cache_path) CacheUtils.visit(cache_path)
series = read_bin(cache_path, start_index, end_index) series = read_bin(cache_path, start_index, end_index)
return series return series
except Exception as e: except Exception:
series = None series = None
self.logger.error("reading %s file error : %s" % (cache_path, traceback.format_exc())) self.logger.error("reading %s file error : %s" % (cache_path, traceback.format_exc()))
return series return series
@@ -1068,7 +1068,7 @@ class SimpleDatasetCache(DatasetCache):
super(SimpleDatasetCache, self).__init__(provider) super(SimpleDatasetCache, self).__init__(provider)
try: try:
self.local_cache_path: Path = Path(C["local_cache_path"]).expanduser().resolve() self.local_cache_path: Path = Path(C["local_cache_path"]).expanduser().resolve()
except (KeyError, TypeError) as e: except (KeyError, TypeError):
self.logger.error("Assign a local_cache_path in config if you want to use this cache mechanism") self.logger.error("Assign a local_cache_path in config if you want to use this cache mechanism")
raise raise
self.logger.info( self.logger.info(

View File

@@ -12,7 +12,7 @@ import queue
import bisect import bisect
import numpy as np import numpy as np
import pandas as pd import pandas as pd
from typing import List, Union from typing import List, Union, Optional
# For supporting multiprocessing in outer code, joblib is used # For supporting multiprocessing in outer code, joblib is used
from joblib import delayed from joblib import delayed
@@ -34,9 +34,11 @@ from ..utils import (
code_to_fname, code_to_fname,
set_log_with_config, set_log_with_config,
time_to_slc_point, time_to_slc_point,
read_period_data,
get_period_list,
) )
from ..utils.paral import ParallelExt from ..utils.paral import ParallelExt
from .ops import Operators # pylint: disable=W0611 from .ops import Operators # pylint: disable=W0611 # noqa: F401
class ProviderBackendMixin: class ProviderBackendMixin:
@@ -331,6 +333,51 @@ class FeatureProvider(abc.ABC):
raise NotImplementedError("Subclass of FeatureProvider must implement `feature` method") raise NotImplementedError("Subclass of FeatureProvider must implement `feature` method")
class PITProvider(abc.ABC):
@abc.abstractmethod
def period_feature(
self,
instrument,
field,
start_index: int,
end_index: int,
cur_time: pd.Timestamp,
period: Optional[int] = None,
) -> pd.Series:
"""
get the historical periods data series between `start_index` and `end_index`
Parameters
----------
start_index: int
start_index is a relative index to the latest period to cur_time
end_index: int
end_index is a relative index to the latest period to cur_time
in most cases, the start_index and end_index will be a non-positive values
For example, start_index == -3 end_index == 0 and current period index is cur_idx,
then the data between [start_index + cur_idx, end_index + cur_idx] will be retrieved.
period: int
This is used for query specific period.
The period is represented with int in Qlib. (e.g. 202001 may represent the first quarter in 2020)
NOTE: `period` will override `start_index` and `end_index`
Returns
-------
pd.Series
The index will be integers to indicate the periods of the data
An typical examples will be
TODO
Raises
------
FileNotFoundError
This exception will be raised if the queried data do not exist.
"""
raise NotImplementedError(f"Please implement the `period_feature` method")
class ExpressionProvider(abc.ABC): class ExpressionProvider(abc.ABC):
"""Expression provider class """Expression provider class
@@ -583,7 +630,7 @@ class DatasetProvider(abc.ABC):
for _processor in inst_processors: for _processor in inst_processors:
if _processor: if _processor:
_processor_obj = init_instance_by_config(_processor, accept_types=InstProcessor) _processor_obj = init_instance_by_config(_processor, accept_types=InstProcessor)
data = _processor_obj(data) data = _processor_obj(data, instrument=inst)
return data return data
@@ -694,6 +741,95 @@ class LocalFeatureProvider(FeatureProvider, ProviderBackendMixin):
return self.backend_obj(instrument=instrument, field=field, freq=freq)[start_index : end_index + 1] return self.backend_obj(instrument=instrument, field=field, freq=freq)[start_index : end_index + 1]
class LocalPITProvider(PITProvider):
# TODO: Add PIT backend file storage
# NOTE: This class is not multi-threading-safe!!!!
def period_feature(self, instrument, field, start_index, end_index, cur_time, period=None):
if not isinstance(cur_time, pd.Timestamp):
raise ValueError(
f"Expected pd.Timestamp for `cur_time`, got '{cur_time}'. Advices: you can't query PIT data directly(e.g. '$$roewa_q'), you must use `P` operator to convert data to each day (e.g. 'P($$roewa_q)')"
)
assert end_index <= 0 # PIT don't support querying future data
DATA_RECORDS = [
("date", C.pit_record_type["date"]),
("period", C.pit_record_type["period"]),
("value", C.pit_record_type["value"]),
("_next", C.pit_record_type["index"]),
]
VALUE_DTYPE = C.pit_record_type["value"]
field = str(field).lower()[2:]
instrument = code_to_fname(instrument)
# {For acceleration
# start_index, end_index, cur_index = kwargs["info"]
# if cur_index == start_index:
# if not hasattr(self, "all_fields"):
# self.all_fields = []
# self.all_fields.append(field)
# if not hasattr(self, "period_index"):
# self.period_index = {}
# if field not in self.period_index:
# self.period_index[field] = {}
# For acceleration}
if not field.endswith("_q") and not field.endswith("_a"):
raise ValueError("period field must ends with '_q' or '_a'")
quarterly = field.endswith("_q")
index_path = C.dpm.get_data_uri() / "financial" / instrument.lower() / f"{field}.index"
data_path = C.dpm.get_data_uri() / "financial" / instrument.lower() / f"{field}.data"
if not (index_path.exists() and data_path.exists()):
raise FileNotFoundError("No file is found. Raise exception and ")
# NOTE: The most significant performance loss is here.
# Does the acceleration that makes the program complicated really matters?
# - It makes parameters of the interface complicate
# - It does not performance in the optimal way (places all the pieces together, we may achieve higher performance)
# - If we design it carefully, we can go through for only once to get the historical evolution of the data.
# So I decide to deprecated previous implementation and keep the logic of the program simple
# Instead, I'll add a cache for the index file.
data = np.fromfile(data_path, dtype=DATA_RECORDS)
# find all revision periods before `cur_time`
cur_time_int = int(cur_time.year) * 10000 + int(cur_time.month) * 100 + int(cur_time.day)
loc = np.searchsorted(data["date"], cur_time_int, side="right")
if loc <= 0:
return pd.Series()
last_period = data["period"][:loc].max() # return the latest quarter
first_period = data["period"][:loc].min()
period_list = get_period_list(first_period, last_period, quarterly)
if period is not None:
# NOTE: `period` has higher priority than `start_index` & `end_index`
if period not in period_list:
return pd.Series()
else:
period_list = [period]
else:
period_list = period_list[max(0, len(period_list) + start_index - 1) : len(period_list) + end_index]
value = np.full((len(period_list),), np.nan, dtype=VALUE_DTYPE)
for i, p in enumerate(period_list):
# last_period_index = self.period_index[field].get(period) # For acceleration
value[i], now_period_index = read_period_data(
index_path, data_path, p, cur_time_int, quarterly # , last_period_index # For acceleration
)
# self.period_index[field].update({period: now_period_index}) # For acceleration
# NOTE: the index is period_list; So it may result in unexpected values(e.g. nan)
# when calculation between different features and only part of its financial indicator is published
series = pd.Series(value, index=period_list, dtype=VALUE_DTYPE)
# {For acceleration
# if cur_index == end_index:
# self.all_fields.remove(field)
# if not len(self.all_fields):
# del self.all_fields
# del self.period_index
# For acceleration}
return series
class LocalExpressionProvider(ExpressionProvider): class LocalExpressionProvider(ExpressionProvider):
"""Local expression data provider class """Local expression data provider class
@@ -1003,6 +1139,8 @@ class ClientDatasetProvider(DatasetProvider):
class BaseProvider: class BaseProvider:
"""Local provider class """Local provider class
It is a set of interface that allow users to access data.
Because PITD is not exposed publicly to users, so it is not included in the interface.
To keep compatible with old qlib provider. To keep compatible with old qlib provider.
""" """
@@ -1126,6 +1264,7 @@ if sys.version_info >= (3, 9):
CalendarProviderWrapper = Annotated[CalendarProvider, Wrapper] CalendarProviderWrapper = Annotated[CalendarProvider, Wrapper]
InstrumentProviderWrapper = Annotated[InstrumentProvider, Wrapper] InstrumentProviderWrapper = Annotated[InstrumentProvider, Wrapper]
FeatureProviderWrapper = Annotated[FeatureProvider, Wrapper] FeatureProviderWrapper = Annotated[FeatureProvider, Wrapper]
PITProviderWrapper = Annotated[PITProvider, Wrapper]
ExpressionProviderWrapper = Annotated[ExpressionProvider, Wrapper] ExpressionProviderWrapper = Annotated[ExpressionProvider, Wrapper]
DatasetProviderWrapper = Annotated[DatasetProvider, Wrapper] DatasetProviderWrapper = Annotated[DatasetProvider, Wrapper]
BaseProviderWrapper = Annotated[BaseProvider, Wrapper] BaseProviderWrapper = Annotated[BaseProvider, Wrapper]
@@ -1133,6 +1272,7 @@ else:
CalendarProviderWrapper = CalendarProvider CalendarProviderWrapper = CalendarProvider
InstrumentProviderWrapper = InstrumentProvider InstrumentProviderWrapper = InstrumentProvider
FeatureProviderWrapper = FeatureProvider FeatureProviderWrapper = FeatureProvider
PITProviderWrapper = PITProvider
ExpressionProviderWrapper = ExpressionProvider ExpressionProviderWrapper = ExpressionProvider
DatasetProviderWrapper = DatasetProvider DatasetProviderWrapper = DatasetProvider
BaseProviderWrapper = BaseProvider BaseProviderWrapper = BaseProvider
@@ -1140,6 +1280,7 @@ else:
Cal: CalendarProviderWrapper = Wrapper() Cal: CalendarProviderWrapper = Wrapper()
Inst: InstrumentProviderWrapper = Wrapper() Inst: InstrumentProviderWrapper = Wrapper()
FeatureD: FeatureProviderWrapper = Wrapper() FeatureD: FeatureProviderWrapper = Wrapper()
PITD: PITProviderWrapper = Wrapper()
ExpressionD: ExpressionProviderWrapper = Wrapper() ExpressionD: ExpressionProviderWrapper = Wrapper()
DatasetD: DatasetProviderWrapper = Wrapper() DatasetD: DatasetProviderWrapper = Wrapper()
D: BaseProviderWrapper = Wrapper() D: BaseProviderWrapper = Wrapper()
@@ -1165,6 +1306,11 @@ def register_all_wrappers(C):
register_wrapper(FeatureD, feature_provider, "qlib.data") register_wrapper(FeatureD, feature_provider, "qlib.data")
logger.debug(f"registering FeatureD {C.feature_provider}") logger.debug(f"registering FeatureD {C.feature_provider}")
if getattr(C, "pit_provider", None) is not None:
pit_provider = init_instance_by_config(C.pit_provider, module)
register_wrapper(PITD, pit_provider, "qlib.data")
logger.debug(f"registering PITD {C.pit_provider}")
if getattr(C, "expression_provider", None) is not None: if getattr(C, "expression_provider", None) is not None:
# This provider is unnecessary in client provider # This provider is unnecessary in client provider
_eprovider = init_instance_by_config(C.expression_provider, module) _eprovider = init_instance_by_config(C.expression_provider, module)

View File

@@ -171,6 +171,7 @@ class DatasetH(Dataset):
Parameters Parameters
---------- ----------
slc : please refer to the docs of `prepare` slc : please refer to the docs of `prepare`
NOTE: it may not be an instance of slice. It may be a segment of `segments` from `def prepare`
""" """
if hasattr(self, "fetch_kwargs"): if hasattr(self, "fetch_kwargs"):
return self.handler.fetch(slc, **kwargs, **self.fetch_kwargs) return self.handler.fetch(slc, **kwargs, **self.fetch_kwargs)
@@ -199,6 +200,9 @@ class DatasetH(Dataset):
col_set : str col_set : str
The col_set will be passed to self.handler when fetching data. The col_set will be passed to self.handler when fetching data.
TODO: make it automatic:
- select DK_I for test data
- select DK_L for training data.
data_key : str data_key : str
The data to fetch: DK_* 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**.
@@ -346,7 +350,7 @@ class TSDataSampler:
flt_data = flt_data.reindex(self.data_index).fillna(False).astype(np.bool) flt_data = flt_data.reindex(self.data_index).fillna(False).astype(np.bool)
self.flt_data = flt_data.values self.flt_data = flt_data.values
self.idx_map = self.flt_idx_map(self.flt_data, self.idx_map) self.idx_map = self.flt_idx_map(self.flt_data, self.idx_map)
self.data_index = self.data_index[np.where(self.flt_data is True)[0]] self.data_index = self.data_index[np.where(self.flt_data)[0]]
self.idx_map = self.idx_map2arr(self.idx_map) self.idx_map = self.idx_map2arr(self.idx_map)
self.start_idx, self.end_idx = self.data_index.slice_locs( self.start_idx, self.end_idx = self.data_index.slice_locs(
@@ -609,3 +613,6 @@ class TSDatasetH(DatasetH):
tsds = TSDataSampler(data=data, start=start, end=end, step_len=self.step_len, dtype=dtype, flt_data=flt_data) tsds = TSDataSampler(data=data, start=start, end=end, step_len=self.step_len, dtype=dtype, flt_data=flt_data)
return tsds return tsds
__all__ = ["Optional"]

View File

@@ -515,7 +515,7 @@ class DataHandlerLP(DataHandler):
# data for learning # data for learning
# 1) assign # 1) assign
if self.process_type == DataHandlerLP.PTYPE_I: if self.process_type == DataHandlerLP.PTYPE_I:
_learn_df = self._data _learn_df = _shared_df
elif self.process_type == DataHandlerLP.PTYPE_A: elif self.process_type == DataHandlerLP.PTYPE_A:
# based on `infer_df` and append the processor # based on `infer_df` and append the processor
_learn_df = _infer_df _learn_df = _infer_df

View File

@@ -187,7 +187,13 @@ class Fillna(Processor):
df.fillna(self.fill_value, inplace=True) df.fillna(self.fill_value, inplace=True)
else: else:
cols = get_group_columns(df, self.fields_group) cols = get_group_columns(df, self.fields_group)
df.fillna({col: self.fill_value for col in cols}, inplace=True) # this implementation is extremely slow
# df.fillna({col: self.fill_value for col in cols}, inplace=True)
# So we use numpy to accelerate filling values
nan_select = np.isnan(df.values)
nan_select[:, ~df.columns.isin(cols)] = False
df.values[nan_select] = self.fill_value
return df return df
@@ -318,6 +324,20 @@ class CSRankNorm(Processor):
The operations across different stocks are often called Cross Sectional Operation. The operations across different stocks are often called Cross Sectional Operation.
For example, CSRankNorm is an operation that grouping the data by each day and rank `across` all the stocks in each day. For example, CSRankNorm is an operation that grouping the data by each day and rank `across` all the stocks in each day.
Explanation about 3.46 & 0.5
.. code-block:: python
import numpy as np
import pandas as pd
x = np.random.random(10000) # for any variable
x_rank = pd.Series(x).rank(pct=True) # if it is converted to rank, it will be a uniform distributed
x_rank_norm = (x_rank - x_rank.mean()) / x_rank.std() # Normally, we will normalize it to make it like normal distribution
x_rank.mean() # accounts for 0.5
1 / x_rank.std() # accounts for 3.46
""" """
def __init__(self, fields_group=None): def __init__(self, fields_group=None):

View File

@@ -5,7 +5,7 @@ import pandas as pd
class InstProcessor: class InstProcessor:
@abc.abstractmethod @abc.abstractmethod
def __call__(self, df: pd.DataFrame, *args, **kwargs): def __call__(self, df: pd.DataFrame, instrument, *args, **kwargs):
""" """
process the data process the data

View File

@@ -10,9 +10,7 @@ import pandas as pd
from typing import Union, List, Type from typing import Union, List, Type
from scipy.stats import percentileofscore from scipy.stats import percentileofscore
from .base import Expression, ExpressionOps, Feature, PFeature
from .base import Expression, ExpressionOps, Feature
from ..log import get_module_logger from ..log import get_module_logger
from ..utils import get_callable_kwargs from ..utils import get_callable_kwargs
@@ -24,7 +22,7 @@ except ImportError:
"#### Do not import qlib package in the repository directory in case of importing qlib from . without compiling #####" "#### Do not import qlib package in the repository directory in case of importing qlib from . without compiling #####"
) )
raise raise
except ValueError as e: except ValueError:
print("!!!!!!!! A error occurs when importing operators implemented based on Cython.!!!!!!!!") print("!!!!!!!! A error occurs when importing operators implemented based on Cython.!!!!!!!!")
print("!!!!!!!! They will be disabled. Please Upgrade your numpy to enable them !!!!!!!!") print("!!!!!!!! They will be disabled. Please Upgrade your numpy to enable them !!!!!!!!")
# We catch this error because some platform can't upgrade there package (e.g. Kaggle) # We catch this error because some platform can't upgrade there package (e.g. Kaggle)
@@ -84,8 +82,8 @@ class NpElemOperator(ElemOperator):
self.func = func self.func = func
super(NpElemOperator, self).__init__(feature) super(NpElemOperator, self).__init__(feature)
def _load_internal(self, instrument, start_index, end_index, freq): def _load_internal(self, instrument, start_index, end_index, *args):
series = self.feature.load(instrument, start_index, end_index, freq) series = self.feature.load(instrument, start_index, end_index, *args)
return getattr(np, self.func)(series) return getattr(np, self.func)(series)
@@ -124,11 +122,11 @@ class Sign(NpElemOperator):
def __init__(self, feature): def __init__(self, feature):
super(Sign, self).__init__(feature, "sign") super(Sign, self).__init__(feature, "sign")
def _load_internal(self, instrument, start_index, end_index, freq): def _load_internal(self, instrument, start_index, end_index, *args):
""" """
To avoid error raised by bool type input, we transform the data into float32. To avoid error raised by bool type input, we transform the data into float32.
""" """
series = self.feature.load(instrument, start_index, end_index, freq) series = self.feature.load(instrument, start_index, end_index, *args)
# TODO: More precision types should be configurable # TODO: More precision types should be configurable
series = series.astype(np.float32) series = series.astype(np.float32)
return getattr(np, self.func)(series) return getattr(np, self.func)(series)
@@ -152,32 +150,6 @@ class Log(NpElemOperator):
super(Log, self).__init__(feature, "log") super(Log, self).__init__(feature, "log")
class Power(NpElemOperator):
"""Feature Power
Parameters
----------
feature : Expression
feature instance
Returns
----------
Expression
a feature instance with power
"""
def __init__(self, feature, exponent):
super(Power, self).__init__(feature, "power")
self.exponent = exponent
def __str__(self):
return "{}({},{})".format(type(self).__name__, self.feature, self.exponent)
def _load_internal(self, instrument, start_index, end_index, freq):
series = self.feature.load(instrument, start_index, end_index, freq)
return getattr(np, self.func)(series, self.exponent)
class Mask(NpElemOperator): class Mask(NpElemOperator):
"""Feature Mask """Feature Mask
@@ -201,8 +173,8 @@ class Mask(NpElemOperator):
def __str__(self): def __str__(self):
return "{}({},{})".format(type(self).__name__, self.feature, self.instrument.lower()) return "{}({},{})".format(type(self).__name__, self.feature, self.instrument.lower())
def _load_internal(self, instrument, start_index, end_index, freq): def _load_internal(self, instrument, start_index, end_index, *args):
return self.feature.load(self.instrument, start_index, end_index, freq) return self.feature.load(self.instrument, start_index, end_index, *args)
class Not(NpElemOperator): class Not(NpElemOperator):
@@ -252,24 +224,24 @@ class PairOperator(ExpressionOps):
return "{}({},{})".format(type(self).__name__, self.feature_left, self.feature_right) return "{}({},{})".format(type(self).__name__, self.feature_left, self.feature_right)
def get_longest_back_rolling(self): def get_longest_back_rolling(self):
if isinstance(self.feature_left, Expression): if isinstance(self.feature_left, (Expression,)):
left_br = self.feature_left.get_longest_back_rolling() left_br = self.feature_left.get_longest_back_rolling()
else: else:
left_br = 0 left_br = 0
if isinstance(self.feature_right, Expression): if isinstance(self.feature_right, (Expression,)):
right_br = self.feature_right.get_longest_back_rolling() right_br = self.feature_right.get_longest_back_rolling()
else: else:
right_br = 0 right_br = 0
return max(left_br, right_br) return max(left_br, right_br)
def get_extended_window_size(self): def get_extended_window_size(self):
if isinstance(self.feature_left, Expression): if isinstance(self.feature_left, (Expression,)):
ll, lr = self.feature_left.get_extended_window_size() ll, lr = self.feature_left.get_extended_window_size()
else: else:
ll, lr = 0, 0 ll, lr = 0, 0
if isinstance(self.feature_right, Expression): if isinstance(self.feature_right, (Expression,)):
rl, rr = self.feature_right.get_extended_window_size() rl, rr = self.feature_right.get_extended_window_size()
else: else:
rl, rr = 0, 0 rl, rr = 0, 0
@@ -298,16 +270,16 @@ class NpPairOperator(PairOperator):
self.func = func self.func = func
super(NpPairOperator, self).__init__(feature_left, feature_right) super(NpPairOperator, self).__init__(feature_left, feature_right)
def _load_internal(self, instrument, start_index, end_index, freq): def _load_internal(self, instrument, start_index, end_index, *args):
assert any( assert any(
[isinstance(self.feature_left, Expression), self.feature_right, Expression] [isinstance(self.feature_left, (Expression,)), self.feature_right, Expression]
), "at least one of two inputs is Expression instance" ), "at least one of two inputs is Expression instance"
if isinstance(self.feature_left, Expression): if isinstance(self.feature_left, (Expression,)):
series_left = self.feature_left.load(instrument, start_index, end_index, freq) series_left = self.feature_left.load(instrument, start_index, end_index, *args)
else: else:
series_left = self.feature_left # numeric value series_left = self.feature_left # numeric value
if isinstance(self.feature_right, Expression): if isinstance(self.feature_right, (Expression,)):
series_right = self.feature_right.load(instrument, start_index, end_index, freq) series_right = self.feature_right.load(instrument, start_index, end_index, *args)
else: else:
series_right = self.feature_right series_right = self.feature_right
check_length = isinstance(series_left, (np.ndarray, pd.Series)) and isinstance( check_length = isinstance(series_left, (np.ndarray, pd.Series)) and isinstance(
@@ -335,6 +307,26 @@ class NpPairOperator(PairOperator):
return res return res
class Power(NpPairOperator):
"""Power Operator
Parameters
----------
feature_left : Expression
feature instance
feature_right : Expression
feature instance
Returns
----------
Feature:
The bases in feature_left raised to the exponents in feature_right
"""
def __init__(self, feature_left, feature_right):
super(Power, self).__init__(feature_left, feature_right, "power")
class Add(NpPairOperator): class Add(NpPairOperator):
"""Add Operator """Add Operator
@@ -637,48 +629,48 @@ class If(ExpressionOps):
def __str__(self): def __str__(self):
return "If({},{},{})".format(self.condition, self.feature_left, self.feature_right) return "If({},{},{})".format(self.condition, self.feature_left, self.feature_right)
def _load_internal(self, instrument, start_index, end_index, freq): def _load_internal(self, instrument, start_index, end_index, *args):
series_cond = self.condition.load(instrument, start_index, end_index, freq) series_cond = self.condition.load(instrument, start_index, end_index, *args)
if isinstance(self.feature_left, Expression): if isinstance(self.feature_left, (Expression,)):
series_left = self.feature_left.load(instrument, start_index, end_index, freq) series_left = self.feature_left.load(instrument, start_index, end_index, *args)
else: else:
series_left = self.feature_left series_left = self.feature_left
if isinstance(self.feature_right, Expression): if isinstance(self.feature_right, (Expression,)):
series_right = self.feature_right.load(instrument, start_index, end_index, freq) series_right = self.feature_right.load(instrument, start_index, end_index, *args)
else: else:
series_right = self.feature_right series_right = self.feature_right
series = pd.Series(np.where(series_cond, series_left, series_right), index=series_cond.index) series = pd.Series(np.where(series_cond, series_left, series_right), index=series_cond.index)
return series return series
def get_longest_back_rolling(self): def get_longest_back_rolling(self):
if isinstance(self.feature_left, Expression): if isinstance(self.feature_left, (Expression,)):
left_br = self.feature_left.get_longest_back_rolling() left_br = self.feature_left.get_longest_back_rolling()
else: else:
left_br = 0 left_br = 0
if isinstance(self.feature_right, Expression): if isinstance(self.feature_right, (Expression,)):
right_br = self.feature_right.get_longest_back_rolling() right_br = self.feature_right.get_longest_back_rolling()
else: else:
right_br = 0 right_br = 0
if isinstance(self.condition, Expression): if isinstance(self.condition, (Expression,)):
c_br = self.condition.get_longest_back_rolling() c_br = self.condition.get_longest_back_rolling()
else: else:
c_br = 0 c_br = 0
return max(left_br, right_br, c_br) return max(left_br, right_br, c_br)
def get_extended_window_size(self): def get_extended_window_size(self):
if isinstance(self.feature_left, Expression): if isinstance(self.feature_left, (Expression,)):
ll, lr = self.feature_left.get_extended_window_size() ll, lr = self.feature_left.get_extended_window_size()
else: else:
ll, lr = 0, 0 ll, lr = 0, 0
if isinstance(self.feature_right, Expression): if isinstance(self.feature_right, (Expression,)):
rl, rr = self.feature_right.get_extended_window_size() rl, rr = self.feature_right.get_extended_window_size()
else: else:
rl, rr = 0, 0 rl, rr = 0, 0
if isinstance(self.condition, Expression): if isinstance(self.condition, (Expression,)):
cl, cr = self.condition.get_extended_window_size() cl, cr = self.condition.get_extended_window_size()
else: else:
cl, cr = 0, 0 cl, cr = 0, 0
@@ -719,8 +711,8 @@ class Rolling(ExpressionOps):
def __str__(self): def __str__(self):
return "{}({},{})".format(type(self).__name__, self.feature, self.N) return "{}({},{})".format(type(self).__name__, self.feature, self.N)
def _load_internal(self, instrument, start_index, end_index, freq): def _load_internal(self, instrument, start_index, end_index, *args):
series = self.feature.load(instrument, start_index, end_index, freq) series = self.feature.load(instrument, start_index, end_index, *args)
# NOTE: remove all null check, # NOTE: remove all null check,
# now it's user's responsibility to decide whether use features in null days # now it's user's responsibility to decide whether use features in null days
# isnull = series.isnull() # NOTE: isnull = NaN, inf is not null # isnull = series.isnull() # NOTE: isnull = NaN, inf is not null
@@ -777,8 +769,8 @@ class Ref(Rolling):
def __init__(self, feature, N): def __init__(self, feature, N):
super(Ref, self).__init__(feature, N, "ref") super(Ref, self).__init__(feature, N, "ref")
def _load_internal(self, instrument, start_index, end_index, freq): def _load_internal(self, instrument, start_index, end_index, *args):
series = self.feature.load(instrument, start_index, end_index, freq) series = self.feature.load(instrument, start_index, end_index, *args)
# N = 0, return first day # N = 0, return first day
if series.empty: if series.empty:
return series # Pandas bug, see: https://github.com/pandas-dev/pandas/issues/21049 return series # Pandas bug, see: https://github.com/pandas-dev/pandas/issues/21049
@@ -967,8 +959,8 @@ class IdxMax(Rolling):
def __init__(self, feature, N): def __init__(self, feature, N):
super(IdxMax, self).__init__(feature, N, "idxmax") super(IdxMax, self).__init__(feature, N, "idxmax")
def _load_internal(self, instrument, start_index, end_index, freq): def _load_internal(self, instrument, start_index, end_index, *args):
series = self.feature.load(instrument, start_index, end_index, freq) series = self.feature.load(instrument, start_index, end_index, *args)
if self.N == 0: if self.N == 0:
series = series.expanding(min_periods=1).apply(lambda x: x.argmax() + 1, raw=True) series = series.expanding(min_periods=1).apply(lambda x: x.argmax() + 1, raw=True)
else: else:
@@ -1015,8 +1007,8 @@ class IdxMin(Rolling):
def __init__(self, feature, N): def __init__(self, feature, N):
super(IdxMin, self).__init__(feature, N, "idxmin") super(IdxMin, self).__init__(feature, N, "idxmin")
def _load_internal(self, instrument, start_index, end_index, freq): def _load_internal(self, instrument, start_index, end_index, *args):
series = self.feature.load(instrument, start_index, end_index, freq) series = self.feature.load(instrument, start_index, end_index, *args)
if self.N == 0: if self.N == 0:
series = series.expanding(min_periods=1).apply(lambda x: x.argmin() + 1, raw=True) series = series.expanding(min_periods=1).apply(lambda x: x.argmin() + 1, raw=True)
else: else:
@@ -1047,8 +1039,8 @@ class Quantile(Rolling):
def __str__(self): def __str__(self):
return "{}({},{},{})".format(type(self).__name__, self.feature, self.N, self.qscore) return "{}({},{},{})".format(type(self).__name__, self.feature, self.N, self.qscore)
def _load_internal(self, instrument, start_index, end_index, freq): def _load_internal(self, instrument, start_index, end_index, *args):
series = self.feature.load(instrument, start_index, end_index, freq) series = self.feature.load(instrument, start_index, end_index, *args)
if self.N == 0: if self.N == 0:
series = series.expanding(min_periods=1).quantile(self.qscore) series = series.expanding(min_periods=1).quantile(self.qscore)
else: else:
@@ -1095,8 +1087,8 @@ class Mad(Rolling):
def __init__(self, feature, N): def __init__(self, feature, N):
super(Mad, self).__init__(feature, N, "mad") super(Mad, self).__init__(feature, N, "mad")
def _load_internal(self, instrument, start_index, end_index, freq): def _load_internal(self, instrument, start_index, end_index, *args):
series = self.feature.load(instrument, start_index, end_index, freq) series = self.feature.load(instrument, start_index, end_index, *args)
# TODO: implement in Cython # TODO: implement in Cython
def mad(x): def mad(x):
@@ -1129,8 +1121,8 @@ class Rank(Rolling):
def __init__(self, feature, N): def __init__(self, feature, N):
super(Rank, self).__init__(feature, N, "rank") super(Rank, self).__init__(feature, N, "rank")
def _load_internal(self, instrument, start_index, end_index, freq): def _load_internal(self, instrument, start_index, end_index, *args):
series = self.feature.load(instrument, start_index, end_index, freq) series = self.feature.load(instrument, start_index, end_index, *args)
# TODO: implement in Cython # TODO: implement in Cython
def rank(x): def rank(x):
@@ -1187,8 +1179,8 @@ class Delta(Rolling):
def __init__(self, feature, N): def __init__(self, feature, N):
super(Delta, self).__init__(feature, N, "delta") super(Delta, self).__init__(feature, N, "delta")
def _load_internal(self, instrument, start_index, end_index, freq): def _load_internal(self, instrument, start_index, end_index, *args):
series = self.feature.load(instrument, start_index, end_index, freq) series = self.feature.load(instrument, start_index, end_index, *args)
if self.N == 0: if self.N == 0:
series = series - series.iloc[0] series = series - series.iloc[0]
else: else:
@@ -1225,8 +1217,8 @@ class Slope(Rolling):
def __init__(self, feature, N): def __init__(self, feature, N):
super(Slope, self).__init__(feature, N, "slope") super(Slope, self).__init__(feature, N, "slope")
def _load_internal(self, instrument, start_index, end_index, freq): def _load_internal(self, instrument, start_index, end_index, *args):
series = self.feature.load(instrument, start_index, end_index, freq) series = self.feature.load(instrument, start_index, end_index, *args)
if self.N == 0: if self.N == 0:
series = pd.Series(expanding_slope(series.values), index=series.index) series = pd.Series(expanding_slope(series.values), index=series.index)
else: else:
@@ -1253,8 +1245,8 @@ class Rsquare(Rolling):
def __init__(self, feature, N): def __init__(self, feature, N):
super(Rsquare, self).__init__(feature, N, "rsquare") super(Rsquare, self).__init__(feature, N, "rsquare")
def _load_internal(self, instrument, start_index, end_index, freq): def _load_internal(self, instrument, start_index, end_index, *args):
_series = self.feature.load(instrument, start_index, end_index, freq) _series = self.feature.load(instrument, start_index, end_index, *args)
if self.N == 0: if self.N == 0:
series = pd.Series(expanding_rsquare(_series.values), index=_series.index) series = pd.Series(expanding_rsquare(_series.values), index=_series.index)
else: else:
@@ -1282,8 +1274,8 @@ class Resi(Rolling):
def __init__(self, feature, N): def __init__(self, feature, N):
super(Resi, self).__init__(feature, N, "resi") super(Resi, self).__init__(feature, N, "resi")
def _load_internal(self, instrument, start_index, end_index, freq): def _load_internal(self, instrument, start_index, end_index, *args):
series = self.feature.load(instrument, start_index, end_index, freq) series = self.feature.load(instrument, start_index, end_index, *args)
if self.N == 0: if self.N == 0:
series = pd.Series(expanding_resi(series.values), index=series.index) series = pd.Series(expanding_resi(series.values), index=series.index)
else: else:
@@ -1310,8 +1302,8 @@ class WMA(Rolling):
def __init__(self, feature, N): def __init__(self, feature, N):
super(WMA, self).__init__(feature, N, "wma") super(WMA, self).__init__(feature, N, "wma")
def _load_internal(self, instrument, start_index, end_index, freq): def _load_internal(self, instrument, start_index, end_index, *args):
series = self.feature.load(instrument, start_index, end_index, freq) series = self.feature.load(instrument, start_index, end_index, *args)
# TODO: implement in Cython # TODO: implement in Cython
def weighted_mean(x): def weighted_mean(x):
@@ -1345,8 +1337,8 @@ class EMA(Rolling):
def __init__(self, feature, N): def __init__(self, feature, N):
super(EMA, self).__init__(feature, N, "ema") super(EMA, self).__init__(feature, N, "ema")
def _load_internal(self, instrument, start_index, end_index, freq): def _load_internal(self, instrument, start_index, end_index, *args):
series = self.feature.load(instrument, start_index, end_index, freq) series = self.feature.load(instrument, start_index, end_index, *args)
def exp_weighted_mean(x): def exp_weighted_mean(x):
a = 1 - 2 / (1 + len(x)) a = 1 - 2 / (1 + len(x))
@@ -1392,17 +1384,17 @@ class PairRolling(ExpressionOps):
def __str__(self): def __str__(self):
return "{}({},{},{})".format(type(self).__name__, self.feature_left, self.feature_right, self.N) return "{}({},{},{})".format(type(self).__name__, self.feature_left, self.feature_right, self.N)
def _load_internal(self, instrument, start_index, end_index, freq): def _load_internal(self, instrument, start_index, end_index, *args):
assert any( assert any(
[isinstance(self.feature_left, Expression), self.feature_right, Expression] [isinstance(self.feature_left, Expression), self.feature_right, Expression]
), "at least one of two inputs is Expression instance" ), "at least one of two inputs is Expression instance"
if isinstance(self.feature_left, Expression): if isinstance(self.feature_left, Expression):
series_left = self.feature_left.load(instrument, start_index, end_index, freq) series_left = self.feature_left.load(instrument, start_index, end_index, *args)
else: else:
series_left = self.feature_left # numeric value series_left = self.feature_left # numeric value
if isinstance(self.feature_right, Expression): if isinstance(self.feature_right, Expression):
series_right = self.feature_right.load(instrument, start_index, end_index, freq) series_right = self.feature_right.load(instrument, start_index, end_index, *args)
else: else:
series_right = self.feature_right series_right = self.feature_right
@@ -1465,12 +1457,12 @@ class Corr(PairRolling):
def __init__(self, feature_left, feature_right, N): def __init__(self, feature_left, feature_right, N):
super(Corr, self).__init__(feature_left, feature_right, N, "corr") super(Corr, self).__init__(feature_left, feature_right, N, "corr")
def _load_internal(self, instrument, start_index, end_index, freq): def _load_internal(self, instrument, start_index, end_index, *args):
res: pd.Series = super(Corr, self)._load_internal(instrument, start_index, end_index, freq) res: pd.Series = super(Corr, self)._load_internal(instrument, start_index, end_index, *args)
# NOTE: Load uses MemCache, so calling load again will not cause performance degradation # NOTE: Load uses MemCache, so calling load again will not cause performance degradation
series_left = self.feature_left.load(instrument, start_index, end_index, freq) series_left = self.feature_left.load(instrument, start_index, end_index, *args)
series_right = self.feature_right.load(instrument, start_index, end_index, freq) series_right = self.feature_right.load(instrument, start_index, end_index, *args)
res.loc[ res.loc[
np.isclose(series_left.rolling(self.N, min_periods=1).std(), 0, atol=2e-05) np.isclose(series_left.rolling(self.N, min_periods=1).std(), 0, atol=2e-05)
| np.isclose(series_right.rolling(self.N, min_periods=1).std(), 0, atol=2e-05) | np.isclose(series_right.rolling(self.N, min_periods=1).std(), 0, atol=2e-05)
@@ -1529,8 +1521,8 @@ class TResample(ElemOperator):
def __str__(self): def __str__(self):
return "{}({},{})".format(type(self).__name__, self.feature, self.freq) return "{}({},{})".format(type(self).__name__, self.feature, self.freq)
def _load_internal(self, instrument, start_index, end_index, freq): def _load_internal(self, instrument, start_index, end_index, *args):
series = self.feature.load(instrument, start_index, end_index, freq) series = self.feature.load(instrument, start_index, end_index, *args)
if series.empty: if series.empty:
return series return series
@@ -1590,6 +1582,7 @@ OpsList = [
IdxMin, IdxMin,
If, If,
Feature, Feature,
PFeature,
] + [TResample] ] + [TResample]
@@ -1622,7 +1615,7 @@ class OpsWrapper:
else: else:
_ops_class = _operator _ops_class = _operator
if not issubclass(_ops_class, Expression): if not issubclass(_ops_class, (Expression,)):
raise TypeError("operator must be subclass of ExpressionOps, not {}".format(_ops_class)) raise TypeError("operator must be subclass of ExpressionOps, not {}".format(_ops_class))
if _ops_class.__name__ in self._ops: if _ops_class.__name__ in self._ops:
@@ -1644,8 +1637,10 @@ def register_all_ops(C):
"""register all operator""" """register all operator"""
logger = get_module_logger("ops") logger = get_module_logger("ops")
from qlib.data.pit import P, PRef # pylint: disable=C0415
Operators.reset() Operators.reset()
Operators.register(OpsList) Operators.register(OpsList + [P, PRef])
if getattr(C, "custom_ops", None) is not None: if getattr(C, "custom_ops", None) is not None:
Operators.register(C.custom_ops) Operators.register(C.custom_ops)

72
qlib/data/pit.py Normal file
View File

@@ -0,0 +1,72 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
"""
Qlib follow the logic below to supporting point-in-time database
For each stock, the format of its data is <observe_time, feature>. Expression Engine support calculation on such format of data
To calculate the feature value f_t at a specific observe time t, data with format <period_time, feature> will be used.
For example, the average earning of last 4 quarters (period_time) on 20190719 (observe_time)
The calculation of both <period_time, feature> and <observe_time, feature> data rely on expression engine. It consists of 2 phases.
1) calculation <period_time, feature> at each observation time t and it will collasped into a point (just like a normal feature)
2) concatenate all th collasped data, we will get data with format <observe_time, feature>.
Qlib will use the operator `P` to perform the collapse.
"""
import numpy as np
import pandas as pd
from qlib.data.ops import ElemOperator
from qlib.log import get_module_logger
from .data import Cal
class P(ElemOperator):
def _load_internal(self, instrument, start_index, end_index, freq):
_calendar = Cal.calendar(freq=freq)
resample_data = np.empty(end_index - start_index + 1, dtype="float32")
for cur_index in range(start_index, end_index + 1):
cur_time = _calendar[cur_index]
# To load expression accurately, more historical data are required
start_ws, end_ws = self.feature.get_extended_window_size()
if end_ws > 0:
raise ValueError(
"PIT database does not support referring to future period (e.g. expressions like `Ref('$$roewa_q', -1)` are not supported"
)
# The calculated value will always the last element, so the end_offset is zero.
try:
s = self._load_feature(instrument, -start_ws, 0, cur_time)
resample_data[cur_index - start_index] = s.iloc[-1] if len(s) > 0 else np.nan
except FileNotFoundError:
get_module_logger("base").warning(f"WARN: period data not found for {str(self)}")
return pd.Series(dtype="float32", name=str(self))
resample_series = pd.Series(
resample_data, index=pd.RangeIndex(start_index, end_index + 1), dtype="float32", name=str(self)
)
return resample_series
def _load_feature(self, instrument, start_index, end_index, cur_time):
return self.feature.load(instrument, start_index, end_index, cur_time)
def get_longest_back_rolling(self):
# The period data will collapse as a normal feature. So no extending and looking back
return 0
def get_extended_window_size(self):
# The period data will collapse as a normal feature. So no extending and looking back
return 0, 0
class PRef(P):
def __init__(self, feature, period):
super().__init__(feature)
self.period = period
def __str__(self):
return f"{super().__str__()}[{self.period}]"
def _load_feature(self, instrument, start_index, end_index, cur_time):
return self.feature.load(instrument, start_index, end_index, cur_time, self.period)

View File

@@ -2,3 +2,6 @@
# Licensed under the MIT License. # Licensed under the MIT License.
from .storage import CalendarStorage, InstrumentStorage, FeatureStorage, CalVT, InstVT, InstKT from .storage import CalendarStorage, InstrumentStorage, FeatureStorage, CalVT, InstVT, InstKT
__all__ = ["CalendarStorage", "InstrumentStorage", "FeatureStorage", "CalVT", "InstVT", "InstKT"]

View File

@@ -79,6 +79,7 @@ class FileCalendarStorage(FileStorageMixin, CalendarStorage):
self.future = future self.future = future
self._provider_uri = None if provider_uri is None else C.DataPathManager.format_provider_uri(provider_uri) self._provider_uri = None if provider_uri is None else C.DataPathManager.format_provider_uri(provider_uri)
self.enable_read_cache = True # TODO: make it configurable self.enable_read_cache = True # TODO: make it configurable
self.region = C["region"]
@property @property
def file_name(self) -> str: def file_name(self) -> str:
@@ -130,7 +131,9 @@ class FileCalendarStorage(FileStorageMixin, CalendarStorage):
else: else:
_calendar = self._read_calendar() _calendar = self._read_calendar()
if Freq(self._freq_file) != Freq(self.freq): if Freq(self._freq_file) != Freq(self.freq):
_calendar = resam_calendar(np.array(list(map(pd.Timestamp, _calendar))), self._freq_file, self.freq) _calendar = resam_calendar(
np.array(list(map(pd.Timestamp, _calendar))), self._freq_file, self.freq, self.region
)
return _calendar return _calendar
def _get_storage_freq(self) -> List[str]: def _get_storage_freq(self) -> List[str]:

View File

@@ -126,12 +126,10 @@ class CalendarStorage(BaseStorage):
@overload @overload
def __setitem__(self, i: int, value: CalVT) -> None: def __setitem__(self, i: int, value: CalVT) -> None:
"""x.__setitem__(i, o) <==> (x[i] = o)""" """x.__setitem__(i, o) <==> (x[i] = o)"""
...
@overload @overload
def __setitem__(self, s: slice, value: Iterable[CalVT]) -> None: def __setitem__(self, s: slice, value: Iterable[CalVT]) -> None:
"""x.__setitem__(s, o) <==> (x[s] = o)""" """x.__setitem__(s, o) <==> (x[s] = o)"""
...
def __setitem__(self, i, value) -> None: def __setitem__(self, i, value) -> None:
raise NotImplementedError( raise NotImplementedError(
@@ -141,12 +139,10 @@ class CalendarStorage(BaseStorage):
@overload @overload
def __delitem__(self, i: int) -> None: def __delitem__(self, i: int) -> None:
"""x.__delitem__(i) <==> del x[i]""" """x.__delitem__(i) <==> del x[i]"""
...
@overload @overload
def __delitem__(self, i: slice) -> None: def __delitem__(self, i: slice) -> None:
"""x.__delitem__(slice(start: int, stop: int, step: int)) <==> del x[start:stop:step]""" """x.__delitem__(slice(start: int, stop: int, step: int)) <==> del x[start:stop:step]"""
...
def __delitem__(self, i) -> None: def __delitem__(self, i) -> None:
""" """
@@ -162,12 +158,10 @@ class CalendarStorage(BaseStorage):
@overload @overload
def __getitem__(self, s: slice) -> Iterable[CalVT]: def __getitem__(self, s: slice) -> Iterable[CalVT]:
"""x.__getitem__(slice(start: int, stop: int, step: int)) <==> x[start:stop:step]""" """x.__getitem__(slice(start: int, stop: int, step: int)) <==> x[start:stop:step]"""
...
@overload @overload
def __getitem__(self, i: int) -> CalVT: def __getitem__(self, i: int) -> CalVT:
"""x.__getitem__(i) <==> x[i]""" """x.__getitem__(i) <==> x[i]"""
...
def __getitem__(self, i) -> CalVT: def __getitem__(self, i) -> CalVT:
""" """
@@ -467,12 +461,10 @@ class FeatureStorage(BaseStorage):
------- -------
pd.Series(values, index=pd.RangeIndex(start, len(values)) pd.Series(values, index=pd.RangeIndex(start, len(values))
""" """
...
@overload @overload
def __getitem__(self, i: int) -> Tuple[int, float]: def __getitem__(self, i: int) -> Tuple[int, float]:
"""x.__getitem__(y) <==> x[y]""" """x.__getitem__(y) <==> x[y]"""
...
def __getitem__(self, i) -> Union[Tuple[int, float], pd.Series]: def __getitem__(self, i) -> Union[Tuple[int, float], pd.Series]:
"""x.__getitem__(y) <==> x[y] """x.__getitem__(y) <==> x[y]

View File

@@ -4,3 +4,6 @@
import warnings import warnings
from .base import Model from .base import Model
__all__ = ["Model", "warnings"]

View File

@@ -3,3 +3,6 @@
from .task import MetaTask from .task import MetaTask
from .dataset import MetaTaskDataset from .dataset import MetaTaskDataset
__all__ = ["MetaTask", "MetaTaskDataset"]

View File

@@ -5,3 +5,11 @@ from .base import RiskModel
from .poet import POETCovEstimator from .poet import POETCovEstimator
from .shrink import ShrinkCovEstimator from .shrink import ShrinkCovEstimator
from .structured import StructuredCovEstimator from .structured import StructuredCovEstimator
__all__ = [
"RiskModel",
"POETCovEstimator",
"ShrinkCovEstimator",
"StructuredCovEstimator",
]

View File

@@ -2,7 +2,6 @@
# Licensed under the MIT License. # Licensed under the MIT License.
import numpy as np import numpy as np
import pandas as pd
from typing import Union from typing import Union
from sklearn.decomposition import PCA, FactorAnalysis from sklearn.decomposition import PCA, FactorAnalysis

View File

@@ -14,28 +14,29 @@ import json
import yaml import yaml
import redis import redis
import bisect import bisect
import shutil import struct
import difflib import difflib
import inspect import inspect
import hashlib import hashlib
import warnings
import datetime import datetime
import requests import requests
import tempfile
import importlib import importlib
import contextlib import contextlib
import collections import collections
import numpy as np import numpy as np
import pandas as pd import pandas as pd
from pathlib import Path from pathlib import Path
from typing import Dict, Union, Tuple, Any, Text, Optional, Callable from typing import List, Dict, Union, Tuple, Any, Optional, Callable
from types import ModuleType from types import ModuleType
from urllib.parse import urlparse from urllib.parse import urlparse
from packaging import version
from .file import get_or_create_path, save_multiple_parts_file, unpack_archive_with_buffer, get_tmp_file_with_buffer from .file import get_or_create_path, save_multiple_parts_file, unpack_archive_with_buffer, get_tmp_file_with_buffer
from ..config import C from ..config import C
from ..log import get_module_logger, set_log_with_config from ..log import get_module_logger, set_log_with_config
log = get_module_logger("utils") log = get_module_logger("utils")
# MultiIndex.is_lexsorted() is a deprecated method in Pandas 1.3.0.
is_deprecated_lexsorted_pandas = version.parse(pd.__version__) > version.parse("1.3.0")
#################### Server #################### #################### Server ####################
@@ -62,6 +63,104 @@ def read_bin(file_path: Union[str, Path], start_index, end_index):
return series return series
def get_period_list(first: int, last: int, quarterly: bool) -> List[int]:
"""
This method will be used in PIT database.
It return all the possible values between `first` and `end` (first and end is included)
Parameters
----------
quarterly : bool
will it return quarterly index or yearly index.
Returns
-------
List[int]
the possible index between [first, last]
"""
if not quarterly:
assert all(1900 <= x <= 2099 for x in (first, last)), "invalid arguments"
return list(range(first, last + 1))
else:
assert all(190000 <= x <= 209904 for x in (first, last)), "invalid arguments"
res = []
for year in range(first // 100, last // 100 + 1):
for q in range(1, 5):
period = year * 100 + q
if first <= period <= last:
res.append(year * 100 + q)
return res
def get_period_offset(first_year, period, quarterly):
if quarterly:
offset = (period // 100 - first_year) * 4 + period % 100 - 1
else:
offset = period - first_year
return offset
def read_period_data(index_path, data_path, period, cur_date_int: int, quarterly, last_period_index: int = None):
"""
At `cur_date`(e.g. 20190102), read the information at `period`(e.g. 201803).
Only the updating info before cur_date or at cur_date will be used.
Parameters
----------
period: int
date period represented by interger, e.g. 201901 corresponds to the first quarter in 2019
cur_date_int: int
date which represented by interger, e.g. 20190102
last_period_index: int
it is a optional parameter; it is designed to avoid repeatedly access the .index data of PIT database when
sequentially observing the data (Because the latest index of a specific period of data certainly appear in after the one in last observation).
Returns
-------
the query value and byte index the index value
"""
DATA_DTYPE = "".join(
[
C.pit_record_type["date"],
C.pit_record_type["period"],
C.pit_record_type["value"],
C.pit_record_type["index"],
]
)
PERIOD_DTYPE = C.pit_record_type["period"]
INDEX_DTYPE = C.pit_record_type["index"]
NAN_VALUE = C.pit_record_nan["value"]
NAN_INDEX = C.pit_record_nan["index"]
# find the first index of linked revisions
if last_period_index is None:
with open(index_path, "rb") as fi:
(first_year,) = struct.unpack(PERIOD_DTYPE, fi.read(struct.calcsize(PERIOD_DTYPE)))
all_periods = np.fromfile(fi, dtype=INDEX_DTYPE)
offset = get_period_offset(first_year, period, quarterly)
_next = all_periods[offset]
else:
_next = last_period_index
# load data following the `_next` link
prev_value = NAN_VALUE
prev_next = _next
with open(data_path, "rb") as fd:
while _next != NAN_INDEX:
fd.seek(_next)
date, period, value, new_next = struct.unpack(DATA_DTYPE, fd.read(struct.calcsize(DATA_DTYPE)))
if date > cur_date_int:
break
prev_next = _next
_next = new_next
prev_value = value
return prev_value, prev_next
def np_ffill(arr: np.array): def np_ffill(arr: np.array):
""" """
forward fill a 1D numpy array forward fill a 1D numpy array
@@ -172,7 +271,17 @@ def parse_field(field):
if not isinstance(field, str): if not isinstance(field, str):
field = str(field) field = str(field)
for pattern, new in [(r"\$(\w+)", rf'Feature("\1")'), (r"(\w+\s*)\(", r"Operators.\1(")]: # Features # Operators # Chinese punctuation regex:
# \u3001 -> 、
# \uff1a ->
# \uff08 -> (
# \uff09 -> )
chinese_punctuation_regex = r"\u3001\uff1a\uff08\uff09"
for pattern, new in [
(rf"\$\$([\w{chinese_punctuation_regex}]+)", r'PFeature("\1")'), # $$ must be before $
(rf"\$([\w{chinese_punctuation_regex}]+)", r'Feature("\1")'),
(r"(\w+\s*)\(", r"Operators.\1("),
]: # Features # Operators
field = re.sub(pattern, new, field) field = re.sub(pattern, new, field)
return field return field
@@ -685,11 +794,15 @@ def lazy_sort_index(df: pd.DataFrame, axis=0) -> pd.DataFrame:
sorted dataframe sorted dataframe
""" """
idx = df.index if axis == 0 else df.columns idx = df.index if axis == 0 else df.columns
# NOTE: MultiIndex.is_lexsorted() is a deprecated method in Pandas 1.3.0 and is suggested to be replaced by MultiIndex.is_monotonic_increasing (see discussion here: https://github.com/pandas-dev/pandas/issues/32259). However, in case older versions of Pandas is implemented, MultiIndex.is_lexsorted() is necessary to prevent certain fatal errors. if (
if idx.is_monotonic_increasing and not (isinstance(idx, pd.MultiIndex) and not idx.is_lexsorted()): not idx.is_monotonic_increasing
return df or not is_deprecated_lexsorted_pandas
else: and isinstance(idx, pd.MultiIndex)
and not idx.is_lexsorted()
): # this case is for the old version
return df.sort_index(axis=axis) return df.sort_index(axis=axis)
else:
return df
FLATTEN_TUPLE = "_FLATTEN_TUPLE" FLATTEN_TUPLE = "_FLATTEN_TUPLE"
@@ -935,3 +1048,12 @@ def fname_to_code(fname: str):
if fname.startswith(prefix): if fname.startswith(prefix):
fname = fname.lstrip(prefix) fname = fname.lstrip(prefix)
return fname return fname
__all__ = [
"get_or_create_path",
"save_multiple_parts_file",
"unpack_archive_with_buffer",
"get_tmp_file_with_buffer",
"set_log_with_config",
]

View File

@@ -62,7 +62,7 @@ def sum_by_index(data_list: Union["SingleData"], new_index: list, fill_value=0)
new_index : list new_index : list
the new_index of new SingleData. the new_index of new SingleData.
fill_value : float fill_value : float
fill the missing values or replace np.NaN. fill the missing values or replace np.NaN.
Returns Returns
------- -------

View File

@@ -6,9 +6,12 @@ from typing import Union, Callable
from . import lazy_sort_index from . import lazy_sort_index
from .time import Freq, cal_sam_minute from .time import Freq, cal_sam_minute
from ..config import C
def resam_calendar(calendar_raw: np.ndarray, freq_raw: Union[str, Freq], freq_sam: Union[str, Freq]) -> np.ndarray: def resam_calendar(
calendar_raw: np.ndarray, freq_raw: Union[str, Freq], freq_sam: Union[str, Freq], region: str = None
) -> np.ndarray:
""" """
Resample the calendar with frequency freq_raw into the calendar with frequency freq_sam Resample the calendar with frequency freq_raw into the calendar with frequency freq_sam
Assumption: Assumption:
@@ -22,12 +25,16 @@ def resam_calendar(calendar_raw: np.ndarray, freq_raw: Union[str, Freq], freq_sa
Frequency of the raw calendar Frequency of the raw calendar
freq_sam : str freq_sam : str
Sample frequency Sample frequency
region: str
Region, for example, "cn", "us"
Returns Returns
------- -------
np.ndarray np.ndarray
The calendar with frequency freq_sam The calendar with frequency freq_sam
""" """
if region is None:
region = C["region"]
freq_raw = Freq(freq_raw) freq_raw = Freq(freq_raw)
freq_sam = Freq(freq_sam) freq_sam = Freq(freq_sam)
if not len(calendar_raw): if not len(calendar_raw):
@@ -40,7 +47,7 @@ def resam_calendar(calendar_raw: np.ndarray, freq_raw: Union[str, Freq], freq_sa
else: else:
if freq_raw.count > freq_sam.count: if freq_raw.count > freq_sam.count:
raise ValueError("raw freq must be higher than sampling freq") raise ValueError("raw freq must be higher than sampling freq")
_calendar_minute = np.unique(list(map(lambda x: cal_sam_minute(x, freq_sam.count), calendar_raw))) _calendar_minute = np.unique(list(map(lambda x: cal_sam_minute(x, freq_sam.count, region), calendar_raw)))
return _calendar_minute return _calendar_minute
# else, convert the raw calendar into day calendar, and divide the whole calendar into several bars evenly # else, convert the raw calendar into day calendar, and divide the whole calendar into several bars evenly

View File

@@ -4,7 +4,7 @@
Time related utils are compiled in this script Time related utils are compiled in this script
""" """
import bisect import bisect
from datetime import datetime, time, date from datetime import datetime, time, date, timedelta
from typing import List, Optional, Tuple, Union from typing import List, Optional, Tuple, Union
import functools import functools
import re import re
@@ -12,10 +12,24 @@ import re
import pandas as pd import pandas as pd
from qlib.config import C from qlib.config import C
from qlib.constant import REG_CN, REG_TW, REG_US
CN_TIME = [
datetime.strptime("9:30", "%H:%M"),
datetime.strptime("11:30", "%H:%M"),
datetime.strptime("13:00", "%H:%M"),
datetime.strptime("15:00", "%H:%M"),
]
US_TIME = [datetime.strptime("9:30", "%H:%M"), datetime.strptime("16:00", "%H:%M")]
TW_TIME = [
datetime.strptime("9:00", "%H:%M"),
datetime.strptime("13:30", "%H:%M"),
]
@functools.lru_cache(maxsize=240) @functools.lru_cache(maxsize=240)
def get_min_cal(shift: int = 0) -> List[time]: def get_min_cal(shift: int = 0, region: str = REG_CN) -> List[time]:
""" """
get the minute level calendar in day period get the minute level calendar in day period
@@ -24,6 +38,8 @@ def get_min_cal(shift: int = 0) -> List[time]:
shift : int shift : int
the shift direction would be like pandas shift. the shift direction would be like pandas shift.
series.shift(1) will replace the value at `i`-th with the one at `i-1`-th series.shift(1) will replace the value at `i`-th with the one at `i-1`-th
region: str
Region, for example, "cn", "us"
Returns Returns
------- -------
@@ -31,14 +47,30 @@ def get_min_cal(shift: int = 0) -> List[time]:
""" """
cal = [] cal = []
for ts in list(pd.date_range("9:30", "11:29", freq="1min") - pd.Timedelta(minutes=shift)) + list(
pd.date_range("13:00", "14:59", freq="1min") - pd.Timedelta(minutes=shift) if region == REG_CN:
): for ts in list(
cal.append(ts.time()) pd.date_range(CN_TIME[0], CN_TIME[1] - timedelta(minutes=1), freq="1min") - pd.Timedelta(minutes=shift)
) + list(
pd.date_range(CN_TIME[2], CN_TIME[3] - timedelta(minutes=1), freq="1min") - pd.Timedelta(minutes=shift)
):
cal.append(ts.time())
elif region == REG_TW:
for ts in list(
pd.date_range(TW_TIME[0], TW_TIME[1] - timedelta(minutes=1), freq="1min") - pd.Timedelta(minutes=shift)
):
cal.append(ts.time())
elif region == REG_US:
for ts in list(
pd.date_range(US_TIME[0], US_TIME[1] - timedelta(minutes=1), freq="1min") - pd.Timedelta(minutes=shift)
):
cal.append(ts.time())
else:
raise ValueError(f"{region} is not supported")
return cal return cal
def is_single_value(start_time, end_time, freq, region="cn"): def is_single_value(start_time, end_time, freq, region: str = REG_CN):
"""Is there only one piece of data for stock market. """Is there only one piece of data for stock market.
Parameters Parameters
@@ -48,12 +80,14 @@ def is_single_value(start_time, end_time, freq, region="cn"):
end_time : Union[pd.Timestamp, str] end_time : Union[pd.Timestamp, str]
closed end time for data. closed end time for data.
freq : freq :
region: str
Region, for example, "cn", "us"
Returns Returns
------- -------
bool bool
True means one piece of data to obtain. True means one piece of data to obtain.
""" """
if region == "cn": if region == REG_CN:
if end_time - start_time < freq: if end_time - start_time < freq:
return True return True
if start_time.hour == 11 and start_time.minute == 29 and start_time.second == 0: if start_time.hour == 11 and start_time.minute == 29 and start_time.second == 0:
@@ -61,6 +95,18 @@ def is_single_value(start_time, end_time, freq, region="cn"):
if start_time.hour == 14 and start_time.minute == 59 and start_time.second == 0: if start_time.hour == 14 and start_time.minute == 59 and start_time.second == 0:
return True return True
return False return False
elif region == REG_TW:
if end_time - start_time < freq:
return True
if start_time.hour == 13 and start_time.minute >= 25 and start_time.second == 0:
return True
return False
elif region == REG_US:
if end_time - start_time < freq:
return True
if start_time.hour == 15 and start_time.minute == 59 and start_time.second == 0:
return True
return False
else: else:
raise NotImplementedError(f"please implement the is_single_value func for {region}") raise NotImplementedError(f"please implement the is_single_value func for {region}")
@@ -72,8 +118,6 @@ class Freq:
NORM_FREQ_MINUTE = "min" # using min instead of minute for align with Qlib's data filename NORM_FREQ_MINUTE = "min" # using min instead of minute for align with Qlib's data filename
SUPPORT_CAL_LIST = [NORM_FREQ_MINUTE, NORM_FREQ_DAY] # FIXME: this list should from data SUPPORT_CAL_LIST = [NORM_FREQ_MINUTE, NORM_FREQ_DAY] # FIXME: this list should from data
MIN_CAL = get_min_cal()
def __init__(self, freq: Union[str, "Freq"]) -> None: def __init__(self, freq: Union[str, "Freq"]) -> None:
if isinstance(freq, str): if isinstance(freq, str):
self.count, self.base = self.parse(freq) self.count, self.base = self.parse(freq)
@@ -199,10 +243,8 @@ class Freq:
""" """
base_freq = Freq(base_freq) base_freq = Freq(base_freq)
# use the nearest freq greater than 0 # use the nearest freq greater than 0
_freq_minutes = []
min_freq = None min_freq = None
for _freq in freq_list: for _freq in freq_list:
freq = Freq(_freq)
_min_delta = Freq.get_min_delta(base_freq, _freq) _min_delta = Freq.get_min_delta(base_freq, _freq)
if _min_delta < 0: if _min_delta < 0:
continue continue
@@ -213,36 +255,32 @@ class Freq:
return min_freq[1] if min_freq else None return min_freq[1] if min_freq else None
CN_TIME = [ def time_to_day_index(time_obj: Union[str, datetime], region: str = REG_CN):
datetime.strptime("9:30", "%H:%M"),
datetime.strptime("11:30", "%H:%M"),
datetime.strptime("13:00", "%H:%M"),
datetime.strptime("15:00", "%H:%M"),
]
US_TIME = [datetime.strptime("9:30", "%H:%M"), datetime.strptime("16:00", "%H:%M")]
def time_to_day_index(time_obj: Union[str, datetime], region: str = "cn"):
if isinstance(time_obj, str): if isinstance(time_obj, str):
time_obj = datetime.strptime(time_obj, "%H:%M") time_obj = datetime.strptime(time_obj, "%H:%M")
if region == "cn": if region == REG_CN:
if CN_TIME[0] <= time_obj < CN_TIME[1]: if CN_TIME[0] <= time_obj < CN_TIME[1]:
return int((time_obj - CN_TIME[0]).total_seconds() / 60) return int((time_obj - CN_TIME[0]).total_seconds() / 60)
elif CN_TIME[2] <= time_obj < CN_TIME[3]: elif CN_TIME[2] <= time_obj < CN_TIME[3]:
return int((time_obj - CN_TIME[2]).total_seconds() / 60) + 120 return int((time_obj - CN_TIME[2]).total_seconds() / 60) + 120
else: else:
raise ValueError(f"{time_obj} is not the opening time of the {region} stock market") raise ValueError(f"{time_obj} is not the opening time of the {region} stock market")
elif region == "us": elif region == REG_US:
if US_TIME[0] <= time_obj < US_TIME[1]: if US_TIME[0] <= time_obj < US_TIME[1]:
return int((time_obj - US_TIME[0]).total_seconds() / 60) return int((time_obj - US_TIME[0]).total_seconds() / 60)
else: else:
raise ValueError(f"{time_obj} is not the opening time of the {region} stock market") raise ValueError(f"{time_obj} is not the opening time of the {region} stock market")
elif region == REG_TW:
if TW_TIME[0] <= time_obj < TW_TIME[1]:
return int((time_obj - TW_TIME[0]).total_seconds() / 60)
else:
raise ValueError(f"{time_obj} is not the opening time of the {region} stock market")
else: else:
raise ValueError(f"{region} is not supported") raise ValueError(f"{region} is not supported")
def get_day_min_idx_range(start: str, end: str, freq: str) -> Tuple[int, int]: def get_day_min_idx_range(start: str, end: str, freq: str, region: str) -> Tuple[int, int]:
""" """
get the min-bar index in a day for a time range (both left and right is closed) given a fixed frequency get the min-bar index in a day for a time range (both left and right is closed) given a fixed frequency
Parameters Parameters
@@ -262,7 +300,7 @@ def get_day_min_idx_range(start: str, end: str, freq: str) -> Tuple[int, int]:
start = pd.Timestamp(start).time() start = pd.Timestamp(start).time()
end = pd.Timestamp(end).time() end = pd.Timestamp(end).time()
freq = Freq(freq) freq = Freq(freq)
in_day_cal = Freq.MIN_CAL[:: freq.count] in_day_cal = get_min_cal(region=region)[:: freq.count]
left_idx = bisect.bisect_left(in_day_cal, start) left_idx = bisect.bisect_left(in_day_cal, start)
right_idx = bisect.bisect_right(in_day_cal, end) - 1 right_idx = bisect.bisect_right(in_day_cal, end) - 1
return left_idx, right_idx return left_idx, right_idx
@@ -282,7 +320,7 @@ def concat_date_time(date_obj: date, time_obj: time) -> pd.Timestamp:
) )
def cal_sam_minute(x: pd.Timestamp, sam_minutes: int) -> pd.Timestamp: def cal_sam_minute(x: pd.Timestamp, sam_minutes: int, region: str = REG_CN) -> pd.Timestamp:
""" """
align the minute-level data to a down sampled calendar align the minute-level data to a down sampled calendar
@@ -294,13 +332,15 @@ def cal_sam_minute(x: pd.Timestamp, sam_minutes: int) -> pd.Timestamp:
datetime to be aligned datetime to be aligned
sam_minutes : int sam_minutes : int
align to `sam_minutes` minute-level calendar align to `sam_minutes` minute-level calendar
region: str
Region, for example, "cn", "us"
Returns Returns
------- -------
pd.Timestamp: pd.Timestamp:
the datetime after aligned the datetime after aligned
""" """
cal = get_min_cal(C.min_data_shift)[::sam_minutes] cal = get_min_cal(C.min_data_shift, region)[::sam_minutes]
idx = bisect.bisect_right(cal, x.time()) - 1 idx = bisect.bisect_right(cal, x.time()) - 1
_date, new_time = x.date(), cal[idx] _date, new_time = x.date(), cal[idx]
return concat_date_time(_date, new_time) return concat_date_time(_date, new_time)
@@ -334,4 +374,4 @@ def epsilon_change(date_time: pd.Timestamp, direction: str = "backward") -> pd.T
if __name__ == "__main__": if __name__ == "__main__":
print(get_day_min_idx_range("8:30", "14:59", "10min")) print(get_day_min_idx_range("8:30", "14:59", "10min", REG_CN))

View File

@@ -7,7 +7,6 @@ from pathlib import Path
import qlib import qlib
import fire import fire
import pandas as pd
import ruamel.yaml as yaml import ruamel.yaml as yaml
from qlib.config import C from qlib.config import C
from qlib.model.trainer import task_train from qlib.model.trainer import task_train
@@ -44,16 +43,26 @@ def sys_config(config, config_path):
# workflow handler function # workflow handler function
def workflow(config_path, experiment_name="workflow", uri_folder="mlruns"): def workflow(config_path, experiment_name="workflow", uri_folder="mlruns"):
"""
This is a Qlib CLI entrance.
User can run the whole Quant research workflow defined by a configure file
- the code is located here ``qlib/workflow/cli.py`
"""
with open(config_path) as fp: with open(config_path) as fp:
config = yaml.safe_load(fp) config = yaml.safe_load(fp)
# config the `sys` section # config the `sys` section
sys_config(config, config_path) sys_config(config, config_path)
exp_manager = C["exp_manager"] if "exp_manager" in config.get("qlib_init"):
exp_manager["kwargs"]["uri"] = "file:" + str(Path(os.getcwd()).resolve() / uri_folder) qlib.init(**config.get("qlib_init"))
qlib.init(**config.get("qlib_init"), exp_manager=exp_manager) else:
exp_manager = C["exp_manager"]
exp_manager["kwargs"]["uri"] = "file:" + str(Path(os.getcwd()).resolve() / uri_folder)
qlib.init(**config.get("qlib_init"), exp_manager=exp_manager)
if "experiment_name" in config:
experiment_name = config["experiment_name"]
recorder = task_train(config.get("task"), experiment_name=experiment_name) recorder = task_train(config.get("task"), experiment_name=experiment_name)
recorder.save_objects(config=config) recorder.save_objects(config=config)

View File

@@ -170,12 +170,9 @@ class ExpManager:
experiment_name = self._default_exp_name experiment_name = self._default_exp_name
if create: if create:
exp, is_new = self._get_or_create_exp(experiment_id=experiment_id, experiment_name=experiment_name) exp, _ = self._get_or_create_exp(experiment_id=experiment_id, experiment_name=experiment_name)
else: else:
exp, is_new = ( exp = self._get_exp(experiment_id=experiment_id, experiment_name=experiment_name)
self._get_exp(experiment_id=experiment_id, experiment_name=experiment_name),
False,
)
if self.active_experiment is None and start: if self.active_experiment is None and start:
self.active_experiment = exp self.active_experiment = exp
# start the recorder # start the recorder
@@ -201,7 +198,7 @@ class ExpManager:
# So we supported it in the interface wrapper # So we supported it in the interface wrapper
pr = urlparse(self.uri) pr = urlparse(self.uri)
if pr.scheme == "file": if pr.scheme == "file":
with FileLock(os.path.join(pr.netloc, pr.path, "filelock")) as f: # pylint: disable=E0110 with FileLock(os.path.join(pr.netloc, pr.path, "filelock")): # pylint: disable=E0110
return self.create_exp(experiment_name), True return self.create_exp(experiment_name), True
# NOTE: for other schemes like http, we double check to avoid create exp conflicts # NOTE: for other schemes like http, we double check to avoid create exp conflicts
try: try:

View File

@@ -77,7 +77,6 @@ class RecordUpdater(metaclass=ABCMeta):
""" """
Update info for specific recorder Update info for specific recorder
""" """
...
class DSBasedUpdater(RecordUpdater, metaclass=ABCMeta): class DSBasedUpdater(RecordUpdater, metaclass=ABCMeta):
@@ -248,7 +247,6 @@ class DSBasedUpdater(RecordUpdater, metaclass=ABCMeta):
- `update_date` only include some data specific feature - `update_date` only include some data specific feature
- `update` include some general routine steps(e.g. prepare dataset, checking) - `update` include some general routine steps(e.g. prepare dataset, checking)
""" """
...
def _replace_range(data, new_data): def _replace_range(data, new_data):

View File

@@ -96,11 +96,12 @@ class RecordTemp:
""" """
try: try:
return self.recorder.load_object(self.get_path(name)) return self.recorder.load_object(self.get_path(name))
except LoadObjectError: except LoadObjectError as e:
if parents: if parents:
if self.depend_cls is not None: if self.depend_cls is not None:
with class_casting(self, self.depend_cls): with class_casting(self, self.depend_cls):
return self.load(name, parents=True) return self.load(name, parents=True)
raise e
def list(self): def list(self):
""" """
@@ -145,7 +146,7 @@ class RecordTemp:
for item in self.list(): for item in self.list():
ps = self.get_path(item).split("/") ps = self.get_path(item).split("/")
dirn, fn = "/".join(ps[:-1]), ps[-1] dirn = "/".join(ps[:-1])
if self.get_path(item) not in _get_arts(dirn): if self.get_path(item) not in _get_arts(dirn):
raise FileNotFoundError raise FileNotFoundError
if parents: if parents:

View File

@@ -347,18 +347,20 @@ class MLflowRecorder(Recorder):
""" """
assert self.uri is not None, "Please start the experiment and recorder first before using recorder directly." assert self.uri is not None, "Please start the experiment and recorder first before using recorder directly."
path = None
try: try:
path = self.client.download_artifacts(self.id, name) path = self.client.download_artifacts(self.id, name)
with Path(path).open("rb") as f: with Path(path).open("rb") as f:
data = unpickler(f).load() data = unpickler(f).load()
ar = self.client._tracking_client._get_artifact_repo(self.id)
if isinstance(ar, AzureBlobArtifactRepository):
# for saving disk space
# For safety, only remove redundant file for specific ArtifactRepository
shutil.rmtree(Path(path).absolute().parent)
return data return data
except Exception as e: except Exception as e:
raise LoadObjectError(str(e)) from e raise LoadObjectError(str(e)) from e
finally:
ar = self.client._tracking_client._get_artifact_repo(self.id)
if isinstance(ar, AzureBlobArtifactRepository) and path is not None:
# for saving disk space
# For safety, only remove redundant file for specific ArtifactRepository
shutil.rmtree(Path(path).absolute().parent)
@AsyncCaller.async_dec(ac_attr="async_log") @AsyncCaller.async_dec(ac_attr="async_log")
def log_params(self, **kwargs): def log_params(self, **kwargs):

View File

@@ -10,6 +10,7 @@ from qlib.log import TimeInspector
from typing import Callable, Dict, Iterable, List from typing import Callable, Dict, Iterable, List
from qlib.log import get_module_logger from qlib.log import get_module_logger
from qlib.utils.serial import Serializable from qlib.utils.serial import Serializable
from qlib.utils.exceptions import LoadObjectError
from qlib.workflow import R from qlib.workflow import R
from qlib.workflow.exp import Experiment from qlib.workflow.exp import Experiment
from qlib.workflow.recorder import Recorder from qlib.workflow.recorder import Recorder
@@ -228,9 +229,10 @@ class RecorderCollector(Collector):
else: else:
try: try:
artifact = rec.load_object(self.artifacts_path[key]) artifact = rec.load_object(self.artifacts_path[key])
except Exception as e: except LoadObjectError as e:
if only_exist: if only_exist:
# only collect existing artifact # only collect existing artifact
logger.warning(f"Fail to load {self.artifacts_path[key]} and it is ignored.")
continue continue
raise e raise e
# give user some warning if the values are overridden # give user some warning if the values are overridden

View File

@@ -170,7 +170,7 @@ class BaseCollector(abc.ABC):
df["symbol"] = symbol df["symbol"] = symbol
if instrument_path.exists(): if instrument_path.exists():
_old_df = pd.read_csv(instrument_path) _old_df = pd.read_csv(instrument_path)
df = _old_df.append(df, sort=False) df = pd.concat([_old_df, df], sort=False)
df.to_csv(instrument_path, index=False) df.to_csv(instrument_path, index=False)
def cache_small_data(self, symbol, df): def cache_small_data(self, symbol, df):
@@ -323,7 +323,7 @@ class BaseRun(abc.ABC):
freq, value from [1min, 1d], default 1d freq, value from [1min, 1d], default 1d
""" """
if source_dir is None: if source_dir is None:
source_dir = Path(self.default_base_dir).joinpath("_source") source_dir = Path(self.default_base_dir).joinpath("source")
self.source_dir = Path(source_dir).expanduser().resolve() self.source_dir = Path(source_dir).expanduser().resolve()
self.source_dir.mkdir(parents=True, exist_ok=True) self.source_dir.mkdir(parents=True, exist_ok=True)
@@ -359,6 +359,7 @@ class BaseRun(abc.ABC):
end=None, end=None,
check_data_length: int = None, check_data_length: int = None,
limit_nums=None, limit_nums=None,
**kwargs,
): ):
"""download data from Internet """download data from Internet
@@ -368,8 +369,6 @@ class BaseRun(abc.ABC):
default 2 default 2
delay: float delay: float
time.sleep(delay), default 0 time.sleep(delay), default 0
interval: str
freq, value from [1min, 1d], default 1d
start: str start: str
start datetime, default "2000-01-01" start datetime, default "2000-01-01"
end: str end: str
@@ -398,6 +397,7 @@ class BaseRun(abc.ABC):
interval=self.interval, interval=self.interval,
check_data_length=check_data_length, check_data_length=check_data_length,
limit_nums=limit_nums, limit_nums=limit_nums,
**kwargs,
).collector_data() ).collector_data()
def normalize_data(self, date_field_name: str = "date", symbol_field_name: str = "symbol", **kwargs): def normalize_data(self, date_field_name: str = "date", symbol_field_name: str = "symbol", **kwargs):

View File

@@ -0,0 +1,61 @@
# iBOVESPA History Companies Collection
## Requirements
- Install the libs from the file `requirements.txt`
```bash
pip install -r requirements.txt
```
- `requirements.txt` file was generated using python3.8
## For the ibovespa (IBOV) index, we have:
<hr/>
### Method `get_new_companies`
#### <b>Index start date</b>
- The ibovespa index started on 2 January 1968 ([wiki](https://en.wikipedia.org/wiki/%C3%8Dndice_Bovespa)). In order to use this start date in our `bench_start_date(self)` method, two conditions must be satisfied:
1) APIs used to download brazilian stocks (B3) historical prices must keep track of such historic data since 2 January 1968
2) Some website or API must provide, from that date, the historic index composition. In other words, the companies used to build the index .
As a consequence, the method `bench_start_date(self)` inside `collector.py` was implemented using `pd.Timestamp("2003-01-03")` due to two reasons
1) The earliest ibov composition that have been found was from the first quarter of 2003. More informations about such composition can be seen on the sections below.
2) Yahoo finance, one of the libraries used to download symbols historic prices, keeps track from this date forward.
- Within the `get_new_companies` method, a logic was implemented to get, for each ibovespa component stock, the start date that yahoo finance keeps track of.
#### <b>Code Logic</b>
The code does a web scrapping into the B3's [website](https://sistemaswebb3-listados.b3.com.br/indexPage/day/IBOV?language=pt-br), which keeps track of the ibovespa stocks composition on the current day.
Other approaches, such as `request` and `Beautiful Soup` could have been used. However, the website shows the table with the stocks with some delay, since it uses a script inside of it to obtain such compositions.
Alternatively, `selenium` was used to download this stocks' composition in order to overcome this problem.
Futhermore, the data downloaded from the selenium script was preprocessed so it could be saved into the `csv` format stablished by `scripts/data_collector/index.py`.
<hr/>
### Method `get_changes`
No suitable data source that keeps track of ibovespa's history stocks composition has been found. Except from this [repository](https://github.com/igor17400/IBOV-HCI) which provide such information have been used, however it only provides the data from the 1st quarter of 2003 to 3rd quarter of 2021.
With that reference, the index's composition can be compared quarter by quarter and year by year and then generate a file that keeps track of which stocks have been removed and which have been added each quarter and year.
<hr/>
### Collector Data
```bash
# parse instruments, using in qlib/instruments.
python collector.py --index_name IBOV --qlib_dir ~/.qlib/qlib_data/br_data --method parse_instruments
# parse new companies
python collector.py --index_name IBOV --qlib_dir ~/.qlib/qlib_data/br_data --method save_new_companies
```

View File

@@ -0,0 +1,277 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
from functools import partial
import sys
from pathlib import Path
import importlib
import datetime
import fire
import pandas as pd
from tqdm import tqdm
from loguru import logger
CUR_DIR = Path(__file__).resolve().parent
sys.path.append(str(CUR_DIR.parent.parent))
from data_collector.index import IndexBase
from data_collector.utils import get_instruments
quarter_dict = {"1Q": "01-03", "2Q": "05-01", "3Q": "09-01"}
class IBOVIndex(IndexBase):
ibov_index_composition = "https://raw.githubusercontent.com/igor17400/IBOV-HCI/main/historic_composition/{}.csv"
years_4_month_periods = []
def __init__(
self,
index_name: str,
qlib_dir: [str, Path] = None,
freq: str = "day",
request_retry: int = 5,
retry_sleep: int = 3,
):
super(IBOVIndex, self).__init__(
index_name=index_name, qlib_dir=qlib_dir, freq=freq, request_retry=request_retry, retry_sleep=retry_sleep
)
self.today: datetime = datetime.date.today()
self.current_4_month_period = self.get_current_4_month_period(self.today.month)
self.year = str(self.today.year)
self.years_4_month_periods = self.get_four_month_period()
@property
def bench_start_date(self) -> pd.Timestamp:
"""
The ibovespa index started on 2 January 1968 (wiki), however,
no suitable data source that keeps track of ibovespa's history
stocks composition has been found. Except from the repo indicated
in README. Which keeps track of such information starting from
the first quarter of 2003
"""
return pd.Timestamp("2003-01-03")
def get_current_4_month_period(self, current_month: int):
"""
This function is used to calculated what is the current
four month period for the current month. For example,
If the current month is August 8, its four month period
is 2Q.
OBS: In english Q is used to represent *quarter*
which means a three month period. However, in
portuguese we use Q to represent a four month period.
In other words,
Jan, Feb, Mar, Apr: 1Q
May, Jun, Jul, Aug: 2Q
Sep, Oct, Nov, Dez: 3Q
Parameters
----------
month : int
Current month (1 <= month <= 12)
Returns
-------
current_4m_period:str
Current Four Month Period (1Q or 2Q or 3Q)
"""
if current_month < 5:
return "1Q"
if current_month < 9:
return "2Q"
if current_month <= 12:
return "3Q"
else:
return -1
def get_four_month_period(self):
"""
The ibovespa index is updated every four months.
Therefore, we will represent each time period as 2003_1Q
which means 2003 first four mount period (Jan, Feb, Mar, Apr)
"""
four_months_period = ["1Q", "2Q", "3Q"]
init_year = 2003
now = datetime.datetime.now()
current_year = now.year
current_month = now.month
for year in [item for item in range(init_year, current_year)]:
for el in four_months_period:
self.years_4_month_periods.append(str(year)+"_"+el)
# For current year the logic must be a little different
current_4_month_period = self.get_current_4_month_period(current_month)
for i in range(int(current_4_month_period[0])):
self.years_4_month_periods.append(str(current_year) + "_" + str(i+1) + "Q")
return self.years_4_month_periods
def format_datetime(self, inst_df: pd.DataFrame) -> pd.DataFrame:
"""formatting the datetime in an instrument
Parameters
----------
inst_df: pd.DataFrame
inst_df.columns = [self.SYMBOL_FIELD_NAME, self.START_DATE_FIELD, self.END_DATE_FIELD]
Returns
-------
inst_df: pd.DataFrame
"""
logger.info("Formatting Datetime")
if self.freq != "day":
inst_df[self.END_DATE_FIELD] = inst_df[self.END_DATE_FIELD].apply(
lambda x: (pd.Timestamp(x) + pd.Timedelta(hours=23, minutes=59)).strftime("%Y-%m-%d %H:%M:%S")
)
else:
inst_df[self.START_DATE_FIELD] = inst_df[self.START_DATE_FIELD].apply(
lambda x: (pd.Timestamp(x)).strftime("%Y-%m-%d")
)
inst_df[self.END_DATE_FIELD] = inst_df[self.END_DATE_FIELD].apply(
lambda x: (pd.Timestamp(x)).strftime("%Y-%m-%d")
)
return inst_df
def format_quarter(self, cell: str):
"""
Parameters
----------
cell: str
It must be on the format 2003_1Q --> years_4_month_periods
Returns
----------
date: str
Returns date in format 2003-03-01
"""
cell_split = cell.split("_")
return cell_split[0] + "-" + quarter_dict[cell_split[1]]
def get_changes(self):
"""
Access the index historic composition and compare it quarter
by quarter and year by year in order to generate a file that
keeps track of which stocks have been removed and which have
been added.
The Dataframe used as reference will provided the index
composition for each year an quarter:
pd.DataFrame:
symbol
SH600000
SH600001
.
.
.
Parameters
----------
self: is used to represent the instance of the class.
Returns
----------
pd.DataFrame:
symbol date type
SH600000 2019-11-11 add
SH600001 2020-11-10 remove
dtypes:
symbol: str
date: pd.Timestamp
type: str, value from ["add", "remove"]
"""
logger.info("Getting companies changes in {} index ...".format(self.index_name))
try:
df_changes_list = []
for i in tqdm(range(len(self.years_4_month_periods) - 1)):
df = pd.read_csv(self.ibov_index_composition.format(self.years_4_month_periods[i]), on_bad_lines="skip")["symbol"]
df_ = pd.read_csv(self.ibov_index_composition.format(self.years_4_month_periods[i + 1]), on_bad_lines="skip")["symbol"]
## Remove Dataframe
remove_date = self.years_4_month_periods[i].split("_")[0] + "-" + quarter_dict[self.years_4_month_periods[i].split("_")[1]]
list_remove = list(df[~df.isin(df_)])
df_removed = pd.DataFrame(
{
"date": len(list_remove) * [remove_date],
"type": len(list_remove) * ["remove"],
"symbol": list_remove,
}
)
## Add Dataframe
add_date = self.years_4_month_periods[i + 1].split("_")[0] + "-" + quarter_dict[self.years_4_month_periods[i + 1].split("_")[1]]
list_add = list(df_[~df_.isin(df)])
df_added = pd.DataFrame(
{"date": len(list_add) * [add_date], "type": len(list_add) * ["add"], "symbol": list_add}
)
df_changes_list.append(pd.concat([df_added, df_removed], sort=False))
df = pd.concat(df_changes_list).reset_index(drop=True)
df["symbol"] = df["symbol"].astype(str) + ".SA"
return df
except Exception as E:
logger.error("An error occured while downloading 2008 index composition - {}".format(E))
def get_new_companies(self):
"""
Get latest index composition.
The repo indicated on README has implemented a script
to get the latest index composition from B3 website using
selenium. Therefore, this method will download the file
containing such composition
Parameters
----------
self: is used to represent the instance of the class.
Returns
----------
pd.DataFrame:
symbol start_date end_date
RRRP3 2020-11-13 2022-03-02
ALPA4 2008-01-02 2022-03-02
dtypes:
symbol: str
start_date: pd.Timestamp
end_date: pd.Timestamp
"""
logger.info("Getting new companies in {} index ...".format(self.index_name))
try:
## Get index composition
df_index = pd.read_csv(
self.ibov_index_composition.format(self.year + "_" + self.current_4_month_period), on_bad_lines="skip"
)
df_date_first_added = pd.read_csv(
self.ibov_index_composition.format("date_first_added_" + self.year + "_" + self.current_4_month_period),
on_bad_lines="skip",
)
df = df_index.merge(df_date_first_added, on="symbol")[["symbol", "Date First Added"]]
df[self.START_DATE_FIELD] = df["Date First Added"].map(self.format_quarter)
# end_date will be our current quarter + 1, since the IBOV index updates itself every quarter
df[self.END_DATE_FIELD] = self.year + "-" + quarter_dict[self.current_4_month_period]
df = df[["symbol", self.START_DATE_FIELD, self.END_DATE_FIELD]]
df["symbol"] = df["symbol"].astype(str) + ".SA"
return df
except Exception as E:
logger.error("An error occured while getting new companies - {}".format(E))
def filter_df(self, df: pd.DataFrame) -> pd.DataFrame:
if "Código" in df.columns:
return df.loc[:, ["Código"]].copy()
if __name__ == "__main__":
fire.Fire(partial(get_instruments, market_index="br_index" ))

View File

@@ -0,0 +1,34 @@
async-generator==1.10
attrs==21.4.0
certifi==2021.10.8
cffi==1.15.0
charset-normalizer==2.0.12
cryptography==36.0.1
fire==0.4.0
h11==0.13.0
idna==3.3
loguru==0.6.0
lxml==4.8.0
multitasking==0.0.10
numpy==1.22.2
outcome==1.1.0
pandas==1.4.1
pycoingecko==2.2.0
pycparser==2.21
pyOpenSSL==22.0.0
PySocks==1.7.1
python-dateutil==2.8.2
pytz==2021.3
requests==2.27.1
requests-futures==1.0.0
six==1.16.0
sniffio==1.2.0
sortedcontainers==2.4.0
termcolor==1.1.0
tqdm==4.63.0
trio==0.20.0
trio-websocket==0.9.2
urllib3==1.26.8
wget==3.2
wsproto==1.1.0
yahooquery==2.2.15

Some files were not shown because too many files have changed in this diff Show More