From 08de1a18740023858aed29271db8150eb2a2fde4 Mon Sep 17 00:00:00 2001 From: Linlang <30293408+SunsetWolf@users.noreply.github.com> Date: Sat, 22 Oct 2022 17:32:56 +0800 Subject: [PATCH 01/15] fix_CI_error (#1325) --- setup.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/setup.py b/setup.py index 4527cf91091..8ce3b93f6ec 100644 --- a/setup.py +++ b/setup.py @@ -154,6 +154,9 @@ def get_version(rel_path: str) -> str: "baostock", "yahooquery", "beautifulsoup4", + # The 5.0.0 version of importlib-metadata removed the deprecated endpoint, + # which prevented flake8 from working properly, so we restricted the version of importlib-metadata. + "importlib-metadata<5.0.0", "tianshou", "gym>=0.24", # If you do not put gym at the end, gym will degrade causing pytest results to fail. ], From fb5888be9ee976c3e3879e7298e3a0a1030db37e Mon Sep 17 00:00:00 2001 From: Chia-hung Tai Date: Sun, 30 Oct 2022 16:27:59 +0800 Subject: [PATCH 02/15] Use mock data for element operator tests. (#1330) --- qlib/tests/__init__.py | 216 +++++++++++++++++++++++++++++++- tests/ops/test_elem_operator.py | 40 +++++- 2 files changed, 253 insertions(+), 3 deletions(-) diff --git a/qlib/tests/__init__.py b/qlib/tests/__init__.py index a23092a2ef4..52c924918a3 100644 --- a/qlib/tests/__init__.py +++ b/qlib/tests/__init__.py @@ -1,10 +1,16 @@ +from typing import Union, List, Dict, Tuple import unittest +import pandas as pd +import numpy as np +import io + from .data import GetData from .. import init -from ..constant import REG_CN +from ..constant import REG_CN, REG_TW from qlib.data.filter import NameDFilter from qlib.data import D from qlib.data.data import Cal, DatasetD +from qlib.data.storage import CalendarStorage, InstrumentStorage, FeatureStorage, CalVT, InstKT, InstVT class TestAutoData(unittest.TestCase): @@ -75,3 +81,211 @@ def setUpClass(cls, enable_1d_type="simple", enable_1min=False) -> None: cls.end_time = cal[-1] cls.inst = list(instruments_d.keys())[0] cls.spans = list(instruments_d.values())[0] + + +MOCK_DATA = """ +id,symbol,datetime,interval,volume,open,high,low,close +20275,0050,2022-01-03 00:00:00,day,6761.0,146.0,147.35,146.0,146.4 +20276,0050,2022-01-04 00:00:00,day,9608.0,147.7,149.6,147.7,149.6 +20277,0050,2022-01-05 00:00:00,day,11387.0,150.1,150.55,149.1,149.3 +20278,0050,2022-01-06 00:00:00,day,8611.0,148.3,148.75,147.0,147.9 +20279,0050,2022-01-07 00:00:00,day,6954.0,148.3,149.0,146.5,146.6 +20280,0050,2022-01-10 00:00:00,day,15684.0,146.0,147.8,145.4,147.55 +20281,0050,2022-01-11 00:00:00,day,17741.0,147.6,148.5,146.7,148.3 +20282,0050,2022-01-12 00:00:00,day,10134.0,149.35,149.6,148.7,149.55 +20283,0050,2022-01-13 00:00:00,day,7431.0,149.55,150.45,149.55,150.3 +20284,0050,2022-01-14 00:00:00,day,10091.0,150.8,151.2,149.05,150.3 +20285,0050,2022-01-17 00:00:00,day,6899.0,151.1,152.4,151.1,152.0 +20286,0050,2022-01-18 00:00:00,day,14360.0,152.2,152.25,150.15,150.3 +20287,0050,2022-01-19 00:00:00,day,14654.0,149.0,149.65,148.25,148.5 +20288,0050,2022-01-20 00:00:00,day,16201.0,148.5,149.2,147.6,149.1 +20289,0050,2022-01-21 00:00:00,day,29848.0,143.9,143.95,142.3,142.65 +20290,0050,2022-01-24 00:00:00,day,13143.0,142.1,144.0,141.7,144.0 +20291,0050,2022-01-25 00:00:00,day,23982.0,142.55,142.55,141.25,141.65 +20292,0050,2022-01-26 00:00:00,day,17729.0,141.15,142.2,141.05,141.55 +8547,1101,2021-12-01 00:00:00,day,16119.0,46.0,46.85,46.0,46.6 +8548,1101,2021-12-02 00:00:00,day,14521.0,46.6,46.7,46.3,46.3 +8549,1101,2021-12-03 00:00:00,day,14357.0,46.55,46.85,46.4,46.4 +8550,1101,2021-12-06 00:00:00,day,15115.0,46.45,47.35,46.4,47.3 +8551,1101,2021-12-07 00:00:00,day,13117.0,47.35,47.55,46.9,47.55 +8552,1101,2021-12-08 00:00:00,day,10329.0,47.75,47.8,47.5,47.7 +8553,1101,2021-12-09 00:00:00,day,9300.0,47.8,47.85,47.1,47.4 +8554,1101,2021-12-10 00:00:00,day,9919.0,47.4,47.6,47.1,47.3 +8555,1101,2021-12-13 00:00:00,day,7784.0,47.3,47.75,47.1,47.1 +8556,1101,2021-12-14 00:00:00,day,9373.0,47.05,47.2,46.95,47.0 +8557,1101,2021-12-15 00:00:00,day,11189.0,47.0,47.3,46.8,46.95 +8558,1101,2021-12-16 00:00:00,day,7516.0,47.0,47.15,46.8,46.9 +8559,1101,2021-12-17 00:00:00,day,18502.0,46.95,47.6,46.9,47.45 +8560,1101,2021-12-20 00:00:00,day,11309.0,47.45,47.5,47.1,47.4 +8561,1101,2021-12-21 00:00:00,day,5666.0,47.4,47.45,47.1,47.25 +8562,1101,2021-12-22 00:00:00,day,5460.0,47.4,47.45,47.2,47.4 +8563,1101,2021-12-23 00:00:00,day,9371.0,47.3,47.7,47.3,47.7 +8564,1101,2021-12-24 00:00:00,day,5980.0,47.75,47.95,47.75,47.9 +8565,1101,2021-12-27 00:00:00,day,5709.0,47.9,48.1,47.9,48.1 +8566,1101,2021-12-28 00:00:00,day,7777.0,48.1,48.15,47.95,48.15 +8567,1101,2021-12-29 00:00:00,day,5309.0,48.15,48.25,48.05,48.15 +8568,1101,2021-12-30 00:00:00,day,4616.0,48.15,48.2,48.0,48.0 +8569,1101,2022-01-03 00:00:00,day,12350.0,48.05,48.15,47.35,47.45 +8570,1101,2022-01-04 00:00:00,day,11439.0,47.5,47.6,47.0,47.3 +8571,1101,2022-01-05 00:00:00,day,9692.0,47.1,47.3,47.0,47.15 +8572,1101,2022-01-06 00:00:00,day,12361.0,47.3,47.6,47.15,47.6 +8573,1101,2022-01-07 00:00:00,day,10921.0,47.6,47.65,47.2,47.45 +8574,1101,2022-01-10 00:00:00,day,11925.0,47.45,47.5,47.0,47.3 +8575,1101,2022-01-11 00:00:00,day,11047.0,47.1,47.5,47.1,47.5 +8576,1101,2022-01-12 00:00:00,day,10817.0,47.5,47.5,47.1,47.5 +8577,1101,2022-01-13 00:00:00,day,13849.0,47.5,47.95,47.4,47.95 +8578,1101,2022-01-14 00:00:00,day,9460.0,47.85,47.85,47.45,47.6 +8579,1101,2022-01-17 00:00:00,day,9057.0,47.55,47.7,47.35,47.6 +8580,1101,2022-01-18 00:00:00,day,8089.0,47.6,47.75,47.45,47.75 +8581,1101,2022-01-19 00:00:00,day,5110.0,47.6,47.7,47.5,47.6 +8582,1101,2022-01-20 00:00:00,day,6327.0,47.55,47.7,47.45,47.5 +8583,1101,2022-01-21 00:00:00,day,9470.0,47.5,47.65,47.15,47.4 +8584,1101,2022-01-24 00:00:00,day,5475.0,47.1,47.3,47.0,47.15 +8585,1101,2022-01-25 00:00:00,day,16153.0,47.0,47.05,46.6,46.8 +8586,1101,2022-01-26 00:00:00,day,7772.0,46.7,47.0,46.55,46.85 +8587,1101,2022-02-07 00:00:00,day,17031.0,46.55,47.1,46.0,47.1 +8588,1101,2022-02-08 00:00:00,day,9741.0,47.1,47.25,46.9,46.95 +8589,1101,2022-02-09 00:00:00,day,7968.0,46.95,47.3,46.9,47.3 +8590,1101,2022-02-10 00:00:00,day,7479.0,47.15,47.55,47.05,47.55 +8591,1101,2022-02-11 00:00:00,day,6841.0,47.3,47.55,47.15,47.55 +8592,1101,2022-02-14 00:00:00,day,9136.0,47.2,47.3,46.95,47.15 +8593,1101,2022-02-15 00:00:00,day,5444.0,47.05,47.1,46.8,47.0 +8594,1101,2022-02-16 00:00:00,day,8751.0,47.0,47.15,47.0,47.0 +8595,1101,2022-02-17 00:00:00,day,10662.0,47.15,47.55,47.1,47.45 +8596,1101,2022-02-18 00:00:00,day,8781.0,47.25,47.55,47.2,47.45 +8597,1101,2022-02-21 00:00:00,day,8201.0,47.35,47.75,47.15,47.6 +8598,1101,2022-02-22 00:00:00,day,10655.0,47.4,47.7,47.1,47.7 +8599,1101,2022-02-23 00:00:00,day,8040.0,47.7,47.85,47.45,47.65 +8600,1101,2022-02-24 00:00:00,day,13124.0,47.5,47.5,47.1,47.3 +8601,1101,2022-02-25 00:00:00,day,14556.0,47.2,47.5,46.9,47.35 +""" + +MOCK_DF = pd.read_csv(io.StringIO(MOCK_DATA), header=0, dtype={"symbol": str}) + + +class MockStorageBase: + def __init__(self, **kwargs): + self.df = MOCK_DF + + +class MockCalendarStorage(MockStorageBase, CalendarStorage): + def __init__(self, **kwargs): + super().__init__() + self._data = sorted(self.df["datetime"].unique()) + + @property + def data(self) -> List[CalVT]: + return self._data + + def __getitem__(self, i: Union[int, slice]) -> Union[CalVT, List[CalVT]]: + return self.data[i] + + def __len__(self) -> int: + return len(self.data) + + +class MockInstrumentStorage(MockStorageBase, InstrumentStorage): + def __init__(self, **kwargs): + super().__init__() + instruments = {} + for symbol, group in self.df.groupby(by="symbol"): + start = group["datetime"].iloc[0] + end = group["datetime"].iloc[-1] + instruments[symbol] = [(start, end)] + self._data = instruments + + @property + def data(self) -> Dict[InstKT, InstVT]: + return self._data + + def __getitem__(self, k: InstKT) -> InstVT: + return self.data[k] + + def __len__(self) -> int: + return len(self.data) + + +class MockFeatureStorage(MockStorageBase, FeatureStorage): + def __init__(self, instrument: str, field: str, freq: str, db_region: str = None, **kwargs): # type: ignore + super().__init__(instrument=instrument, field=field, freq=freq, db_region=db_region, **kwargs) + self.field = field + calendar = sorted(self.df["datetime"].unique()) + df_calendar = pd.DataFrame(calendar, columns=["datetime"]).set_index("datetime") + df = self.df[self.df["symbol"] == instrument] + data_dt_field = "datetime" + cal_df = df_calendar[ + (df_calendar.index >= df[data_dt_field].min()) & (df_calendar.index <= df[data_dt_field].max()) + ] + df = df.set_index(data_dt_field) + df_data = df.reindex(cal_df.index) + date_index = df_calendar.index.get_loc(df_data.index.min()) # type: ignore + df_data.reset_index(inplace=True) + df_data.index += date_index + self._data = df_data + + @property + def data(self) -> pd.Series: + return self._data[self.field] + + @property + def start_index(self) -> Union[int, None]: + if self._data.empty: + return None + return self._data.index[0] + + @property + def end_index(self) -> Union[int, None]: + if self._data.empty: + return None + # The next data appending index point will be `end_index + 1` + return self._data.index[-1] + + def __getitem__(self, i: Union[int, slice]) -> Union[Tuple[int, float], pd.Series]: + df = self._data + storage_start_index = df.index[0] + storage_end_index = df.index[-1] + if isinstance(i, int): + if storage_start_index > i or i > storage_end_index: + raise IndexError(f"{i}: start index is {storage_start_index}") + data = self.data[i] + return i, data + elif isinstance(i, slice): + start_index = storage_start_index if i.start is None else i.start + end_index = storage_end_index if i.stop is None else i.stop + si = max(start_index, storage_start_index) + if si > end_index or self.field not in df.columns: + return pd.Series(dtype=np.float32) # type: ignore + data = df[self.field].tolist() + result = data[si - storage_start_index : end_index - storage_start_index] + return pd.Series(result, index=pd.RangeIndex(si, si + len(result))) # type: ignore + else: + raise TypeError(f"type(i) = {type(i)}") + + def __len__(self) -> int: + return len(self.data) + + +class TestMockData(unittest.TestCase): + _setup_kwargs = { + "calendar_provider": { + "class": "LocalCalendarProvider", + "module_path": "qlib.data.data", + "kwargs": {"backend": {"class": "MockCalendarStorage", "module_path": "qlib.tests"}}, + }, + "instrument_provider": { + "class": "LocalInstrumentProvider", + "module_path": "qlib.data.data", + "kwargs": {"backend": {"class": "MockInstrumentStorage", "module_path": "qlib.tests"}}, + }, + "feature_provider": { + "class": "LocalFeatureProvider", + "module_path": "qlib.data.data", + "kwargs": {"backend": {"class": "MockFeatureStorage", "module_path": "qlib.tests"}}, + }, + } + + @classmethod + def setUpClass(cls) -> None: + + provider_uri = "Not necessary." + init(region=REG_TW, provider_uri=provider_uri, expression_cache=None, dataset_cache=None, **cls._setup_kwargs) diff --git a/tests/ops/test_elem_operator.py b/tests/ops/test_elem_operator.py index e641b1ac2e0..8349157ff44 100644 --- a/tests/ops/test_elem_operator.py +++ b/tests/ops/test_elem_operator.py @@ -1,17 +1,52 @@ import unittest +import numpy as np +import pytest from qlib.data import DatasetProvider -from qlib.tests import TestOperatorData +from qlib.data.data import ExpressionD +from qlib.tests import TestOperatorData, TestMockData, MOCK_DF from qlib.config import C +class TestElementOperator(TestMockData): + def setUp(self) -> None: + self.instrument = "0050" + self.start_time = "2022-01-01" + self.end_time = "2022-02-01" + self.freq = "day" + self.mock_df = MOCK_DF[MOCK_DF["symbol"] == self.instrument] + + def test_Abs(self): + field = "Abs($close-Ref($close, 1))" + result = ExpressionD.expression(self.instrument, field, self.start_time, self.end_time, self.freq) + self.assertGreaterEqual(result.min(), 0) + result = result.to_numpy() + prev_close = self.mock_df["close"].shift(1) + close = self.mock_df["close"] + change = prev_close - close + golden = change.abs().to_numpy() + self.assertIsNone(np.testing.assert_allclose(result, golden)) + + def test_Sign(self): + field = "Sign($close-Ref($close, 1))" + result = ExpressionD.expression(self.instrument, field, self.start_time, self.end_time, self.freq) + result = result.to_numpy() + prev_close = self.mock_df["close"].shift(1) + close = self.mock_df["close"] + change = close - prev_close + change[change > 0] = 1.0 + change[change < 0] = -1.0 + golden = change.to_numpy() + self.assertIsNone(np.testing.assert_allclose(result, golden)) + + class TestOperatorDataSetting(TestOperatorData): def test_setting(self): self.assertEqual(len(self.instruments_d), 1) self.assertGreater(len(self.cal), 0) -class TestElementOperator(TestOperatorData): +class TestInstElementOperator(TestOperatorData): def setUp(self) -> None: freq = "day" expressions = [ @@ -24,6 +59,7 @@ def setUp(self) -> None: ) self.data.columns = columns + @pytest.mark.slow def test_abs(self): abs_values = self.data["abs"] self.assertGreater(abs_values[2], 0) From 67d618d4b2b5272f693db219e7a6d1e1d10343e5 Mon Sep 17 00:00:00 2001 From: wony <5218603+wony-zheng@users.noreply.github.com> Date: Thu, 3 Nov 2022 17:08:22 +0800 Subject: [PATCH 03/15] Update cache.py (#1329) make D.feature([symbol], [Feature('close')], disk_cache=1) work correctly --- qlib/data/cache.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qlib/data/cache.py b/qlib/data/cache.py index fc53aab3d34..7c692377ad6 100644 --- a/qlib/data/cache.py +++ b/qlib/data/cache.py @@ -471,7 +471,7 @@ def cache_to_origin_data(data, fields): not_space_fields = remove_fields_space(fields) data = data.loc[:, not_space_fields] # set features fields - data.columns = list(fields) + data.columns = [str(i) for i in fields] return data @staticmethod From 2fae407b1924ebfe832018bfc74139091c0a438c Mon Sep 17 00:00:00 2001 From: lerit Date: Fri, 4 Nov 2022 21:15:23 +0800 Subject: [PATCH 04/15] Update dump_bin.py (#1273) dump_fix data that not in calendar_list, throw error: ``` NaT is not in list ``` --- scripts/dump_bin.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/scripts/dump_bin.py b/scripts/dump_bin.py index dbee65446a2..269366f75ea 100644 --- a/scripts/dump_bin.py +++ b/scripts/dump_bin.py @@ -219,6 +219,9 @@ def _data_to_bin(self, df: pd.DataFrame, calendar_list: List[pd.Timestamp], feat return # align index _df = self.data_merge_calendar(df, calendar_list) + if _df.empty: + logger.warning(f"{features_dir.name} data is not in calendars") + return # used when creating a bin file date_index = self.get_datetime_index(_df, calendar_list) for field in self.get_dump_fields(_df.columns): From 94e420f755f534650e23366af3f6bea0eb98b63b Mon Sep 17 00:00:00 2001 From: qianyun210603 Date: Mon, 7 Nov 2022 23:37:18 +0800 Subject: [PATCH 05/15] Correct errors and typos in doc strings (#1338) * add missing parameters to doc string in order_generate * fix some typos in doc strings * reformat base on code style standard * Update qlib/backtest/__init__.py * Update examples/run_all_model.py * Update examples/run_all_model.py Co-authored-by: you-n-g --- examples/run_all_model.py | 2 +- qlib/backtest/__init__.py | 2 +- qlib/backtest/account.py | 2 +- qlib/contrib/strategy/order_generator.py | 48 ++++++++++++++++-------- 4 files changed, 35 insertions(+), 19 deletions(-) diff --git a/examples/run_all_model.py b/examples/run_all_model.py index 71589049a2a..dda3b98f622 100644 --- a/examples/run_all_model.py +++ b/examples/run_all_model.py @@ -253,7 +253,7 @@ def run( default "" indicates that qlib_uri : str the uri to install qlib with pip - it could be url on the we or local path (NOTE: the local path must be a absolute path) + it could be URI on the remote or local path (NOTE: the local path must be an absolute path) exp_folder_name: str the name of the experiment folder wait_before_rm_env : bool diff --git a/qlib/backtest/__init__.py b/qlib/backtest/__init__.py index 81c6437d6d2..aca45f4e1eb 100644 --- a/qlib/backtest/__init__.py +++ b/qlib/backtest/__init__.py @@ -244,7 +244,7 @@ def backtest( benchmark: str the benchmark for reporting. account : Union[float, int, Position] - information for describing how to creating the account + information for describing how to create the account For `float` or `int`: Using Account with only initial cash For `Position`: diff --git a/qlib/backtest/account.py b/qlib/backtest/account.py index 6054ac638b6..9d60ff09242 100644 --- a/qlib/backtest/account.py +++ b/qlib/backtest/account.py @@ -236,7 +236,7 @@ def update_current_position( if not self.current_position.skip_update(): stock_list = self.current_position.get_stock_list() for code in stock_list: - # if suspend, no new price to be updated, profit is 0 + # if suspended, no new price to be updated, profit is 0 if trade_exchange.check_stock_suspended(code, trade_start_time, trade_end_time): continue bar_close = cast(float, trade_exchange.get_close(code, trade_start_time, trade_end_time)) diff --git a/qlib/contrib/strategy/order_generator.py b/qlib/contrib/strategy/order_generator.py index 9e84d504660..fe0d048bfdd 100644 --- a/qlib/contrib/strategy/order_generator.py +++ b/qlib/contrib/strategy/order_generator.py @@ -33,10 +33,14 @@ def generate_order_list_from_target_weight_position( :type target_weight_position: dict :param risk_degree: :type risk_degree: float - :param pred_date: the date the score is predicted - :type pred_date: pd.Timestamp - :param trade_date: the date the stock is traded - :type trade_date: pd.Timestamp + :param pred_start_time: + :type pred_start_time: pd.Timestamp + :param pred_end_time: + :type pred_end_time: pd.Timestamp + :param trade_start_time: + :type trade_start_time: pd.Timestamp + :param trade_end_time: + :type trade_end_time: pd.Timestamp :rtype: list """ @@ -72,10 +76,14 @@ def generate_order_list_from_target_weight_position( :type target_weight_position: dict :param risk_degree: :type risk_degree: float - :param pred_date: - :type pred_date: pd.Timestamp - :param trade_date: - :type trade_date: pd.Timestamp + :param pred_start_time: + :type pred_start_time: pd.Timestamp + :param pred_end_time: + :type pred_end_time: pd.Timestamp + :param trade_start_time: + :type trade_start_time: pd.Timestamp + :param trade_end_time: + :type trade_end_time: pd.Timestamp :rtype: list """ @@ -147,9 +155,12 @@ def generate_order_list_from_target_weight_position( ) -> list: """generate_order_list_from_target_weight_position - generate order list directly not using the information (e.g. whether can be traded, the accurate trade price) at trade date. - In target weight position, generating order list need to know the price of objective stock in trade date, but we cannot get that - value when do not interact with exchange, so we check the %close price at pred_date or price recorded in current position. + generate order list directly not using the information (e.g. whether can be traded, the accurate trade price) + at trade date. + In target weight position, generating order list need to know the price of objective stock in trade date, + but we cannot get that + value when do not interact with exchange, so we check the %close price at pred_date or price recorded + in current position. :param current: :type current: Position @@ -159,10 +170,14 @@ def generate_order_list_from_target_weight_position( :type target_weight_position: dict :param risk_degree: :type risk_degree: float - :param pred_date: - :type pred_date: pd.Timestamp - :param trade_date: - :type trade_date: pd.Timestamp + :param pred_start_time: + :type pred_start_time: pd.Timestamp + :param pred_end_time: + :type pred_end_time: pd.Timestamp + :param trade_start_time: + :type trade_start_time: pd.Timestamp + :param trade_end_time: + :type trade_end_time: pd.Timestamp :rtype: list of generated orders """ @@ -185,7 +200,8 @@ def generate_order_list_from_target_weight_position( * target_weight_position[stock_id] / trade_exchange.get_close(stock_id, start_time=pred_start_time, end_time=pred_end_time) ) - # TODO: Qlib use None to represent trading suspension. So last close price can't be the estimated trading price. + # TODO: Qlib use None to represent trading suspension. + # So last close price can't be the estimated trading price. # Maybe a close price with forward fill will be a better solution. elif stock_id in current_stock: amount_dict[stock_id] = ( From 59fbf23a7148107cd26e73fb9815f644a791d9ee Mon Sep 17 00:00:00 2001 From: lerit Date: Tue, 8 Nov 2022 10:51:43 +0800 Subject: [PATCH 06/15] fix position access error (#1267) * fix position access error position is s sub attribute of _value error since commit(id:89972f6c6f9fa629b4f74093d4ba1e93c9f7a5e5) * lint with blank --- qlib/contrib/report/analysis_position/parse_position.py | 1 + 1 file changed, 1 insertion(+) diff --git a/qlib/contrib/report/analysis_position/parse_position.py b/qlib/contrib/report/analysis_position/parse_position.py index 1a89862f3dd..61064d3e6af 100644 --- a/qlib/contrib/report/analysis_position/parse_position.py +++ b/qlib/contrib/report/analysis_position/parse_position.py @@ -39,6 +39,7 @@ def parse_position(position: dict = None) -> pd.DataFrame: result_df = pd.DataFrame() for _trading_date, _value in position.items(): + _value = _value.position # pd_date type: pd.Timestamp _cash = _value.pop("cash") for _item in ["now_account_value"]: From 49a5bccfec3c1b7bb91b56e67a02ab56b212b88c Mon Sep 17 00:00:00 2001 From: Jinge Wang Date: Thu, 10 Nov 2022 09:20:33 +0800 Subject: [PATCH 07/15] Don't disable existing logger when initializing qlib. (#1339) * Don't disable existing logger when initializing qlib. * Add comma in the end of the config line. * Add comment to the added config. Co-authored-by: Jinge Wang --- qlib/config.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/qlib/config.py b/qlib/config.py index 8fc7d778d7e..4b4123643ce 100644 --- a/qlib/config.py +++ b/qlib/config.py @@ -172,6 +172,9 @@ def register_from_C(self, config, skip_register=True): } }, "loggers": {"qlib": {"level": logging.DEBUG, "handlers": ["console"]}}, + # To let qlib work with other packages, we shouldn't disable existing loggers. + # Note that this param is default to True according to the documentation of logging. + "disable_existing_loggers": False, }, # Default config for experiment manager "exp_manager": { From 35794846ffe0da3426d9620571732fd9f35ab881 Mon Sep 17 00:00:00 2001 From: Huoran Li Date: Thu, 10 Nov 2022 21:10:11 +0800 Subject: [PATCH 08/15] Refine RL todos (#1332) * Refine several todos * CI issues * Remove Dropna limitation of `quote_df` in Exchange (#1334) * Remove Dropna limitation of `quote_df` of Exchange * Impreove docstring * Fix type error when expression is specified (#1335) * Refine fill_missing_data() * Remove several TODO comments * Add back env for interpreters * Change Literal import * Resolve PR comments * Move to SAOEState * Add Trainer.get_policy_state_dict() * Mypy issue Co-authored-by: you-n-g --- qlib/backtest/__init__.py | 12 +- qlib/backtest/backtest.py | 46 +-- qlib/backtest/exchange.py | 63 +++- qlib/rl/contrib/backtest.py | 88 +++--- qlib/rl/contrib/naive_config_parser.py | 8 +- qlib/rl/data/native.py | 13 +- qlib/rl/interpreter.py | 14 +- qlib/rl/order_execution/interpreter.py | 39 +-- qlib/rl/order_execution/policy.py | 16 +- qlib/rl/order_execution/simulator_qlib.py | 23 +- qlib/rl/order_execution/simulator_simple.py | 5 +- qlib/rl/order_execution/state.py | 283 +----------------- qlib/rl/order_execution/strategy.py | 309 ++++++++++++++++++-- qlib/rl/trainer/trainer.py | 9 +- qlib/rl/utils/env_wrapper.py | 37 +-- qlib/workflow/record_temp.py | 5 +- tests/backtest/test_file_strategy.py | 2 +- tests/backtest/test_high_freq_trading.py | 2 +- tests/rl/test_qlib_simulator.py | 6 +- tests/rl/test_saoe_simple.py | 11 - 20 files changed, 461 insertions(+), 530 deletions(-) diff --git a/qlib/backtest/__init__.py b/qlib/backtest/__init__.py index aca45f4e1eb..ec0725230a1 100644 --- a/qlib/backtest/__init__.py +++ b/qlib/backtest/__init__.py @@ -10,7 +10,6 @@ import pandas as pd from .account import Account -from .report import Indicator, PortfolioMetrics if TYPE_CHECKING: from ..strategy.base import BaseStrategy @@ -20,7 +19,7 @@ from ..config import C from ..log import get_module_logger from ..utils import init_instance_by_config -from .backtest import backtest_loop, collect_data_loop +from .backtest import INDICATOR_METRIC, PORT_METRIC, backtest_loop, collect_data_loop from .decision import Order from .exchange import Exchange from .utils import CommonInfrastructure @@ -223,7 +222,7 @@ def backtest( account: Union[float, int, dict] = 1e9, exchange_kwargs: dict = {}, pos_type: str = "Position", -) -> Tuple[PortfolioMetrics, Indicator]: +) -> Tuple[PORT_METRIC, INDICATOR_METRIC]: """initialize the strategy and executor, then backtest function for the interaction of the outermost strategy and executor in the nested decision execution @@ -256,9 +255,9 @@ def backtest( Returns ------- - portfolio_metrics_dict: Dict[PortfolioMetrics] + portfolio_dict: PORT_METRIC it records the trading portfolio_metrics information - indicator_dict: Dict[Indicator] + indicator_dict: INDICATOR_METRIC it computes the trading indicator It is organized in a dict format @@ -273,8 +272,7 @@ def backtest( exchange_kwargs, pos_type=pos_type, ) - portfolio_metrics, indicator = backtest_loop(start_time, end_time, trade_strategy, trade_executor) - return portfolio_metrics, indicator + return backtest_loop(start_time, end_time, trade_strategy, trade_executor) def collect_data( diff --git a/qlib/backtest/backtest.py b/qlib/backtest/backtest.py index f79622bff63..cf0a3a57861 100644 --- a/qlib/backtest/backtest.py +++ b/qlib/backtest/backtest.py @@ -3,12 +3,12 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Generator, Optional, Tuple, Union, cast +from typing import Dict, TYPE_CHECKING, Generator, Optional, Tuple, Union, cast import pandas as pd from qlib.backtest.decision import BaseTradeDecision -from qlib.backtest.report import Indicator, PortfolioMetrics +from qlib.backtest.report import Indicator if TYPE_CHECKING: from qlib.strategy.base import BaseStrategy @@ -19,30 +19,35 @@ from ..utils.time import Freq +PORT_METRIC = Dict[str, Tuple[pd.DataFrame, dict]] +INDICATOR_METRIC = Dict[str, Tuple[pd.DataFrame, Indicator]] + + def backtest_loop( start_time: Union[pd.Timestamp, str], end_time: Union[pd.Timestamp, str], trade_strategy: BaseStrategy, trade_executor: BaseExecutor, -) -> Tuple[PortfolioMetrics, Indicator]: +) -> Tuple[PORT_METRIC, INDICATOR_METRIC]: """backtest function for the interaction of the outermost strategy and executor in the nested decision execution please refer to the docs of `collect_data_loop` Returns ------- - portfolio_metrics: PortfolioMetrics + portfolio_dict: PORT_METRIC it records the trading portfolio_metrics information - indicator: Indicator + indicator_dict: INDICATOR_METRIC it computes the trading indicator """ return_value: dict = {} for _decision in collect_data_loop(start_time, end_time, trade_strategy, trade_executor, return_value): pass - portfolio_metrics = cast(PortfolioMetrics, return_value.get("portfolio_metrics")) - indicator = cast(Indicator, return_value.get("indicator")) - return portfolio_metrics, indicator + portfolio_dict = cast(PORT_METRIC, return_value.get("portfolio_dict")) + indicator_dict = cast(INDICATOR_METRIC, return_value.get("indicator_dict")) + + return portfolio_dict, indicator_dict def collect_data_loop( @@ -89,14 +94,17 @@ def collect_data_loop( if return_value is not None: all_executors = trade_executor.get_all_executors() - all_portfolio_metrics = { - "{}{}".format(*Freq.parse(_executor.time_per_step)): _executor.trade_account.get_portfolio_metrics() - for _executor in all_executors - if _executor.trade_account.is_port_metr_enabled() - } - all_indicators = {} - for _executor in all_executors: - key = "{}{}".format(*Freq.parse(_executor.time_per_step)) - all_indicators[key] = _executor.trade_account.get_trade_indicator().generate_trade_indicators_dataframe() - all_indicators[key + "_obj"] = _executor.trade_account.get_trade_indicator() - return_value.update({"portfolio_metrics": all_portfolio_metrics, "indicator": all_indicators}) + + portfolio_dict: PORT_METRIC = {} + indicator_dict: INDICATOR_METRIC = {} + + for executor in all_executors: + key = "{}{}".format(*Freq.parse(executor.time_per_step)) + if executor.trade_account.is_port_metr_enabled(): + portfolio_dict[key] = executor.trade_account.get_portfolio_metrics() + + indicator_df = executor.trade_account.get_trade_indicator().generate_trade_indicators_dataframe() + indicator_obj = executor.trade_account.get_trade_indicator() + indicator_dict[key] = (indicator_df, indicator_obj) + + return_value.update({"portfolio_dict": portfolio_dict, "indicator_dict": indicator_dict}) diff --git a/qlib/backtest/exchange.py b/qlib/backtest/exchange.py index 16cd8815f9e..cc760be44dd 100644 --- a/qlib/backtest/exchange.py +++ b/qlib/backtest/exchange.py @@ -26,6 +26,15 @@ class Exchange: + # `quote_df` is a pd.DataFrame class that contains basic information for backtesting + # After some processing, the data will later be maintained by `quote_cls` object for faster data retriving. + # Some conventions for `quote_df` + # - $close is for calculating the total value at end of each day. + # - if $close is None, the stock on that day is reguarded as suspended. + # - $factor is for rounding to the trading unit; + # - if any $factor is missing when $close exists, trading unit rounding will be disabled + quote_df: pd.DataFrame + def __init__( self, freq: str = "day", @@ -159,6 +168,7 @@ def __init__( self.codes = codes # Necessary fields # $close is for calculating the total value at end of each day. + # - if $close is None, the stock on that day is reguarded as suspended. # $factor is for rounding to the trading unit # $change is for calculating the limit of the stock @@ -199,7 +209,7 @@ def get_quote_from_qlib(self) -> None: self.end_time, freq=self.freq, disk_cache=True, - ).dropna(subset=["$close"]) + ) self.quote_df.columns = self.all_fields # check buy_price data and sell_price data @@ -209,7 +219,7 @@ def get_quote_from_qlib(self) -> None: self.logger.warning("{} field data contains nan.".format(pstr)) # update trade_w_adj_price - if self.quote_df["$factor"].isna().any(): + if (self.quote_df["$factor"].isna() & ~self.quote_df["$close"].isna()).any(): # The 'factor.day.bin' file not exists, and `factor` field contains `nan` # Use adjusted price self.trade_w_adj_price = True @@ -245,9 +255,9 @@ def get_quote_from_qlib(self) -> None: assert set(self.extra_quote.columns) == set(self.quote_df.columns) - {"$change"} self.quote_df = pd.concat([self.quote_df, self.extra_quote], sort=False, axis=0) - LT_TP_EXP = "(exp)" # Tuple[str, str] - LT_FLT = "float" # float - LT_NONE = "none" # none + LT_TP_EXP = "(exp)" # Tuple[str, str]: the limitation is calculated by a Qlib expression. + LT_FLT = "float" # float: the trading limitation is based on `abs($change) < limit_threshold` + LT_NONE = "none" # none: there is no trading limitation def _get_limit_type(self, limit_threshold: Union[tuple, float, None]) -> str: """get limit type""" @@ -261,20 +271,25 @@ def _get_limit_type(self, limit_threshold: Union[tuple, float, None]) -> str: raise NotImplementedError(f"This type of `limit_threshold` is not supported") def _update_limit(self, limit_threshold: Union[Tuple, float, None]) -> None: + # $close is may contains NaN, the nan indicates that the stock is not tradable at that timestamp + suspended = self.quote_df["$close"].isna() # check limit_threshold limit_type = self._get_limit_type(limit_threshold) if limit_type == self.LT_NONE: - self.quote_df["limit_buy"] = False - self.quote_df["limit_sell"] = False + self.quote_df["limit_buy"] = suspended + self.quote_df["limit_sell"] = suspended elif limit_type == self.LT_TP_EXP: # set limit limit_threshold = cast(tuple, limit_threshold) - self.quote_df["limit_buy"] = self.quote_df[limit_threshold[0]] - self.quote_df["limit_sell"] = self.quote_df[limit_threshold[1]] + # astype bool is necessary, because quote_df is an expression and could be float + self.quote_df["limit_buy"] = self.quote_df[limit_threshold[0]].astype("bool") | suspended + self.quote_df["limit_sell"] = self.quote_df[limit_threshold[1]].astype("bool") | suspended elif limit_type == self.LT_FLT: limit_threshold = cast(float, limit_threshold) - self.quote_df["limit_buy"] = self.quote_df["$change"].ge(limit_threshold) - self.quote_df["limit_sell"] = self.quote_df["$change"].le(-limit_threshold) # pylint: disable=E1130 + self.quote_df["limit_buy"] = self.quote_df["$change"].ge(limit_threshold) | suspended + self.quote_df["limit_sell"] = ( + self.quote_df["$change"].le(-limit_threshold) | suspended + ) # pylint: disable=E1130 @staticmethod def _get_vol_limit(volume_threshold: Union[tuple, dict, None]) -> Tuple[Optional[list], Optional[list], set]: @@ -338,8 +353,18 @@ def check_stock_limit( - if direction is None, check if tradable for buying and selling. - if direction == Order.BUY, check the if tradable for buying - if direction == Order.SELL, check the sell limit for selling. + + Returns + ------- + True: the trading of the stock is limted (maybe hit the highest/lowest price), hence the stock is not tradable + False: the trading of the stock is not limited, hence the stock may be tradable """ + # NOTE: + # **all** is used when checking limitation. + # For example, the stock trading is limited in a day if every miniute is limited in a day if every miniute is limited. if direction is None: + # The trading limitation is related to the trading direction + # if the direction is not provided, then any limitation from buy or sell will result in trading limitation buy_limit = self.quote.get_data(stock_id, start_time, end_time, field="limit_buy", method="all") sell_limit = self.quote.get_data(stock_id, start_time, end_time, field="limit_sell", method="all") return bool(buy_limit or sell_limit) @@ -356,10 +381,24 @@ def check_stock_suspended( start_time: pd.Timestamp, end_time: pd.Timestamp, ) -> bool: + """if stock is suspended(hence not tradable), True will be returned""" # is suspended if stock_id in self.quote.get_all_stock(): - return self.quote.get_data(stock_id, start_time, end_time, "$close") is None + # suspended stocks are represented by None $close stock + # The $close may contains NaN, + close = self.quote.get_data(stock_id, start_time, end_time, "$close") + if close is None: + # if no close record exists + return True + elif isinstance(close, IndexData): + # **any** non-NaN $close represents trading opportunity may exists + # if all returned is nan, then the stock is suspended + return cast(bool, cast(IndexData, close).isna().all()) + else: + # it is single value, make sure is is not None + return np.isnan(close) else: + # if the stock is not in the stock list, then it is not tradable and regarded as suspended return True def is_stock_tradable( diff --git a/qlib/rl/contrib/backtest.py b/qlib/rl/contrib/backtest.py index 695c13d2ed4..4d1eae46db7 100644 --- a/qlib/rl/contrib/backtest.py +++ b/qlib/rl/contrib/backtest.py @@ -8,23 +8,22 @@ import pickle from collections import defaultdict from pathlib import Path -from typing import List, Optional, Tuple, Union +from typing import Dict, List, Optional, Tuple, Union, cast import numpy as np import pandas as pd import torch from joblib import Parallel, delayed -from qlib.typehint import Literal -from qlib.backtest import collect_data_loop, get_strategy_executor +from qlib.backtest import INDICATOR_METRIC, collect_data_loop, get_strategy_executor from qlib.backtest.decision import BaseTradeDecision, Order, OrderDir, TradeRangeByTime -from qlib.backtest.executor import BaseExecutor, NestedExecutor, SimulatorExecutor +from qlib.backtest.executor import SimulatorExecutor from qlib.backtest.high_performance_ds import BaseOrderIndicator from qlib.rl.contrib.naive_config_parser import get_backtest_config_fromfile from qlib.rl.contrib.utils import read_order_file from qlib.rl.data.integration import init_qlib from qlib.rl.order_execution.simulator_qlib import SingleAssetOrderExecution -from qlib.rl.utils.env_wrapper import CollectDataEnvWrapper +from qlib.typehint import Literal def _get_multi_level_executor_config( @@ -61,15 +60,6 @@ def _get_multi_level_executor_config( return executor_config -def _set_env_for_all_strategy(executor: BaseExecutor) -> None: - if isinstance(executor, NestedExecutor): - if hasattr(executor.inner_strategy, "set_env"): - env = CollectDataEnvWrapper() - env.reset() - executor.inner_strategy.set_env(env) - _set_env_for_all_strategy(executor.inner_executor) - - def _convert_indicator_to_dataframe(indicator: dict) -> Optional[pd.DataFrame]: record_list = [] for time, value_dict in indicator.items(): @@ -94,9 +84,10 @@ def _convert_indicator_to_dataframe(indicator: dict) -> Optional[pd.DataFrame]: return records -# TODO: there should be richer annotation for the input (e.g. report) and the returned report -# TODO: For example, @ dataclass with typed fields and detailed docstrings. -def _generate_report(decisions: List[BaseTradeDecision], report_indicators: List[dict]) -> dict: +def _generate_report( + decisions: List[BaseTradeDecision], + report_indicators: List[INDICATOR_METRIC], +) -> Dict[str, Tuple[pd.DataFrame, pd.DataFrame]]: """Generate backtest reports Parameters @@ -109,28 +100,25 @@ def _generate_report(decisions: List[BaseTradeDecision], report_indicators: List ------- """ - indicator_dict = defaultdict(list) - indicator_his = defaultdict(list) + indicator_dict: Dict[str, List[pd.DataFrame]] = defaultdict(list) + indicator_his: Dict[str, List[dict]] = defaultdict(list) + for report_indicator in report_indicators: - for key, value in report_indicator.items(): - if key.endswith("_obj"): - indicator_his[key].append(value.order_indicator_his) - else: - indicator_dict[key].append(value) + for key, (indicator_df, indicator_obj) in report_indicator.items(): + indicator_dict[key].append(indicator_df) + indicator_his[key].append(indicator_obj.order_indicator_his) report = {} decision_details = pd.concat([getattr(d, "details") for d in decisions if hasattr(d, "details")]) - for key in ["1min", "5min", "30min", "1day"]: - if key not in indicator_dict: - continue - - report[key] = pd.concat(indicator_dict[key]) - report[key + "_obj"] = pd.concat([_convert_indicator_to_dataframe(his) for his in indicator_his[key + "_obj"]]) - + for key in indicator_dict: + cur_dict = pd.concat(indicator_dict[key]) + cur_his = pd.concat([_convert_indicator_to_dataframe(his) for his in indicator_his[key]]) cur_details = decision_details[decision_details.freq == key].set_index(["instrument", "datetime"]) if len(cur_details) > 0: cur_details.pop("freq") - report[key + "_obj"] = report[key + "_obj"].join(cur_details, how="outer") + cur_his = cur_his.join(cur_details, how="outer") + + report[key] = (cur_dict, cur_his) return report @@ -209,25 +197,25 @@ def single_with_simulator( exchange_config=exchange_config, qlib_config=None, cash_limit=None, - backtest_mode=True, ) reports.append(simulator.report_dict) decisions += simulator.decisions - indicator = {k: v for report in reports for k, v in report["indicator"]["1day_obj"].order_indicator_his.items()} - records = _convert_indicator_to_dataframe(indicator) + indicator_1day_objs = [report["indicator"]["1day"][1] for report in reports] + indicator_info = {k: v for obj in indicator_1day_objs for k, v in obj.order_indicator_his.items()} + records = _convert_indicator_to_dataframe(indicator_info) assert records is None or not np.isnan(records["ffr"]).any() if generate_report: - report = _generate_report(decisions, [report["indicator"] for report in reports]) + _report = _generate_report(decisions, [report["indicator"] for report in reports]) if split == "stock": stock_id = orders.iloc[0].instrument - report = {stock_id: report} + report = {stock_id: _report} else: day = orders.iloc[0].datetime - report = {day: report} + report = {day: _report} return records, report else: @@ -312,22 +300,22 @@ def single_with_collect_data_loop( exchange_kwargs=exchange_config, pos_type="Position" if cash_limit is not None else "InfPosition", ) - _set_env_for_all_strategy(executor=executor) report_dict: dict = {} decisions = list(collect_data_loop(trade_start_time, trade_end_time, strategy, executor, report_dict)) - records = _convert_indicator_to_dataframe(report_dict["indicator"]["1day_obj"].order_indicator_his) + indicator_dict = cast(INDICATOR_METRIC, report_dict.get("indicator_dict")) + records = _convert_indicator_to_dataframe(indicator_dict["1day"][1].order_indicator_his) assert records is None or not np.isnan(records["ffr"]).any() if generate_report: - report = _generate_report(decisions, [report_dict["indicator"]]) + _report = _generate_report(decisions, [indicator_dict]) if split == "stock": stock_id = orders.iloc[0].instrument - report = {stock_id: report} + report = {stock_id: _report} else: day = orders.iloc[0].datetime - report = {day: report} + report = {day: _report} return records, report else: return records @@ -337,7 +325,7 @@ def backtest(backtest_config: dict, with_simulator: bool = False) -> pd.DataFram order_df = read_order_file(backtest_config["order_file"]) cash_limit = backtest_config["exchange"].pop("cash_limit") - generate_report = backtest_config["exchange"].pop("generate_report") + generate_report = backtest_config.pop("generate_report") stock_pool = order_df["instrument"].unique().tolist() stock_pool.sort() @@ -382,9 +370,19 @@ def backtest(backtest_config: dict, with_simulator: bool = False) -> pd.DataFram parser = argparse.ArgumentParser() parser.add_argument("--config_path", type=str, required=True, help="Path to the config file") parser.add_argument("--use_simulator", action="store_true", help="Whether to use simulator as the backend") + parser.add_argument( + "--n_jobs", + type=int, + required=False, + help="The number of jobs for running backtest parallely(1 for single process)", + ) args = parser.parse_args() + config = get_backtest_config_fromfile(args.config_path) + if args.n_jobs is not None: + config["concurrency"] = args.n_jobs + backtest( - backtest_config=get_backtest_config_fromfile(args.config_path), + backtest_config=config, with_simulator=args.use_simulator, ) diff --git a/qlib/rl/contrib/naive_config_parser.py b/qlib/rl/contrib/naive_config_parser.py index 3f3d2eeadc8..ab5e9535969 100644 --- a/qlib/rl/contrib/naive_config_parser.py +++ b/qlib/rl/contrib/naive_config_parser.py @@ -11,11 +11,14 @@ import yaml +DELETE_KEY = "_delete_" + + def merge_a_into_b(a: dict, b: dict) -> dict: b = b.copy() for k, v in a.items(): if isinstance(v, dict) and k in b: - v.pop("_delete_", False) # TODO: make this more elegant + v.pop(DELETE_KEY, False) b[k] = merge_a_into_b(v, b[k]) else: b[k] = v @@ -86,7 +89,6 @@ def get_backtest_config_fromfile(path: str) -> dict: "min_cost": 5.0, "trade_unit": 100.0, "cash_limit": None, - "generate_report": False, } backtest_config["exchange"] = merge_a_into_b(a=backtest_config["exchange"], b=exchange_config_default) backtest_config["exchange"] = _convert_all_list_to_tuple(backtest_config["exchange"]) @@ -97,7 +99,7 @@ def get_backtest_config_fromfile(path: str) -> dict: "concurrency": -1, "multiplier": 1.0, "output_dir": "outputs/", - # "runtime": {}, + "generate_report": False, } backtest_config = merge_a_into_b(a=backtest_config, b=backtest_config_default) diff --git a/qlib/rl/data/native.py b/qlib/rl/data/native.py index 9417534f867..f09d909bc8d 100644 --- a/qlib/rl/data/native.py +++ b/qlib/rl/data/native.py @@ -13,7 +13,6 @@ from .base import BaseIntradayBacktestData, BaseIntradayProcessedData, ProcessedDataProvider from .integration import fetch_features -from ...data import D class IntradayBacktestData(BaseIntradayBacktestData): @@ -81,17 +80,7 @@ def load_backtest_data( trade_exchange: Exchange, trade_range: TradeRange, ) -> IntradayBacktestData: - # TODO: making exchange return data without missing will make it more elegant. Fix this in the future. - tmp_data = D.features( - trade_exchange.codes, - trade_exchange.all_fields, - trade_exchange.start_time, - trade_exchange.end_time, - freq=trade_exchange.freq, - disk_cache=True, - ) - - ticks_index = pd.DatetimeIndex(tmp_data.reset_index()["datetime"]) + ticks_index = pd.DatetimeIndex(trade_exchange.quote_df.reset_index()["datetime"]) ticks_index = ticks_index[order.start_time <= ticks_index] ticks_index = ticks_index[ticks_index <= order.end_time] diff --git a/qlib/rl/interpreter.py b/qlib/rl/interpreter.py index d2d81f81cd3..5c9cc26c4e6 100644 --- a/qlib/rl/interpreter.py +++ b/qlib/rl/interpreter.py @@ -3,19 +3,15 @@ from __future__ import annotations -from typing import Any, Generic, Optional, TYPE_CHECKING, TypeVar +from typing import Any, Generic, TypeVar +import gym import numpy as np +from gym import spaces from qlib.typehint import final from .simulator import ActType, StateType -if TYPE_CHECKING: - from .utils.env_wrapper import BaseEnvWrapper - -import gym -from gym import spaces - ObsType = TypeVar("ObsType") PolicyActType = TypeVar("PolicyActType") @@ -39,8 +35,6 @@ class Interpreter: class StateInterpreter(Generic[StateType, ObsType], Interpreter): """State Interpreter that interpret execution result of qlib executor into rl env state""" - env: Optional[BaseEnvWrapper] = None - @property def observation_space(self) -> gym.Space: raise NotImplementedError() @@ -73,8 +67,6 @@ def interpret(self, simulator_state: StateType) -> ObsType: class ActionInterpreter(Generic[StateType, PolicyActType, ActType], Interpreter): """Action Interpreter that interpret rl agent action into qlib orders""" - env: Optional[BaseEnvWrapper] = None - @property def action_space(self) -> gym.Space: raise NotImplementedError() diff --git a/qlib/rl/order_execution/interpreter.py b/qlib/rl/order_execution/interpreter.py index 0b89977491b..0d45624bdad 100644 --- a/qlib/rl/order_execution/interpreter.py +++ b/qlib/rl/order_execution/interpreter.py @@ -69,8 +69,6 @@ class FullHistoryStateInterpreter(StateInterpreter[SAOEState, FullHistoryObs]): Provider of the processed data. """ - # TODO: All implementations related to `data_dir` is coupled with the specific data format for that specific case. - # TODO: So it should be redesigned after the data interface is well-designed. def __init__( self, max_step: int, @@ -78,6 +76,8 @@ def __init__( data_dim: int, processed_data_provider: dict | ProcessedDataProvider, ) -> None: + super().__init__() + self.max_step = max_step self.data_ticks = data_ticks self.data_dim = data_dim @@ -87,10 +87,6 @@ def __init__( ) def interpret(self, state: SAOEState) -> FullHistoryObs: - # TODO: This interpreter relies on EnvWrapper.status, so we have to give it a dummy EnvWrapper when running - # backtest. Currently, the dummy EnvWrapper is CollectDataEnvWrapper. We should find a more elegant - # way to decompose interpreter and EnvWrapper in the future. - processed = self.processed_data_provider.get_data( stock_id=state.order.stock_id, date=pd.Timestamp(state.order.start_time.date()), @@ -102,8 +98,6 @@ def interpret(self, state: SAOEState) -> FullHistoryObs: position_history[0] = state.order.amount position_history[1 : len(state.history_steps) + 1] = state.history_steps["position"].to_numpy() - assert self.env is not None - # The min, slice here are to make sure that indices fit into the range, # even after the final step of the simulator (in the done step), # to make network in policy happy. @@ -115,7 +109,7 @@ def interpret(self, state: SAOEState) -> FullHistoryObs: "data_processed_prev": np.array(processed.yesterday), "acquiring": _to_int32(state.order.direction == state.order.BUY), "cur_tick": _to_int32(min(int(np.sum(state.ticks_index < state.cur_time)), self.data_ticks - 1)), - "cur_step": _to_int32(min(self.env.status["cur_step"], self.max_step - 1)), + "cur_step": _to_int32(min(state.cur_step, self.max_step - 1)), "num_step": _to_int32(self.max_step), "target": _to_float32(state.order.amount), "position": _to_float32(state.position), @@ -163,6 +157,8 @@ class CurrentStepStateInterpreter(StateInterpreter[SAOEState, CurrentStateObs]): """ def __init__(self, max_step: int) -> None: + super().__init__() + self.max_step = max_step @property @@ -177,15 +173,10 @@ def observation_space(self) -> spaces.Dict: return spaces.Dict(space) def interpret(self, state: SAOEState) -> CurrentStateObs: - # TODO: This interpreter relies on EnvWrapper.status, so we have to give it a dummy EnvWrapper when running - # backtest. Currently, the dummy EnvWrapper is CollectDataEnvWrapper. We should find a more elegant - # way to decompose interpreter and EnvWrapper in the future. - - assert self.env is not None - assert self.env.status["cur_step"] <= self.max_step + assert state.cur_step <= self.max_step obs = CurrentStateObs( acquiring=state.order.direction == state.order.BUY, - cur_step=self.env.status["cur_step"], + cur_step=state.cur_step, num_step=self.max_step, target=state.order.amount, position=state.position, @@ -208,6 +199,8 @@ class CategoricalActionInterpreter(ActionInterpreter[SAOEState, int, float]): """ def __init__(self, values: int | List[float], max_step: Optional[int] = None) -> None: + super().__init__() + if isinstance(values, int): values = [i / values for i in range(0, values + 1)] self.action_values = values @@ -218,13 +211,8 @@ def action_space(self) -> spaces.Discrete: return spaces.Discrete(len(self.action_values)) def interpret(self, state: SAOEState, action: int) -> float: - # TODO: This interpreter relies on EnvWrapper.status, so we have to give it a dummy EnvWrapper when running - # backtest. Currently, the dummy EnvWrapper is CollectDataEnvWrapper. We should find a more elegant - # way to decompose interpreter and EnvWrapper in the future. - assert 0 <= action < len(self.action_values) - assert self.env is not None - if self.max_step is not None and self.env.status["cur_step"] >= self.max_step - 1: + if self.max_step is not None and state.cur_step >= self.max_step - 1: return state.position else: return min(state.position, state.order.amount * self.action_values[action]) @@ -244,13 +232,8 @@ def action_space(self) -> spaces.Box: return spaces.Box(0, np.inf, shape=(), dtype=np.float32) def interpret(self, state: SAOEState, action: float) -> float: - # TODO: This interpreter relies on EnvWrapper.status, so we have to give it a dummy EnvWrapper when running - # backtest. Currently, the dummy EnvWrapper is CollectDataEnvWrapper. We should find a more elegant - # way to decompose interpreter and EnvWrapper in the future. - - assert self.env is not None estimated_total_steps = math.ceil(len(state.ticks_for_order) / state.ticks_per_step) - twap_volume = state.position / (estimated_total_steps - self.env.status["cur_step"]) + twap_volume = state.position / (estimated_total_steps - state.cur_step) return min(state.position, twap_volume * action) diff --git a/qlib/rl/order_execution/policy.py b/qlib/rl/order_execution/policy.py index 7f7a98e9a71..598e6b589a6 100644 --- a/qlib/rl/order_execution/policy.py +++ b/qlib/rl/order_execution/policy.py @@ -4,7 +4,7 @@ from __future__ import annotations from pathlib import Path -from typing import Any, Dict, Generator, Iterable, Optional, Tuple, cast +from typing import Any, Dict, Generator, Iterable, Optional, OrderedDict, Tuple, cast import gym import numpy as np @@ -14,6 +14,8 @@ from tianshou.data import Batch, ReplayBuffer, to_torch from tianshou.policy import BasePolicy, PPOPolicy +from qlib.rl.trainer.trainer import Trainer + __all__ = ["AllOne", "PPO"] @@ -148,7 +150,7 @@ def __init__( action_space=action_space, ) if weight_file is not None: - load_weight(self, weight_file) + set_weight(self, Trainer.get_policy_state_dict(weight_file)) # utilities: these should be put in a separate (common) file. # @@ -160,15 +162,7 @@ def auto_device(module: nn.Module) -> torch.device: return torch.device("cpu") # fallback to cpu -def load_weight(policy: nn.Module, path: Path) -> None: - assert isinstance(policy, nn.Module), "Policy has to be an nn.Module to load weight." - loaded_weight = torch.load(path, map_location="cpu") - - # TODO: this should be handled by whoever calls load_weight. - # TODO: For example, when the outer class receives a weight, it should first unpack it, - # TODO: and send the corresponding part to individual component. - if "vessel" in loaded_weight: - loaded_weight = loaded_weight["vessel"]["policy"] +def set_weight(policy: nn.Module, loaded_weight: OrderedDict) -> None: try: policy.load_state_dict(loaded_weight) except RuntimeError: diff --git a/qlib/rl/order_execution/simulator_qlib.py b/qlib/rl/order_execution/simulator_qlib.py index c9702b1e48d..610a0c0bd50 100644 --- a/qlib/rl/order_execution/simulator_qlib.py +++ b/qlib/rl/order_execution/simulator_qlib.py @@ -9,12 +9,11 @@ from qlib.backtest import collect_data_loop, get_strategy_executor from qlib.backtest.decision import BaseTradeDecision, Order, TradeRangeByTime -from qlib.backtest.executor import BaseExecutor, NestedExecutor +from qlib.backtest.executor import NestedExecutor from qlib.rl.data.integration import init_qlib from qlib.rl.simulator import Simulator -from .state import SAOEState, SAOEStateAdapter -from .strategy import SAOEStrategy -from ..utils.env_wrapper import CollectDataEnvWrapper +from .state import SAOEState +from .strategy import SAOEStateAdapter, SAOEStrategy class SingleAssetOrderExecution(Simulator[Order, SAOEState, float]): @@ -32,8 +31,6 @@ class SingleAssetOrderExecution(Simulator[Order, SAOEState, float]): Configuration used to initialize Qlib. If it is None, Qlib will not be initialized. cash_limit: Cash limit. - backtest_mode - Whether the simulator is under backtest mode. """ def __init__( @@ -43,7 +40,6 @@ def __init__( exchange_config: dict, qlib_config: dict = None, cash_limit: Optional[float] = None, - backtest_mode: bool = False, ) -> None: super().__init__(initial=order) @@ -59,7 +55,7 @@ def __init__( } self._collect_data_loop: Optional[Generator] = None - self.reset(order, strategy_config, executor_config, exchange_config, qlib_config, cash_limit, backtest_mode) + self.reset(order, strategy_config, executor_config, exchange_config, qlib_config, cash_limit) def reset( self, @@ -69,7 +65,6 @@ def reset( exchange_config: dict, qlib_config: dict = None, cash_limit: Optional[float] = None, - backtest_mode: bool = False, ) -> None: if qlib_config is not None: init_qlib(qlib_config, part="skip") @@ -98,16 +93,6 @@ def reset( ) assert isinstance(self._collect_data_loop, Generator) - # TODO: backtest_mode is not a necessary parameter if we carefully design it. - # TODO: It should disappear with CollectDataEnvWrapper in the future. - if backtest_mode: - executor: BaseExecutor = self._executor - while isinstance(executor, NestedExecutor): - if hasattr(executor.inner_strategy, "set_env"): - executor.inner_strategy.set_env(CollectDataEnvWrapper()) - executor = executor.inner_executor - - # Call `step()` with None action to initialize the internal generator. self.step(action=None) self._order = order diff --git a/qlib/rl/order_execution/simulator_simple.py b/qlib/rl/order_execution/simulator_simple.py index 17efb4b0934..9086e6047c7 100644 --- a/qlib/rl/order_execution/simulator_simple.py +++ b/qlib/rl/order_execution/simulator_simple.py @@ -16,8 +16,6 @@ from .state import SAOEMetrics, SAOEState -# TODO: Integrating Qlib's native data with simulator_simple - __all__ = ["SingleAssetOrderExecutionSimple"] @@ -98,6 +96,7 @@ def __init__( self.ticks_for_order = self._get_ticks_slice(self.order.start_time, self.order.end_time) self.cur_time = self.ticks_for_order[0] + self.cur_step = 0 # NOTE: astype(float) is necessary in some systems. # this will align the precision with `.to_numpy()` in `_split_exec_vol` self.twap_price = float(self.backtest_data.get_deal_price().loc[self.ticks_for_order].astype(float).mean()) @@ -194,11 +193,13 @@ def step(self, amount: float) -> None: self.env.logger.add_any(key, value) self.cur_time = self._next_time() + self.cur_step += 1 def get_state(self) -> SAOEState: return SAOEState( order=self.order, cur_time=self.cur_time, + cur_step=self.cur_step, position=self.position, history_exec=self.history_exec, history_steps=self.history_steps, diff --git a/qlib/rl/order_execution/state.py b/qlib/rl/order_execution/state.py index f417173e524..315735eaf84 100644 --- a/qlib/rl/order_execution/state.py +++ b/qlib/rl/order_execution/state.py @@ -4,290 +4,15 @@ from __future__ import annotations import typing -from typing import cast, Callable, List, NamedTuple, Optional, Tuple +from typing import NamedTuple, Optional import numpy as np import pandas as pd -from qlib.backtest import Exchange, Order -from qlib.backtest.executor import BaseExecutor -from qlib.constant import EPS, ONE_MIN, REG_CN -from qlib.rl.order_execution.utils import dataframe_append, price_advantage +from qlib.backtest import Order from qlib.typehint import TypedDict -from qlib.utils.index_data import IndexData -from qlib.utils.time import get_day_min_idx_range if typing.TYPE_CHECKING: from qlib.rl.data.base import BaseIntradayBacktestData - from qlib.rl.data.native import IntradayBacktestData - - -def _get_all_timestamps( - start: pd.Timestamp, - end: pd.Timestamp, - granularity: pd.Timedelta = ONE_MIN, - include_end: bool = True, -) -> pd.DatetimeIndex: - ret = [] - while start <= end: - ret.append(start) - start += granularity - - if ret[-1] > end: - ret.pop() - if ret[-1] == end and not include_end: - ret.pop() - return pd.DatetimeIndex(ret) - - -def fill_missing_data( - original_data: np.ndarray, - total_time_list: List[pd.Timestamp], - found_time_list: List[pd.Timestamp], - fill_method: Callable = np.median, -) -> np.ndarray: - """Fill missing data. We need this function to deal with data that have missing values in some minutes. - - TODO: making exchange return data without missing will make it more elegant. Fix this in the future. - - Parameters - ---------- - original_data - Original data without missing values. - total_time_list - All timestamps that required. - found_time_list - Timestamps found in the original data. - fill_method - Method used to fill the missing data. - - Returns - ------- - The filled data. - """ - assert len(original_data) == len(found_time_list) - tmp = dict(zip(found_time_list, original_data)) - fill_val = fill_method(original_data) - return np.array([tmp.get(t, fill_val) for t in total_time_list]) - - -class SAOEStateAdapter: - """ - Maintain states of the environment. SAOEStateAdapter accepts execution results and update its internal state - according to the execution results with additional information acquired from executors & exchange. For example, - it gets the dealt order amount from execution results, and get the corresponding market price / volume from - exchange. - - Example usage:: - - adapter = SAOEStateAdapter(...) - adapter.update(...) - state = adapter.saoe_state - """ - - def __init__( - self, - order: Order, - executor: BaseExecutor, - exchange: Exchange, - ticks_per_step: int, - backtest_data: IntradayBacktestData, - ) -> None: - self.position = order.amount - self.order = order - self.executor = executor - self.exchange = exchange - self.backtest_data = backtest_data - - self.twap_price = self.backtest_data.get_deal_price().mean() - - metric_keys = list(SAOEMetrics.__annotations__.keys()) # pylint: disable=no-member - self.history_exec = pd.DataFrame(columns=metric_keys).set_index("datetime") - self.history_steps = pd.DataFrame(columns=metric_keys).set_index("datetime") - self.metrics: Optional[SAOEMetrics] = None - - self.cur_time = max(backtest_data.ticks_for_order[0], order.start_time) - self.ticks_per_step = ticks_per_step - - def _next_time(self) -> pd.Timestamp: - current_loc = self.backtest_data.ticks_index.get_loc(self.cur_time) - next_loc = current_loc + self.ticks_per_step - next_loc = next_loc - next_loc % self.ticks_per_step - if ( - next_loc < len(self.backtest_data.ticks_index) - and self.backtest_data.ticks_index[next_loc] < self.order.end_time - ): - return self.backtest_data.ticks_index[next_loc] - else: - return self.order.end_time - - def update( - self, - execute_result: list, - last_step_range: Tuple[int, int], - ) -> None: - last_step_size = last_step_range[1] - last_step_range[0] + 1 - start_time = self.backtest_data.ticks_index[last_step_range[0]] - end_time = self.backtest_data.ticks_index[last_step_range[1]] - - exec_vol = np.zeros(last_step_size) - for order, _, __, ___ in execute_result: - idx, _ = get_day_min_idx_range(order.start_time, order.end_time, "1min", REG_CN) - exec_vol[idx - last_step_range[0]] = order.deal_amount - - if exec_vol.sum() > self.position and exec_vol.sum() > 0.0: - assert exec_vol.sum() < self.position + 1, f"{exec_vol} too large" - exec_vol *= self.position / (exec_vol.sum()) - - market_volume = cast( - IndexData, - self.exchange.get_volume( - self.order.stock_id, - pd.Timestamp(start_time), - pd.Timestamp(end_time), - method=None, - ), - ) - market_price = cast( - IndexData, - self.exchange.get_deal_price( - self.order.stock_id, - pd.Timestamp(start_time), - pd.Timestamp(end_time), - method=None, - direction=self.order.direction, - ), - ) - found_time_list = [pd.Timestamp(e) for e in list(market_volume.index)] - total_time_list = _get_all_timestamps(start_time, end_time) - market_price = fill_missing_data(np.array(market_price).reshape(-1), total_time_list, found_time_list) - market_volume = fill_missing_data(np.array(market_volume).reshape(-1), total_time_list, found_time_list) - - assert market_price.shape == market_volume.shape == exec_vol.shape - - # Get data from the current level executor's indicator - current_trade_account = self.executor.trade_account - current_df = current_trade_account.get_trade_indicator().generate_trade_indicators_dataframe() - self.history_exec = dataframe_append( - self.history_exec, - self._collect_multi_order_metric( - order=self.order, - datetime=_get_all_timestamps(start_time, end_time, include_end=True), - market_vol=market_volume, - market_price=market_price, - exec_vol=exec_vol, - pa=current_df.iloc[-1]["pa"], - ), - ) - - self.history_steps = dataframe_append( - self.history_steps, - [ - self._collect_single_order_metric( - self.order, - self.cur_time, - market_volume, - market_price, - exec_vol.sum(), - exec_vol, - ), - ], - ) - - # TODO: check whether we need this. Can we get this information from Account? - # Do this at the end - self.position -= exec_vol.sum() - - self.cur_time = self._next_time() - - def generate_metrics_after_done(self) -> None: - """Generate metrics once the upper level execution is done""" - - self.metrics = self._collect_single_order_metric( - self.order, - self.backtest_data.ticks_index[0], # start time - self.history_exec["market_volume"], - self.history_exec["market_price"], - self.history_steps["amount"].sum(), - self.history_exec["deal_amount"], - ) - - def _collect_multi_order_metric( - self, - order: Order, - datetime: pd.DatetimeIndex, - market_vol: np.ndarray, - market_price: np.ndarray, - exec_vol: np.ndarray, - pa: float, - ) -> SAOEMetrics: - return SAOEMetrics( - # It should have the same keys with SAOEMetrics, - # but the values do not necessarily have the annotated type. - # Some values could be vectorized (e.g., exec_vol). - stock_id=order.stock_id, - datetime=datetime, - direction=order.direction, - market_volume=market_vol, - market_price=market_price, - amount=exec_vol, - inner_amount=exec_vol, - deal_amount=exec_vol, - trade_price=market_price, - trade_value=market_price * exec_vol, - position=self.position - np.cumsum(exec_vol), - ffr=exec_vol / order.amount, - pa=pa, - ) - - def _collect_single_order_metric( - self, - order: Order, - datetime: pd.Timestamp, - market_vol: np.ndarray, - market_price: np.ndarray, - amount: float, # intended to trade such amount - exec_vol: np.ndarray, - ) -> SAOEMetrics: - assert len(market_vol) == len(market_price) == len(exec_vol) - - if np.abs(np.sum(exec_vol)) < EPS: - exec_avg_price = 0.0 - else: - exec_avg_price = cast(float, np.average(market_price, weights=exec_vol)) # could be nan - if hasattr(exec_avg_price, "item"): # could be numpy scalar - exec_avg_price = exec_avg_price.item() # type: ignore - - exec_sum = exec_vol.sum() - return SAOEMetrics( - stock_id=order.stock_id, - datetime=datetime, - direction=order.direction, - market_volume=market_vol.sum(), - market_price=market_price.mean() if len(market_price) > 0 else np.nan, - amount=amount, - inner_amount=exec_sum, - deal_amount=exec_sum, # in this simulator, there's no other restrictions - trade_price=exec_avg_price, - trade_value=float(np.sum(market_price * exec_vol)), - position=self.position - exec_sum, - ffr=float(exec_sum / order.amount), - pa=price_advantage(exec_avg_price, self.twap_price, order.direction), - ) - - @property - def saoe_state(self) -> SAOEState: - return SAOEState( - order=self.order, - cur_time=self.cur_time, - position=self.position, - history_exec=self.history_exec, - history_steps=self.history_steps, - metrics=self.metrics, - backtest_data=self.backtest_data, - ticks_per_step=self.ticks_per_step, - ticks_index=self.backtest_data.ticks_index, - ticks_for_order=self.backtest_data.ticks_for_order, - ) class SAOEMetrics(TypedDict): @@ -302,7 +27,7 @@ class SAOEMetrics(TypedDict): stock_id: str """Stock ID of this record.""" - datetime: pd.Timestamp | pd.DatetimeIndex # TODO: check this + datetime: pd.Timestamp | pd.DatetimeIndex """Datetime of this record (this is index in the dataframe).""" direction: int """Direction of the order. 0 for sell, 1 for buy.""" @@ -349,6 +74,8 @@ class SAOEState(NamedTuple): """The order we are dealing with.""" cur_time: pd.Timestamp """Current time, e.g., 9:30.""" + cur_step: int + """Current step, e.g., 0.""" position: float """Current remaining volume to execute.""" history_exec: pd.DataFrame diff --git a/qlib/rl/order_execution/strategy.py b/qlib/rl/order_execution/strategy.py index 663b8e8ff4a..0102b9e57ff 100644 --- a/qlib/rl/order_execution/strategy.py +++ b/qlib/rl/order_execution/strategy.py @@ -5,7 +5,7 @@ import collections from types import GeneratorType -from typing import Any, cast, Dict, Generator, List, Optional, Union +from typing import Any, Callable, cast, Dict, Generator, List, Optional, Tuple, Union import numpy as np import pandas as pd @@ -15,14 +15,276 @@ from qlib.backtest import CommonInfrastructure, Order from qlib.backtest.decision import BaseTradeDecision, TradeDecisionWithDetails, TradeDecisionWO, TradeRange -from qlib.backtest.utils import LevelInfrastructure -from qlib.constant import ONE_MIN -from qlib.rl.data.native import load_backtest_data +from qlib.backtest.exchange import Exchange +from qlib.backtest.executor import BaseExecutor +from qlib.backtest.utils import LevelInfrastructure, get_start_end_idx +from qlib.constant import EPS, ONE_MIN, REG_CN +from qlib.rl.data.native import IntradayBacktestData, load_backtest_data from qlib.rl.interpreter import ActionInterpreter, StateInterpreter -from qlib.rl.order_execution.state import SAOEState, SAOEStateAdapter -from qlib.rl.utils.env_wrapper import BaseEnvWrapper +from qlib.rl.order_execution.state import SAOEMetrics, SAOEState +from qlib.rl.order_execution.utils import dataframe_append, price_advantage from qlib.strategy.base import RLStrategy from qlib.utils import init_instance_by_config +from qlib.utils.index_data import IndexData +from qlib.utils.time import get_day_min_idx_range + + +def _get_all_timestamps( + start: pd.Timestamp, + end: pd.Timestamp, + granularity: pd.Timedelta = ONE_MIN, + include_end: bool = True, +) -> pd.DatetimeIndex: + ret = [] + while start <= end: + ret.append(start) + start += granularity + + if ret[-1] > end: + ret.pop() + if ret[-1] == end and not include_end: + ret.pop() + return pd.DatetimeIndex(ret) + + +def fill_missing_data( + original_data: np.ndarray, + fill_method: Callable = np.nanmedian, +) -> np.ndarray: + """Fill missing data. + + Parameters + ---------- + original_data + Original data without missing values. + fill_method + Method used to fill the missing data. + + Returns + ------- + The filled data. + """ + return np.nan_to_num(original_data, nan=fill_method(original_data)) + + +class SAOEStateAdapter: + """ + Maintain states of the environment. SAOEStateAdapter accepts execution results and update its internal state + according to the execution results with additional information acquired from executors & exchange. For example, + it gets the dealt order amount from execution results, and get the corresponding market price / volume from + exchange. + + Example usage:: + + adapter = SAOEStateAdapter(...) + adapter.update(...) + state = adapter.saoe_state + """ + + def __init__( + self, + order: Order, + trade_decision: BaseTradeDecision, + executor: BaseExecutor, + exchange: Exchange, + ticks_per_step: int, + backtest_data: IntradayBacktestData, + ) -> None: + self.position = order.amount + self.order = order + self.executor = executor + self.exchange = exchange + self.backtest_data = backtest_data + self.start_idx, _ = get_start_end_idx(self.executor.trade_calendar, trade_decision) + + self.twap_price = self.backtest_data.get_deal_price().mean() + + metric_keys = list(SAOEMetrics.__annotations__.keys()) # pylint: disable=no-member + self.history_exec = pd.DataFrame(columns=metric_keys).set_index("datetime") + self.history_steps = pd.DataFrame(columns=metric_keys).set_index("datetime") + self.metrics: Optional[SAOEMetrics] = None + + self.cur_time = max(backtest_data.ticks_for_order[0], order.start_time) + self.ticks_per_step = ticks_per_step + + def _next_time(self) -> pd.Timestamp: + current_loc = self.backtest_data.ticks_index.get_loc(self.cur_time) + next_loc = current_loc + self.ticks_per_step + next_loc = next_loc - next_loc % self.ticks_per_step + if ( + next_loc < len(self.backtest_data.ticks_index) + and self.backtest_data.ticks_index[next_loc] < self.order.end_time + ): + return self.backtest_data.ticks_index[next_loc] + else: + return self.order.end_time + + def update( + self, + execute_result: list, + last_step_range: Tuple[int, int], + ) -> None: + last_step_size = last_step_range[1] - last_step_range[0] + 1 + start_time = self.backtest_data.ticks_index[last_step_range[0]] + end_time = self.backtest_data.ticks_index[last_step_range[1]] + + exec_vol = np.zeros(last_step_size) + for order, _, __, ___ in execute_result: + idx, _ = get_day_min_idx_range(order.start_time, order.end_time, "1min", REG_CN) + exec_vol[idx - last_step_range[0]] = order.deal_amount + + if exec_vol.sum() > self.position and exec_vol.sum() > 0.0: + assert exec_vol.sum() < self.position + 1, f"{exec_vol} too large" + exec_vol *= self.position / (exec_vol.sum()) + + market_volume = cast( + IndexData, + self.exchange.get_volume( + self.order.stock_id, + pd.Timestamp(start_time), + pd.Timestamp(end_time), + method=None, + ), + ) + market_price = cast( + IndexData, + self.exchange.get_deal_price( + self.order.stock_id, + pd.Timestamp(start_time), + pd.Timestamp(end_time), + method=None, + direction=self.order.direction, + ), + ) + market_price = fill_missing_data(np.array(market_price, dtype=float).reshape(-1)) + market_volume = fill_missing_data(np.array(market_volume, dtype=float).reshape(-1)) + + assert market_price.shape == market_volume.shape == exec_vol.shape + + # Get data from the current level executor's indicator + current_trade_account = self.executor.trade_account + current_df = current_trade_account.get_trade_indicator().generate_trade_indicators_dataframe() + self.history_exec = dataframe_append( + self.history_exec, + self._collect_multi_order_metric( + order=self.order, + datetime=_get_all_timestamps(start_time, end_time, include_end=True), + market_vol=market_volume, + market_price=market_price, + exec_vol=exec_vol, + pa=current_df.iloc[-1]["pa"], + ), + ) + + self.history_steps = dataframe_append( + self.history_steps, + [ + self._collect_single_order_metric( + self.order, + self.cur_time, + market_volume, + market_price, + exec_vol.sum(), + exec_vol, + ), + ], + ) + + # Do this at the end + self.position -= exec_vol.sum() + + self.cur_time = self._next_time() + + def generate_metrics_after_done(self) -> None: + """Generate metrics once the upper level execution is done""" + + self.metrics = self._collect_single_order_metric( + self.order, + self.backtest_data.ticks_index[0], # start time + self.history_exec["market_volume"], + self.history_exec["market_price"], + self.history_steps["amount"].sum(), + self.history_exec["deal_amount"], + ) + + def _collect_multi_order_metric( + self, + order: Order, + datetime: pd.DatetimeIndex, + market_vol: np.ndarray, + market_price: np.ndarray, + exec_vol: np.ndarray, + pa: float, + ) -> SAOEMetrics: + return SAOEMetrics( + # It should have the same keys with SAOEMetrics, + # but the values do not necessarily have the annotated type. + # Some values could be vectorized (e.g., exec_vol). + stock_id=order.stock_id, + datetime=datetime, + direction=order.direction, + market_volume=market_vol, + market_price=market_price, + amount=exec_vol, + inner_amount=exec_vol, + deal_amount=exec_vol, + trade_price=market_price, + trade_value=market_price * exec_vol, + position=self.position - np.cumsum(exec_vol), + ffr=exec_vol / order.amount, + pa=pa, + ) + + def _collect_single_order_metric( + self, + order: Order, + datetime: pd.Timestamp, + market_vol: np.ndarray, + market_price: np.ndarray, + amount: float, # intended to trade such amount + exec_vol: np.ndarray, + ) -> SAOEMetrics: + assert len(market_vol) == len(market_price) == len(exec_vol) + + if np.abs(np.sum(exec_vol)) < EPS: + exec_avg_price = 0.0 + else: + exec_avg_price = cast(float, np.average(market_price, weights=exec_vol)) # could be nan + if hasattr(exec_avg_price, "item"): # could be numpy scalar + exec_avg_price = exec_avg_price.item() # type: ignore + + exec_sum = exec_vol.sum() + return SAOEMetrics( + stock_id=order.stock_id, + datetime=datetime, + direction=order.direction, + market_volume=market_vol.sum(), + market_price=market_price.mean() if len(market_price) > 0 else np.nan, + amount=amount, + inner_amount=exec_sum, + deal_amount=exec_sum, # in this simulator, there's no other restrictions + trade_price=exec_avg_price, + trade_value=float(np.sum(market_price * exec_vol)), + position=self.position - exec_sum, + ffr=float(exec_sum / order.amount), + pa=price_advantage(exec_avg_price, self.twap_price, order.direction), + ) + + @property + def saoe_state(self) -> SAOEState: + return SAOEState( + order=self.order, + cur_time=self.cur_time, + cur_step=self.executor.trade_calendar.get_trade_step() - self.start_idx, + position=self.position, + history_exec=self.history_exec, + history_steps=self.history_steps, + metrics=self.metrics, + backtest_data=self.backtest_data, + ticks_per_step=self.ticks_per_step, + ticks_index=self.backtest_data.ticks_index, + ticks_for_order=self.backtest_data.ticks_for_order, + ) class SAOEStrategy(RLStrategy): @@ -30,7 +292,7 @@ class SAOEStrategy(RLStrategy): def __init__( self, - policy: object, # TODO: add accurate typehint later. + policy: BasePolicy, outer_trade_decision: BaseTradeDecision = None, level_infra: LevelInfrastructure = None, common_infra: CommonInfrastructure = None, @@ -47,11 +309,17 @@ def __init__( self.adapter_dict: Dict[tuple, SAOEStateAdapter] = {} self._last_step_range = (0, 0) - def _create_qlib_backtest_adapter(self, order: Order, trade_range: TradeRange) -> SAOEStateAdapter: + def _create_qlib_backtest_adapter( + self, + order: Order, + trade_decision: BaseTradeDecision, + trade_range: TradeRange, + ) -> SAOEStateAdapter: backtest_data = load_backtest_data(order, self.trade_exchange, trade_range) return SAOEStateAdapter( order=order, + trade_decision=trade_decision, executor=self.executor, exchange=self.trade_exchange, ticks_per_step=int(pd.Timedelta(self.trade_calendar.get_freq()) / ONE_MIN), @@ -71,7 +339,9 @@ def reset(self, outer_trade_decision: BaseTradeDecision = None, **kwargs: Any) - self.adapter_dict = {} for decision in outer_trade_decision.get_decision(): order = cast(Order, decision) - self.adapter_dict[order.key_by_day] = self._create_qlib_backtest_adapter(order, trade_range) + self.adapter_dict[order.key_by_day] = self._create_qlib_backtest_adapter( + order, outer_trade_decision, trade_range + ) def get_saoe_state_by_order(self, order: Order) -> SAOEState: return self.adapter_dict[order.key_by_day].saoe_state @@ -166,11 +436,10 @@ def __init__( policy: dict | BasePolicy, state_interpreter: dict | StateInterpreter, action_interpreter: dict | ActionInterpreter, - network: object = None, # TODO: add accurate typehint later. + network: dict | torch.nn.Module | None = None, outer_trade_decision: BaseTradeDecision = None, level_infra: LevelInfrastructure = None, common_infra: CommonInfrastructure = None, - backtest: bool = False, **kwargs: Any, ) -> None: super(SAOEIntStrategy, self).__init__( @@ -181,8 +450,6 @@ def __init__( **kwargs, ) - self._backtest = backtest - self._state_interpreter: StateInterpreter = init_instance_by_config( state_interpreter, accept_types=StateInterpreter, @@ -221,21 +488,9 @@ def __init__( if self._policy is not None: self._policy.eval() - def set_env(self, env: BaseEnvWrapper) -> None: - # TODO: This method is used to set EnvWrapper for interpreters since they rely on EnvWrapper. - # We should decompose the interpreters with EnvWrapper in the future and we should remove this method - # after that. - - self._env = env - self._state_interpreter.env = self._action_interpreter.env = self._env - def reset(self, outer_trade_decision: BaseTradeDecision = None, **kwargs: Any) -> None: super().reset(outer_trade_decision=outer_trade_decision, **kwargs) - # In backtest, env.reset() needs to be manually called since there is no outer trainer to call it - if self._backtest: - self._env.reset() - def _generate_trade_details(self, act: np.ndarray, exec_vols: List[float]) -> pd.DataFrame: assert hasattr(self.outer_trade_decision, "order_list") @@ -268,10 +523,6 @@ def _generate_trade_decision(self, execute_result: list = None) -> BaseTradeDeci act = policy_out.act.numpy() if torch.is_tensor(policy_out.act) else policy_out.act exec_vols = [self._action_interpreter.interpret(s, a) for s, a in zip(states, act)] - # In backtest, env.step() needs to be manually called since there is no outer trainer to call it - if self._backtest: - self._env.step(None) - oh = self.trade_exchange.get_order_helper() order_list = [] for decision, exec_vol in zip(self.outer_trade_decision.get_decision(), exec_vols): diff --git a/qlib/rl/trainer/trainer.py b/qlib/rl/trainer/trainer.py index 66a185447db..7573b339116 100644 --- a/qlib/rl/trainer/trainer.py +++ b/qlib/rl/trainer/trainer.py @@ -7,7 +7,7 @@ import copy from contextlib import AbstractContextManager, contextmanager from pathlib import Path -from typing import Any, Dict, Iterable, List, Sequence, TypeVar, cast +from typing import Any, Dict, Iterable, List, OrderedDict, Sequence, TypeVar, cast import torch @@ -152,6 +152,13 @@ def state_dict(self) -> dict: "metrics": self.metrics, } + @staticmethod + def get_policy_state_dict(ckpt_path: Path) -> OrderedDict: + state_dict = torch.load(ckpt_path, map_location="cpu") + if "vessel" in state_dict: + state_dict = state_dict["vessel"]["policy"] + return state_dict + def load_state_dict(self, state_dict: dict) -> None: """Load all states into current trainer.""" self.vessel.load_state_dict(state_dict["vessel"]) diff --git a/qlib/rl/utils/env_wrapper.py b/qlib/rl/utils/env_wrapper.py index f082f3b0131..e0c009b7bde 100644 --- a/qlib/rl/utils/env_wrapper.py +++ b/qlib/rl/utils/env_wrapper.py @@ -48,24 +48,9 @@ class EnvWrapperStatus(TypedDict): reward_history: list -class BaseEnvWrapper( +class EnvWrapper( gym.Env[ObsType, PolicyActType], Generic[InitialStateType, StateType, ActType, ObsType, PolicyActType], -): - """Base env wrapper for RL environments. It has two implementations: - - EnvWrapper: Qlib-based RL environment used in training. - - CollectDataEnvWrapper: Dummy environment used in collect_data_loop. - """ - - def __init__(self) -> None: - self.status: EnvWrapperStatus = cast(EnvWrapperStatus, None) - - def render(self, mode: str = "human") -> None: - raise NotImplementedError("Render is not implemented in BaseEnvWrapper.") - - -class EnvWrapper( - BaseEnvWrapper[InitialStateType, StateType, ActType, ObsType, PolicyActType], ): """Qlib-based RL environment, subclassing ``gym.Env``. A wrapper of components, including simulator, state-interpreter, action-interpreter, reward. @@ -129,8 +114,6 @@ def __init__( # 3. Avoid circular reference. # 4. When the components get serialized, we can throw away the env without any burden. # (though this part is not implemented yet) - super().__init__() - for obj in [state_interpreter, action_interpreter, reward_fn, aux_info_collector]: if obj is not None: obj.env = weakref.proxy(self) # type: ignore @@ -263,19 +246,5 @@ def step(self, policy_action: PolicyActType, **kwargs: Any) -> Tuple[ObsType, fl info_dict = InfoDict(log=self.logger.logs(), aux_info=aux_info) return obs, rew, done, info_dict - -class CollectDataEnvWrapper(BaseEnvWrapper[InitialStateType, StateType, ActType, ObsType, PolicyActType]): - """Dummy EnvWrapper for collect_data_loop. It only has minimum interfaces to support the collect_data_loop.""" - - def reset(self, **kwargs: Any) -> None: - self.status = EnvWrapperStatus( - cur_step=0, - done=False, - initial_state=None, - obs_history=[], - action_history=[], - reward_history=[], - ) - - def step(self, policy_action: Any = None, **kwargs: Any) -> None: - self.status["cur_step"] += 1 + def render(self, mode: str = "human") -> None: + raise NotImplementedError("Render is not implemented in EnvWrapper.") diff --git a/qlib/workflow/record_temp.py b/qlib/workflow/record_temp.py index 55ede19b9b2..5f62e775891 100644 --- a/qlib/workflow/record_temp.py +++ b/qlib/workflow/record_temp.py @@ -473,7 +473,8 @@ def _generate(self, **kwargs): self.save(**{f"positions_normal_{_freq}.pkl": positions_normal}) for _freq, indicators_normal in indicator_dict.items(): - self.save(**{f"indicators_normal_{_freq}.pkl": indicators_normal}) + self.save(**{f"indicators_normal_{_freq}.pkl": indicators_normal[0]}) + self.save(**{f"indicators_normal_{_freq}_obj.pkl": indicators_normal[1]}) for _analysis_freq in self.risk_analysis_freq: if _analysis_freq not in portfolio_metric_dict: @@ -511,7 +512,7 @@ def _generate(self, **kwargs): if _analysis_freq not in indicator_dict: warnings.warn(f"the freq {_analysis_freq} indicator is not found") else: - indicators_normal = indicator_dict.get(_analysis_freq) + indicators_normal = indicator_dict.get(_analysis_freq)[0] if self.indicator_analysis_method is None: analysis_df = indicator_analysis(indicators_normal) else: diff --git a/tests/backtest/test_file_strategy.py b/tests/backtest/test_file_strategy.py index f0497bc91f8..2e30f1a3cbd 100644 --- a/tests/backtest/test_file_strategy.py +++ b/tests/backtest/test_file_strategy.py @@ -107,7 +107,7 @@ def test_file_str(self): ) # ffr valid - ffr_dict = indicator_dict["1day"]["ffr"].to_dict() + ffr_dict = indicator_dict["1day"][0]["ffr"].to_dict() ffr_dict = {str(date).split()[0]: ffr_dict[date] for date in ffr_dict} assert np.isclose(ffr_dict["2020-01-03"], dealt_num_for_1000 / 1000) assert np.isclose(ffr_dict["2020-01-06"], 0) diff --git a/tests/backtest/test_high_freq_trading.py b/tests/backtest/test_high_freq_trading.py index 21bc4e0d472..fd934914d81 100644 --- a/tests/backtest/test_high_freq_trading.py +++ b/tests/backtest/test_high_freq_trading.py @@ -125,7 +125,7 @@ def test_trading(self): # NOTE: please refer to the docs of format_decisions # NOTE: `"track_data": True,` is very NECESSARY for collecting the decision!!!!! f_dec = format_decisions(decisions) - print(indicator["1day"]) + print(indicator["1day"][0]) if __name__ == "__main__": diff --git a/tests/rl/test_qlib_simulator.py b/tests/rl/test_qlib_simulator.py index 92ad9c0583e..382609e5e13 100644 --- a/tests/rl/test_qlib_simulator.py +++ b/tests/rl/test_qlib_simulator.py @@ -7,11 +7,11 @@ import pandas as pd import pytest -from qlib.backtest.decision import Order, OrderDir, TradeRangeByTime + +from qlib.backtest.decision import Order, OrderDir from qlib.backtest.executor import SimulatorExecutor from qlib.rl.order_execution import CategoricalActionInterpreter from qlib.rl.order_execution.simulator_qlib import SingleAssetOrderExecution -from qlib.rl.utils.env_wrapper import CollectDataEnvWrapper TOTAL_POSITION = 2100.0 @@ -183,8 +183,6 @@ def test_interpreter() -> None: order = get_order() simulator = get_simulator(order) interpreter_action = CategoricalActionInterpreter(values=NUM_EXECUTION) - interpreter_action.env = CollectDataEnvWrapper() - interpreter_action.env.reset() NUM_STEPS = 7 state = simulator.get_state() diff --git a/tests/rl/test_saoe_simple.py b/tests/rl/test_saoe_simple.py index 22bd0390963..32d6b4d6e4b 100644 --- a/tests/rl/test_saoe_simple.py +++ b/tests/rl/test_saoe_simple.py @@ -20,7 +20,6 @@ from qlib.rl.order_execution import * from qlib.rl.trainer import backtest, train from qlib.rl.utils import ConsoleWriter, CsvWriter, EnvWrapperStatus -from qlib.rl.utils.env_wrapper import CollectDataEnvWrapper pytestmark = pytest.mark.skipif(sys.version_info < (3, 8), reason="Pickle styled data only supports Python >= 3.8") @@ -186,10 +185,6 @@ class EmulateEnvWrapper(NamedTuple): assert np.sum(obs["data_processed"][60:]) == 0 # second step: action - interpreter_action.env = CollectDataEnvWrapper() - interpreter_action_twap.env = CollectDataEnvWrapper() - interpreter_action.env.reset() - interpreter_action_twap.env.reset() action = interpreter_action(simulator.get_state(), 1) assert action == 15 / 20 @@ -260,8 +255,6 @@ def test_twap_strategy(finite_env_type): state_interp = FullHistoryStateInterpreter(13, 390, 5, PickleProcessedDataProvider(FEATURE_DATA_DIR)) action_interp = TwapRelativeActionInterpreter() - action_interp.env = CollectDataEnvWrapper() - action_interp.env.reset() policy = AllOne(state_interp.observation_space, action_interp.action_space) csv_writer = CsvWriter(Path(__file__).parent / ".output") @@ -291,8 +284,6 @@ def test_cn_ppo_strategy(): state_interp = FullHistoryStateInterpreter(8, 240, 6, PickleProcessedDataProvider(CN_FEATURE_DATA_DIR)) action_interp = CategoricalActionInterpreter(4) - action_interp.env = CollectDataEnvWrapper() - action_interp.env.reset() network = Recurrent(state_interp.observation_space) policy = PPO(network, state_interp.observation_space, action_interp.action_space, 1e-4) policy.load_state_dict(torch.load(CN_POLICY_WEIGHTS_DIR / "ppo_recurrent_30min.pth", map_location="cpu")) @@ -324,8 +315,6 @@ def test_ppo_train(): state_interp = FullHistoryStateInterpreter(8, 240, 6, PickleProcessedDataProvider(CN_FEATURE_DATA_DIR)) action_interp = CategoricalActionInterpreter(4) - action_interp.env = CollectDataEnvWrapper() - action_interp.env.reset() network = Recurrent(state_interp.observation_space) policy = PPO(network, state_interp.observation_space, action_interp.action_space, 1e-4) From e182124e75a020ed992fc89c4d61e327db84e87c Mon Sep 17 00:00:00 2001 From: Lewen Wang <49936435+lwwang1995@users.noreply.github.com> Date: Thu, 10 Nov 2022 21:10:44 +0800 Subject: [PATCH 09/15] Add docs for qlib.rl (#1322) * Add docs for qlib.rl * Update docs for qlib.rl * Add homepage introduct to RL framework * Update index Link * Fix Icon * typo * Update catelog * Update docs for qlib.rl * Update docs for qlib.rl * Update figure * Update docs for qlib.rl * Update setup.py * FIx setup.py * Update docs and fix some typos * Fix the reference to RL docs * Update framework.svg * Update framework.svg * Update framework.svg * Update docs for qlibrl. * Update docs for qlibrl. * Update docs for Qlibrl. * Update docs for qlibrl. * Update docs for qlibrl. * Update docs for qlibrl. * Add new framework * Update jpg * Update framework.svg * Update framework.svg * Update Qlib framework and description * Update grammar * Update README.md * Update README.md * Update docs/component/rl.rst Co-authored-by: you-n-g * Update docs/component/rl.rst Co-authored-by: you-n-g * Update docs for qlib.rl * Change theme for docs. * Update docs for qlib.rl * Update docs for qlib.rl * Update docs for qlib.rl * Update docs for qlib.rl. * Update docs for qlib.rl * Update docs for qlib.rl * Update docs for qlib.rl Co-authored-by: Young Co-authored-by: you-n-g --- README.md | 32 ++-- docs/_static/img/QlibRL_framework.png | Bin 0 -> 92968 bytes docs/_static/img/RL_framework.png | Bin 0 -> 30494 bytes docs/_static/img/framework-abstract.jpg | Bin 0 -> 66107 bytes docs/_static/img/framework.svg | 2 +- docs/component/highfreq.rst | 20 +- docs/component/rl/framework.rst | 45 +++++ docs/component/rl/overall.rst | 50 +++++ docs/component/rl/quickstart.rst | 175 ++++++++++++++++++ docs/component/rl/toctree.rst | 10 + docs/index.rst | 5 +- docs/introduction/introduction.rst | 66 ++++--- docs/reference/api.rst | 33 ++++ examples/rl/README.md | 4 +- .../rl/experiment_config/backtest/config.py | 53 ------ .../rl/experiment_config/backtest/config.yml | 57 ++++++ .../rl/experiment_config/backtest/twap.yml | 21 --- qlib/rl/__init__.py | 6 + qlib/rl/order_execution/__init__.py | 35 +++- qlib/rl/strategy/__init__.py | 3 + qlib/rl/trainer/__init__.py | 2 + setup.py | 11 +- 22 files changed, 494 insertions(+), 136 deletions(-) create mode 100644 docs/_static/img/QlibRL_framework.png create mode 100644 docs/_static/img/RL_framework.png create mode 100644 docs/_static/img/framework-abstract.jpg create mode 100644 docs/component/rl/framework.rst create mode 100644 docs/component/rl/overall.rst create mode 100644 docs/component/rl/quickstart.rst create mode 100644 docs/component/rl/toctree.rst delete mode 100644 examples/rl/experiment_config/backtest/config.py create mode 100644 examples/rl/experiment_config/backtest/config.yml delete mode 100644 examples/rl/experiment_config/backtest/twap.yml diff --git a/README.md b/README.md index 59338ef3725..f1b75340233 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ Recent released features | Feature | Status | | -- | ------ | +| RL Learning Framework | :hammer: :chart_with_upwards_trend: Released on Oct 20, 2022. [#1322](https://github.com/microsoft/qlib/pull/1322), [#1316](https://github.com/microsoft/qlib/pull/1316),[#1299](https://github.com/microsoft/qlib/pull/1299),[#1263](https://github.com/microsoft/qlib/pull/1263), [#1244](https://github.com/microsoft/qlib/pull/1244), [#1169](https://github.com/microsoft/qlib/pull/1169), [#1125](https://github.com/microsoft/qlib/pull/1125), [#1076](https://github.com/microsoft/qlib/pull/1076)| | 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 | @@ -67,6 +68,7 @@ For more details, please refer to our paper ["Qlib: An AI-oriented Quantitative
  • Auto Quant Research Workflow
  • Building Customized Quant Research Workflow by Code
  • Quant Dataset Zoo
  • +
  • Learning Framework
  • More About Qlib
  • Offline Mode and Online Mode
      @@ -105,21 +107,16 @@ Your feedbacks about the features are very important. # Framework of Qlib
      - +
      -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. +The high-level framework of Qlib can be found above(users can find the [detailed framework](https://qlib.readthedocs.io/en/latest/introduction/introduction.html#framework) of Qlib's design when getting into nitty gritty). +The components are designed as loose-coupled modules, and each component could be used stand-alone. -| Name | Description | -| ------ | ----- | -| `Infrastructure` layer | `Infrastructure` layer provides underlying support for Quant research. `DataServer` provides a high-performance infrastructure for users to manage and retrieve raw data. `Trainer` provides a flexible interface to control the training process of models, which enable algorithms to control the training process. | -| `Workflow` layer | `Workflow` layer covers the whole workflow of quantitative investment. `Information Extractor` extracts data for models. `Forecast Model` focuses on producing all kinds of forecast signals (e.g. _alpha_, risk) for other modules. With these signals `Decision Generator` will generate the target trading decisions(i.e. portfolio, orders) to be executed by `Execution Env` (i.e. the trading market). There may be multiple levels of `Trading Agent` and `Execution Env` (e.g. an _order executor trading agent and intraday order execution environment_ could behave like an interday trading environment and nested in _daily portfolio management trading agent and interday trading environment_ ) | -| `Interface` layer | `Interface` layer tries to present a user-friendly interface for the underlying system. `Analyser` module will provide users detailed analysis reports of forecasting signals, portfolios and execution results | - -* The modules with hand-drawn style are under development and will be released in the future. -* The modules with dashed borders are highly user-customizable and extendible. - -(p.s. framework image is created with https://draw.io/) +Qlib provides a strong infrastructure to support Quant research. [Data](https://qlib.readthedocs.io/en/latest/component/data.html) is always an important part. +A strong learning framework is designed to support diverse learning paradigms (e.g. [reinforcement learning](https://qlib.readthedocs.io/en/latest/component/rl.html), [supervised learning](https://qlib.readthedocs.io/en/latest/component/workflow.html#model-section)) and patterns at different levels(e.g. [market dynamic modeling](https://qlib.readthedocs.io/en/latest/component/meta.html)). +By modeling the market, [trading strategies](https://qlib.readthedocs.io/en/latest/component/strategy.html) will generate trade decisions that will be executed. Multiple trading strategies and executors in different levels or granularities can be [nested to be optimized and run together](https://qlib.readthedocs.io/en/latest/component/highfreq.html). +At last, a comprehensive [analysis](https://qlib.readthedocs.io/en/latest/component/report.html) will be provided and the model can be [served online](https://qlib.readthedocs.io/en/latest/component/online.html) in a low cost. # Quick Start @@ -404,6 +401,17 @@ Dataset plays a very important role in Quant. Here is a list of the datasets bui [Here](https://qlib.readthedocs.io/en/latest/advanced/alpha.html) is a tutorial to build dataset with `Qlib`. Your PR to build new Quant dataset is highly welcomed. + +# Learning Framework +Qlib is high customizable and a lot of its components are learnable. +The learnable components are instances of `Forecast Model` and `Trading Agent`. They are learned based on the `Learning Framework` layer and then applied to multiple scenarios in `Workflow` layer. +The learning framework leverages the `Workflow` layer as well(e.g. sharing `Information Extractor`, creating environments based on `Execution Env`). + +Based on learning paradigms, they can be categorized into reinforcement learning and supervised learning. +- For supervised learning, the detailed docs can be found [here](https://qlib.readthedocs.io/en/latest/component/model.html). +- For reinforcement learning, the detailed docs can be found [here](https://qlib.readthedocs.io/en/latest/component/rl.html). Qlib's RL learning framework leverages `Execution Env` in `Workflow` layer to create environments. It's worth noting that `NestedExecutor` is supported as well. This empowers users to optimize different level of strategies/models/agents together (e.g. optimizing an order execution strategy for a specific portfolio management strategy). + + # 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/). diff --git a/docs/_static/img/QlibRL_framework.png b/docs/_static/img/QlibRL_framework.png new file mode 100644 index 0000000000000000000000000000000000000000..4ff221d5ec5a01cb9964d04a9a03bc8ed1d74b36 GIT binary patch literal 92968 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%0D*%CZLEs_J-^<*x1p^xk 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 a%@O?vlkkNp|GHfoAPjVkb*i)x=)VDsVz7e% literal 0 HcmV?d00001 diff --git a/docs/_static/img/RL_framework.png b/docs/_static/img/RL_framework.png new file mode 100644 index 0000000000000000000000000000000000000000..cb972b16f82e09f17637cfb953ad0158cf5103c8 GIT binary patch literal 30494 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 -
      Trading Agent
      Trading Agent
      Meta Controller
      Meta Controller
      Analyser
      Analyser
      Decision
      Decision
      Forecast Model
      Forecast Model
      Interface
      Interface
      Multi-level Workflow
      Multi-level Workflow
      Infrastracture
      Infrastracture
      Forecasting Analyser
      Forecasting...
      Portfolio Analyser
      Portfolio A...
      Execution Analyser
      Execution...
      Information Extractor
      Information Extractor
      Online Serving
      Online Serving
      Graph
      Graph
      Event
      Event
      Factor
      Factor
      Text
      Text
      Risk
      Risk
      Alpha
      Alpha
      Data Server
      Data Server
      local
      local
      remote
      remote
      Trainer
      Trainer
      Algorithms
      Algorithms
      Auto-ML
      Auto-ML
      Model Manager
      Model Manager
      Model
      Model
      Model
      Model
      Models
      Models
      Model
      Model
      Model
      Model
      Decision Generators
      Decision Generators
      Model Interpreter
      Model Interpreter
      Decision Generator
      Decision Generator
      Order execution
      Order executi...
      Execution Results
      Execution Results
      Execution Env
      Execution Env
      Sub-workflow
      Sub-workfl...
      VWAP/Close/...
      Executor
      VWAP/Close/......
      Highly Customizable
      Module
      Highly Customiz...
      Module in development
      Module in devel...
      Explanation
      Explanation
      Sub-workflow(1) (E.g. High-frequency order execution)
      Sub-workflow(1) (E.g. High-frequ...
      Execution Env
      Execution E...
      ...
      ...
      (1)  The sub-workflow will make more fine-grained decisions according to the decision from the upper-level trading agent
      (1)  The sub-workflow will make more fine-grained decisions according to the decision from the upper-level trading agent
      Stock selection
      Stock selecti...
      Asset allocation
      Asset allocat...
      Trading
      Agent
      Trading...
      Viewer does not support full SVG 1.1
      \ No newline at end of file +
      Reinforcement  Learning
      Reinforcement  Learning
      Environment
      Environment
      Simulator
      Simulator
      Strategy
      Strategy
      Supervised-Learning-based Strategy
      Supervised-Learning-based Strategy
      Policy
      Policy
      Supervised Learning
      Supervised Learning
      Meta Controller
      Meta Controller
      Analyser
      Analyser
      Interface
      Interface
      Multi-level Workflow
      Multi-level Workflow
      Infrastracture
      Infrastracture
      Forecasting Analyser
      Forecasting...
      Portfolio Analyser
      Portfolio A...
      Execution Analyser
      Execution...
      Information Extractor
      Information Extractor
      Online Serving
      Online Serving
      Graph
      Graph
      Event
      Event
      Factor
      Factor
      Text
      Text
      Data Server
      Data Server
      local
      local
      remote
      remote
      Trainer
      Trainer
      Algorithms
      Algorithms
      Auto-ML
      Auto-ML
      Model Manager
      Model Manager
      Model
      Model
      Model
      Model
      Models
      Models
      Model
      Model
      Model
      Model
      Decision Generators
      Decision Generators
      Model Interpreter
      Model Interpreter
      Executor
      Executor
      Sub-workflow
      (NestedExecutor)
      Sub-workflow...
      Highly Customizable
      Module
      Highly Customiz...
      Module in development
      Module in devel...
      Explanation
      Explanation
      Sub-workflow(1) (E.g. High-frequency order execution nested in portfolio management)
      Sub-workflow(1) (E.g. High-fr...
      Executor
      Executor
      ...
      ...
      (1)  The sub-workflow will make more fine-grained decisions according to the decision from the upper-level trading agent
      (1)  The sub-workflow will make more fine-grained decisions according to the decision from the upper-level trading agent
      Supervised signal
      Supervised signal
      Model Traning
      Model Traning
      Forecast Model
      Forecast Model
      Risk
      Risk
      Alpha
      Alpha
      Action (Decision)
      Action (Decision)
      Learning Framework
      Learning Framework
      Information Extractor
      Information Extrac...
      Graph
      Graph
      Event
      Event
      Factor
      Factor
      Text
      Text
      OR
      OR
      Decision Generator
      Decision Generator
      Policy
      Policy
      State Intepreter
      State Intepreter
      Action Intepreter
      Action Intepreter
      State/Reward (Execution Results)
      State/Reward (Execution Results)
      Order Execution
      Order Execu...
      Portfolio Management
      Portfolio M...
      Executor
      Executor
      Portfolio Management
      Portfolio Management
      Reinforcement-Learning-based Strategy
      Reinforcement-Learning-based Strategy
      State Intepreter
      State Intepreter
      Action Intepreter
      Action Intepreter
      Decision
      Decision
      Rule-based
      Rule-based
      Portfolio Optimiaztion
      Portfolio Optimiaz...
      Decision
      Decision
      Strategy
      Strategy
      ...
      ...
      Decision
      Decision
      Portfolio management
      Portfolio management
      Order execution
      Order execution
      Asset allocation
      Asset allocation
      Decision
      Decision
      Execution Results
      Execution Results
      Execution
      Results

      Execution...
      Executor
      Executor
      Order Execution
      Order Execution
      Forecast Model
      Forecast Model
      Risk
      Risk
      Alpha
      Alpha
      Text is not SVG - cannot display
      diff --git a/docs/component/highfreq.rst b/docs/component/highfreq.rst index de385a82326..0af0fa29154 100644 --- a/docs/component/highfreq.rst +++ b/docs/component/highfreq.rst @@ -8,31 +8,33 @@ Design of Nested Decision Execution Framework for High-Frequency Trading Introduction ============ -Daily trading (e.g. portfolio management) and intraday trading (e.g. orders execution) are two hot topics in Quant investment and usually studied separately. +Daily trading (e.g. portfolio management) and intraday trading (e.g. orders execution) are two hot topics in Quant investment and are usually studied separately. To get the join trading performance of daily and intraday trading, they must interact with each other and run backtest jointly. -In order to support the joint backtest strategies in multiple levels, a corresponding framework is required. None of the publicly available high-frequency trading frameworks considers multi-level joint trading, which make the backtesting aforementioned inaccurate. +In order to support the joint backtest strategies at multiple levels, a corresponding framework is required. None of the publicly available high-frequency trading frameworks considers multi-level joint trading, which makes the backtesting aforementioned inaccurate. Besides backtesting, the optimization of strategies from different levels is not standalone and can be affected by each other. -For example, the best portfolio management strategy may change with the performance of order executions(e.g. a portfolio with higher turnover may becomes a better choice when we improve the order execution strategies). -To achieve the overall good performance , it is necessary to consider the interaction of strategies in different level. +For example, the best portfolio management strategy may change with the performance of order executions(e.g. a portfolio with higher turnover may become a better choice when we improve the order execution strategies). +To achieve overall good performance, it is necessary to consider the interaction of strategies at a different levels. -Therefore, building a new framework for trading in multiple levels becomes necessary to solve the various problems mentioned above, for which we designed a nested decision execution framework that consider the interaction of strategies. +Therefore, building a new framework for trading on multiple levels becomes necessary to solve the various problems mentioned above, for which we designed a nested decision execution framework that considers the interaction of strategies. .. image:: ../_static/img/framework.svg The design of the framework is shown in the yellow part in the middle of the figure above. Each level consists of ``Trading Agent`` and ``Execution Env``. ``Trading Agent`` has its own data processing module (``Information Extractor``), forecasting module (``Forecast Model``) and decision generator (``Decision Generator``). The trading algorithm generates the decisions by the ``Decision Generator`` based on the forecast signals output by the ``Forecast Module``, and the decisions generated by the trading algorithm are passed to the ``Execution Env``, which returns the execution results. -The frequency of trading algorithm, decision content and execution environment can be customized by users (e.g. intraday trading, daily-frequency trading, weekly-frequency trading), and the execution environment can be nested with finer-grained trading algorithm and execution environment inside (i.e. sub-workflow in the figure, e.g. daily-frequency orders can be turned into finer-grained decisions by splitting orders within the day). The flexibility of nested decision execution framework makes it easy for users to explore the effects of combining different levels of trading strategies and break down the optimization barriers between different levels of trading algorithm. +The frequency of the trading algorithm, decision content and execution environment can be customized by users (e.g. intraday trading, daily-frequency trading, weekly-frequency trading), and the execution environment can be nested with finer-grained trading algorithm and execution environment inside (i.e. sub-workflow in the figure, e.g. daily-frequency orders can be turned into finer-grained decisions by splitting orders within the day). The flexibility of the nested decision execution framework makes it easy for users to explore the effects of combining different levels of trading strategies and break down the optimization barriers between different levels of the trading algorithm. + +The optimization for the nested decision execution framework can be implemented with the support of `QlibRL `_. To know more about how to use the QlibRL, go to API Reference: `RL API <../reference/api.html#rl>`_. Example ======= -An example of nested decision execution framework for high-frequency can be found `here `_. +An example of a nested decision execution framework for high-frequency can be found `here `_. -Besides, the above examples, here are some other related work about high-frequency trading in Qlib. +Besides, the above examples, here are some other related works about high-frequency trading in Qlib. - `Prediction with high-frequency data `_ -- `Examples `_ to extract features form high-frequency data without fixed frequency. +- `Examples `_ to extract features from high-frequency data without fixed frequency. - `A paper `_ for high-frequency trading. diff --git a/docs/component/rl/framework.rst b/docs/component/rl/framework.rst new file mode 100644 index 00000000000..7edb08efd90 --- /dev/null +++ b/docs/component/rl/framework.rst @@ -0,0 +1,45 @@ +The Framework of QlibRL +======================= + +QlibRL contains a full set of components that cover the entire lifecycle of an RL pipeline, including building the simulator of the market, shaping states & actions, training policies (strategies), and backtesting strategies in the simulated environment. + +QlibRL is basically implemented with the support of Tianshou and Gym frameworks. The high-level structure of QlibRL is demonstrated below: + +.. image:: ../../_static/img/QlibRL_framework.png + :width: 600 + :align: center + +Here, we briefly introduce each component in the figure. + +EnvWrapper +------------ +EnvWrapper is the complete capsulation of the simulated environment. It receives actions from outside (policy/strategy/agent), simulates the changes in the market, and then replies rewards and updated states, thus forming an interaction loop. + +In QlibRL, EnvWrapper is a subclass of gym.Env, so it implements all necessary interfaces of gym.Env. Any classes or pipelines that accept gym.Env should also accept EnvWrapper. Developers do not need to implement their own EnvWrapper to build their own environment. Instead, they only need to implement 4 components of the EnvWrapper: + +- `Simulator` + The simulator is the core component responsible for the environment simulation. Developers could implement all the logic that is directly related to the environment simulation in the Simulator in any way they like. In QlibRL, there are already two implementations of Simulator for single asset trading: 1) ``SingleAssetOrderExecution``, which is built based on Qlib's backtest toolkits and hence considers a lot of practical trading details but is slow. 2) ``SimpleSingleAssetOrderExecution``, which is built based on a simplified trading simulator, which ignores a lot of details (e.g. trading limitations, rounding) but is quite fast. +- `State interpreter` + The state interpreter is responsible for "interpret" states in the original format (format provided by the simulator) into states in a format that the policy could understand. For example, transform unstructured raw features into numerical tensors. +- `Action interpreter` + The action interpreter is similar to the state interpreter. But instead of states, it interprets actions generated by the policy, from the format provided by the policy to the format that is acceptable to the simulator. +- `Reward function` + The reward function returns a numerical reward to the policy after each time the policy takes an action. + +EnvWrapper will organically organize these components. Such decomposition allows for better flexibility in development. For example, if the developers want to train multiple types of policies in the same environment, they only need to design one simulator and design different state interpreters/action interpreters/reward functions for different types of policies. + +QlibRL has well-defined base classes for all these 4 components. All the developers need to do is define their own components by inheriting the base classes and then implementing all interfaces required by the base classes. The API for the above base components can be found `here <../../reference/api.html#module-qlib.rl>`_. + +Policy +------------ +QlibRL directly uses Tianshou's policy. Developers could use policies provided by Tianshou off the shelf, or implement their own policies by inheriting Tianshou's policies. + +Training Vessel & Trainer +------------------------- +As stated by their names, training vessels and trainers are helper classes used in training. A training vessel is a ship that contains a simulator/interpreters/reward function/policy, and it controls algorithm-related parts of training. Correspondingly, the trainer is responsible for controlling the runtime parts of training. + +As you may have noticed, a training vessel itself holds all the required components to build an EnvWrapper rather than holding an instance of EnvWrapper directly. This allows the training vessel to create duplicates of EnvWrapper dynamically when necessary (for example, under parallel training). + +With a training vessel, the trainer could finally launch the training pipeline by simple, Scikit-learn-like interfaces (i.e., ``trainer.fit()``). + +The API for Trainer and TrainingVessel and can be found `here <../../reference/api.html#module-qlib.rl.trainer>`_. \ No newline at end of file diff --git a/docs/component/rl/overall.rst b/docs/component/rl/overall.rst new file mode 100644 index 00000000000..4f59dd17a75 --- /dev/null +++ b/docs/component/rl/overall.rst @@ -0,0 +1,50 @@ +===================================================== +Reinforcement Learning in Quantitative Trading +===================================================== + +Reinforcement Learning +====================== +Different from supervised learning tasks such as classification tasks and regression tasks. Another important paradigm in machine learning is Reinforcement Learning, +which attempts to optimize an accumulative numerical reward signal by directly interacting with the environment under a few assumptions such as Markov Decision Process(MDP). + +As demonstrated in the following figure, an RL system consists of four elements, 1)the agent 2) the environment the agent interacts with 3) the policy that the agent follows to take actions on the environment and 4)the reward signal from the environment to the agent. +In general, the agent can perceive and interpret its environment, take actions and learn through reward, to seek long-term and maximum overall reward to achieve an optimal solution. + +.. image:: ../../_static/img/RL_framework.png + :width: 300 + :align: center + +RL attempts to learn to produce actions by trial and error. +By sampling actions and then observing which one leads to our desired outcome, a policy is obtained to generate optimal actions. +In contrast to supervised learning, RL learns this not from a label but from a time-delayed label called a reward. +This scalar value lets us know whether the current outcome is good or bad. +In a word, the target of RL is to take actions to maximize reward. + +The Qlib Reinforcement Learning toolkit (QlibRL) is an RL platform for quantitative investment, which provides support to implement the RL algorithms in Qlib. + + +Potential Application Scenarios in Quantitative Trading +======================================================= +RL methods have already achieved outstanding achievement in many applications, such as game playing, resource allocating, recommendation, marketing and advertising, etc. +Investment is always a continuous process, taking the stock market as an example, investors need to control their positions and stock holdings by one or more buying and selling behaviors, to maximize the investment returns. +Besides, each buy and sell decision is made by investors after fully considering the overall market information and stock information. +From the view of an investor, the process could be described as a continuous decision-making process generated according to interaction with the market, such problems could be solved by the RL algorithms. +Following are some scenarios where RL can potentially be used in quantitative investment. + +Portfolio Construction +---------------------- +Portfolio construction is a process of selecting securities optimally by taking a minimum risk to achieve maximum returns. With an RL-based solution, an agent allocates stocks at every time step by obtaining information for each stock and the market. The key is to develop of policy for building a portfolio and make the policy able to pick the optimal portfolio. + +Order Execution +--------------- +As a fundamental problem in algorithmic trading, order execution aims at fulfilling a specific trading order, either liquidation or acquirement, for a given instrument. Essentially, the goal of order execution is twofold: it not only requires to fulfill the whole order but also targets a more economical execution with maximizing profit gain (or minimizing capital loss). The order execution with only one order of liquidation or acquirement is called single-asset order execution. + +Considering stock investment always aim to pursue long-term maximized profits, it usually manifests as a sequential process of continuously adjusting the asset portfolios, execution for multiple orders, including order of liquidation and acquirement, brings more constraints and makes the sequence of execution for different orders should be considered, e.g. before executing an order to buy some stocks, we have to sell at least one stock. The order execution with multiple assets is called multi-asset order execution. + +According to the order execution’s trait of sequential decision-making, an RL-based solution could be applied to solve the order execution. With an RL-based solution, an agent optimizes execution strategy by interacting with the market environment. + +With QlibRL, the RL algorithm in the above scenarios can be easily implemented. + +Nested Portfolio Construction and Order Executor +------------------------------------------------ +QlibRL makes it possible to jointly optimize different levels of strategies/models/agents. Take `Nested Decision Execution Framework `_ as an example, the optimization of order execution strategy and portfolio management strategies can interact with each other to maximize returns. diff --git a/docs/component/rl/quickstart.rst b/docs/component/rl/quickstart.rst new file mode 100644 index 00000000000..5e98e3baff6 --- /dev/null +++ b/docs/component/rl/quickstart.rst @@ -0,0 +1,175 @@ + +Quick Start +============ +.. currentmodule:: qlib + +QlibRL provides an example of an implementation of a single asset order execution task and the following is an example of the config file to train with QlibRL. + +.. code-block:: yaml + + simulator: + # Each step contains 30mins + time_per_step: 30 + # Upper bound of volume, should be null or a float between 0 and 1, if it is a float, represent upper bound is calculated by the percentage of the market volume + vol_limit: null + env: + # Concurrent environment workers. + concurrency: 1 + # dummy or subproc or shmem. Corresponding to `parallelism in tianshou `_. + parallel_mode: dummy + action_interpreter: + class: CategoricalActionInterpreter + kwargs: + # Candidate actions, it can be a list with length L: [a_1, a_2,..., a_L] or an integer n, in which case the list of length n+1 is auto-generated, i.e., [0, 1/n, 2/n,..., n/n]. + values: 14 + # Total number of steps (an upper-bound estimation) + max_step: 8 + module_path: qlib.rl.order_execution.interpreter + state_interpreter: + class: FullHistoryStateInterpreter + kwargs: + # Number of dimensions in data. + data_dim: 6 + # Equal to the total number of records. For example, in SAOE per minute, data_ticks is the length of the day in minutes. + data_ticks: 240 + # The total number of steps (an upper-bound estimation). For example, 390min / 30min-per-step = 13 steps. + max_step: 8 + # Provider of the processed data. + processed_data_provider: + class: PickleProcessedDataProvider + module_path: qlib.rl.data.pickle_styled + kwargs: + data_dir: ./data/pickle_dataframe/feature + module_path: qlib.rl.order_execution.interpreter + reward: + class: PAPenaltyReward + kwargs: + # The penalty for a large volume in a short time. + penalty: 100.0 + module_path: qlib.rl.order_execution.reward + data: + source: + order_dir: ./data/training_order_split + data_dir: ./data/pickle_dataframe/backtest + # number of time indexes + total_time: 240 + # start time index + default_start_time: 0 + # end time index + default_end_time: 240 + proc_data_dim: 6 + num_workers: 0 + queue_size: 20 + network: + class: Recurrent + module_path: qlib.rl.order_execution.network + policy: + class: PPO + kwargs: + lr: 0.0001 + module_path: qlib.rl.order_execution.policy + runtime: + seed: 42 + use_cuda: false + trainer: + max_epoch: 2 + # Number of episodes collected in each training iteration + repeat_per_collect: 5 + earlystop_patience: 2 + # Episodes per collect at training. + episode_per_collect: 20 + batch_size: 16 + # Perform validation every n iterations + val_every_n_epoch: 1 + checkpoint_path: ./checkpoints + checkpoint_every_n_iters: 1 + + +And the config file for backtesting: + +.. code-block:: yaml + + order_file: ./data/backtest_orders.csv + start_time: "9:45" + end_time: "14:44" + qlib: + provider_uri_1min: ./data/bin + feature_root_dir: ./data/pickle + # feature generated by today's information + feature_columns_today: [ + "$open", "$high", "$low", "$close", "$vwap", "$volume", + ] + # feature generated by yesterday's information + feature_columns_yesterday: [ + "$open_v1", "$high_v1", "$low_v1", "$close_v1", "$vwap_v1", "$volume_v1", + ] + exchange: + # the expression for buying and selling stock limitation + limit_threshold: ['$close == 0', '$close == 0'] + # deal price for buying and selling + deal_price: ["If($close == 0, $vwap, $close)", "If($close == 0, $vwap, $close)"] + volume_threshold: + # volume limits are both buying and selling, "cum" means that this is a cumulative value over time + all: ["cum", "0.2 * DayCumsum($volume, '9:45', '14:44')"] + # the volume limits of buying + buy: ["current", "$close"] + # the volume limits of selling, "current" means that this is a real-time value and will not accumulate over time + sell: ["current", "$close"] + strategies: + 30min: + class: TWAPStrategy + module_path: qlib.contrib.strategy.rule_strategy + kwargs: {} + 1day: + class: SAOEIntStrategy + module_path: qlib.rl.order_execution.strategy + kwargs: + state_interpreter: + class: FullHistoryStateInterpreter + module_path: qlib.rl.order_execution.interpreter + kwargs: + max_step: 8 + data_ticks: 240 + data_dim: 6 + processed_data_provider: + class: PickleProcessedDataProvider + module_path: qlib.rl.data.pickle_styled + kwargs: + data_dir: ./data/pickle_dataframe/feature + action_interpreter: + class: CategoricalActionInterpreter + module_path: qlib.rl.order_execution.interpreter + kwargs: + values: 14 + max_step: 8 + network: + class: Recurrent + module_path: qlib.rl.order_execution.network + kwargs: {} + policy: + class: PPO + module_path: qlib.rl.order_execution.policy + kwargs: + lr: 1.0e-4 + # Local path to the latest model. The model is generated during training, so please run training first if you want to run backtest with a trained policy. You could also remove this parameter file to run backtest with a randomly initialized policy. + weight_file: ./checkpoints/latest.pth + # Concurrent environment workers. + concurrency: 5 + +With the above config files, you can start training the agent by the following command: + +.. code-block:: console + + $ python -m qlib.rl.contrib.train_onpolicy.py --config_path train_config.yml + +After the training, you can backtest with the following command: + +.. code-block:: console + + $ python -m qlib.rl.contrib.backtest.py --config_path backtest_config.yml + +In that case, :class:`~qlib.rl.order_execution.simulator_qlib.SingleAssetOrderExecution` and :class:`~qlib.rl.order_execution.simulator_simple.SingleAssetOrderExecutionSimple` as examples for simulator, :class:`qlib.rl.order_execution.interpreter.FullHistoryStateInterpreter` and :class:`qlib.rl.order_execution.interpreter.CategoricalActionInterpreter` as examples for interpreter, :class:`qlib.rl.order_execution.policy.PPO` as an example for policy, and :class:`qlib.rl.order_execution.reward.PAPenaltyReward` as an example for reward. +For the single asset order execution task, if developers have already defined their simulator/interpreters/reward function/policy, they could launch the training and backtest pipeline by simply modifying the corresponding settings in the config files. +The details about the example can be found `here `_. + +In the future, we will provide more examples for different scenarios such as RL-based portfolio construction. diff --git a/docs/component/rl/toctree.rst b/docs/component/rl/toctree.rst new file mode 100644 index 00000000000..d79d5e060da --- /dev/null +++ b/docs/component/rl/toctree.rst @@ -0,0 +1,10 @@ +.. _rl: + +======================================================================== +Reinforcement Learning in Quantitative Trading +======================================================================== + +.. toctree:: + Overall + Quick Start + Framework diff --git a/docs/index.rst b/docs/index.rst index 71ed8ccec55..0d8cad81ada 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -33,7 +33,7 @@ Document Structure .. toctree:: :maxdepth: 3 - :caption: COMPONENTS: + :caption: MAIN COMPONENTS: Workflow: Workflow Management Data Layer: Data Framework & Usage @@ -44,10 +44,11 @@ Document Structure Qlib Recorder: Experiment Management Analysis: Evaluation & Results Analysis Online Serving: Online Management & Strategy & Tool + Reinforcement Learning .. toctree:: :maxdepth: 3 - :caption: ADVANCED TOPICS: + :caption: OTHER COMPONENTS/FEATURES/TOPICS: Building Formulaic Alphas Online & Offline mode diff --git a/docs/introduction/introduction.rst b/docs/introduction/introduction.rst index 8ca2b41be07..52d58e1639c 100644 --- a/docs/introduction/introduction.rst +++ b/docs/introduction/introduction.rst @@ -15,38 +15,56 @@ With ``Qlib``, users can easily try their ideas to create better Quant investmen Framework ========= + .. image:: ../_static/img/framework.svg :align: center At the module level, Qlib is a platform that consists of above components. The components are designed as loose-coupled modules and each component could be used stand-alone. +This framework may be intimidating for new users to Qlib. It tries to accurately include a lot of details of Qlib's design. +For users new to Qlib, you can skip it first and read it later. + + +=========================== ============================================================================== +Name Description +=========================== ============================================================================== +`Infrastructure` layer `Infrastructure` layer provides underlying support for Quant research. + `DataServer` provides high-performance infrastructure for users to manage + and retrieve raw data. `Trainer` provides flexible interface to control + the training process of models which enable algorithms controlling the + training process. -======================== ============================================================================== -Name Description -======================== ============================================================================== -`Infrastructure` layer `Infrastructure` layer provides underlying support for Quant research. - `DataServer` provides high-performance infrastructure for users to manage - and retrieve raw data. `Trainer` provides flexible interface to control - the training process of models which enable algorithms controlling the - training process. - -`Workflow` layer `Workflow` layer covers the whole workflow of quantitative investment. - `Information Extractor` extracts data for models. `Forecast Model` focuses - on producing all kinds of forecast signals (e.g. *alpha*, risk) for other - modules. With these signals `Decision Generator` will generate the target - trading decisions(i.e. portfolio, orders) to be executed by `Execution Env` - (i.e. the trading market). There may be multiple levels of `Trading Agent` - and `Execution Env` (e.g. an *order executor trading agent and intraday - order execution environment* could behave like an interday trading - environment and nested in *daily portfolio management trading agent and - interday trading environment* ) - -`Interface` layer `Interface` layer tries to present a user-friendly interface for the underlying - system. `Analyser` module will provide users detailed analysis reports of - forecasting signals, portfolios and execution results -======================== ============================================================================== +`Learning Framework` layer The `Forecast Model` and `Trading Agent` are learnable. They are learned + based on the `Learning Framework` layer and then applied to multiple scenarios + in `Workflow` layer. The supported learning paradigms can be categorized into + reinforcement learning and supervised learning. The learning framework + leverages the `Workflow` layer as well(e.g. sharing `Information Extractor`, + creating environments based on `Execution Env`). + +`Workflow` layer `Workflow` layer covers the whole workflow of quantitative investment. + Both supervised-learning-based strategies and RL-based Strategies + are supported. + `Information Extractor` extracts data for models. `Forecast Model` focuses + on producing all kinds of forecast signals (e.g. *alpha*, risk) for other + modules. With these signals `Decision Generator` will generate the target + trading decisions(i.e. portfolio, orders) + If RL-based Strategies are adopted, the `Policy` is learned in a end-to-end way, + the trading deicsions are generated directly. + Decisions will be executed by `Execution Env` + (i.e. the trading market). There may be multiple levels of `Strategy` + and `Executor` (e.g. an *order executor trading strategy and intraday order executor* + could behave like an interday trading loop and be nested in + *daily portfolio management trading strategy and interday trading executor* + trading loop) + +`Interface` layer `Interface` layer tries to present a user-friendly interface for the underlying + system. `Analyser` module will provide users detailed analysis reports of + forecasting signals, portfolios and execution results +=========================== ============================================================================== - The modules with hand-drawn style are under development and will be released in the future. - The modules with dashed borders are highly user-customizable and extendible. + +(p.s. framework image is created with https://draw.io/) diff --git a/docs/reference/api.rst b/docs/reference/api.rst index 06d89b9a896..98f50fc281e 100644 --- a/docs/reference/api.rst +++ b/docs/reference/api.rst @@ -256,3 +256,36 @@ Serializable .. automodule:: qlib.utils.serial.Serializable :members: + +RL +============== + +Base Component +-------------- +.. automodule:: qlib.rl + :members: + :imported-members: + +Strategy +-------- +.. automodule:: qlib.rl.strategy + :members: + :imported-members: + +Trainer +------- +.. automodule:: qlib.rl.trainer + :members: + :imported-members: + +Order Execution +--------------- +.. automodule:: qlib.rl.order_execution + :members: + :imported-members: + +Utils +--------------- +.. automodule:: qlib.rl.utils + :members: + :imported-members: \ No newline at end of file diff --git a/examples/rl/README.md b/examples/rl/README.md index db5cdf20d79..d8b4f4e4934 100644 --- a/examples/rl/README.md +++ b/examples/rl/README.md @@ -41,7 +41,7 @@ data Run: ``` -python ../../qlib/rl/contrib/train_onpolicy.py --config_path ./experiment_config/training/config.yml +python -m qlib.rl.contrib.train_onpolicy.py --config_path ./experiment_config/training/config.yml ``` After training, checkpoints will be stored under `checkpoints/`. @@ -49,7 +49,7 @@ After training, checkpoints will be stored under `checkpoints/`. ## Run backtest ``` -python ../../qlib/rl/contrib/backtest.py --config_path ./experiment_config/backtest/config.py +python -m qlib.rl.contrib.backtest.py --config_path ./experiment_config/backtest/config.yml ``` The backtest workflow will use the trained model in `checkpoints/`. The backtest summary can be found in `outputs/`. diff --git a/examples/rl/experiment_config/backtest/config.py b/examples/rl/experiment_config/backtest/config.py deleted file mode 100644 index 9ac8357895c..00000000000 --- a/examples/rl/experiment_config/backtest/config.py +++ /dev/null @@ -1,53 +0,0 @@ -_base_ = ["./twap.yml"] - -strategies = { - "_delete_": True, - "30min": { - "class": "TWAPStrategy", - "module_path": "qlib.contrib.strategy.rule_strategy", - "kwargs": {}, - }, - "1day": { - "class": "SAOEIntStrategy", - "module_path": "qlib.rl.order_execution.strategy", - "kwargs": { - "state_interpreter": { - "class": "FullHistoryStateInterpreter", - "module_path": "qlib.rl.order_execution.interpreter", - "kwargs": { - "max_step": 8, - "data_ticks": 240, - "data_dim": 6, - "processed_data_provider": { - "class": "PickleProcessedDataProvider", - "module_path": "qlib.rl.data.pickle_styled", - "kwargs": { - "data_dir": "./data/pickle_dataframe/feature", - }, - }, - }, - }, - "action_interpreter": { - "class": "CategoricalActionInterpreter", - "module_path": "qlib.rl.order_execution.interpreter", - "kwargs": { - "values": 14, - "max_step": 8, - }, - }, - "network": { - "class": "Recurrent", - "module_path": "qlib.rl.order_execution.network", - "kwargs": {}, - }, - "policy": { - "class": "PPO", - "module_path": "qlib.rl.order_execution.policy", - "kwargs": { - "lr": 1.0e-4, - "weight_file": "./checkpoints/latest.pth", - }, - }, - }, - }, -} diff --git a/examples/rl/experiment_config/backtest/config.yml b/examples/rl/experiment_config/backtest/config.yml new file mode 100644 index 00000000000..418780c2cc5 --- /dev/null +++ b/examples/rl/experiment_config/backtest/config.yml @@ -0,0 +1,57 @@ +order_file: ./data/backtest_orders.csv +start_time: "9:45" +end_time: "14:44" +qlib: + provider_uri_1min: ./data/bin + feature_root_dir: ./data/pickle + feature_columns_today: [ + "$open", "$high", "$low", "$close", "$vwap", "$volume", + ] + feature_columns_yesterday: [ + "$open_v1", "$high_v1", "$low_v1", "$close_v1", "$vwap_v1", "$volume_v1", + ] +exchange: + limit_threshold: ['$close == 0', '$close == 0'] + deal_price: ["If($close == 0, $vwap, $close)", "If($close == 0, $vwap, $close)"] + volume_threshold: + all: ["cum", "0.2 * DayCumsum($volume, '9:45', '14:44')"] + buy: ["current", "$close"] + sell: ["current", "$close"] +strategies: + 30min: + class: TWAPStrategy + module_path: qlib.contrib.strategy.rule_strategy + kwargs: {} + 1day: + class: SAOEIntStrategy + module_path: qlib.rl.order_execution.strategy + kwargs: + state_interpreter: + class: FullHistoryStateInterpreter + module_path: qlib.rl.order_execution.interpreter + kwargs: + max_step: 8 + data_ticks: 240 + data_dim: 6 + processed_data_provider: + class: PickleProcessedDataProvider + module_path: qlib.rl.data.pickle_styled + kwargs: + data_dir: ./data/pickle_dataframe/feature + action_interpreter: + class: CategoricalActionInterpreter + module_path: qlib.rl.order_execution.interpreter + kwargs: + values: 14 + max_step: 8 + network: + class: Recurrent + module_path: qlib.rl.order_execution.network + kwargs: {} + policy: + class: PPO + module_path: qlib.rl.order_execution.policy + kwargs: + lr: 1.0e-4 + weight_file: ./checkpoints/latest.pth +concurrency: 5 diff --git a/examples/rl/experiment_config/backtest/twap.yml b/examples/rl/experiment_config/backtest/twap.yml deleted file mode 100644 index e0c342502b7..00000000000 --- a/examples/rl/experiment_config/backtest/twap.yml +++ /dev/null @@ -1,21 +0,0 @@ -order_file: ./data/backtest_orders.csv -start_time: "9:45" -end_time: "14:44" -qlib: - provider_uri_1min: ./data/bin - feature_root_dir: ./data/pickle - feature_columns_today: [ - "$open", "$high", "$low", "$close", "$vwap", "$volume", - ] - feature_columns_yesterday: [ - "$open_v1", "$high_v1", "$low_v1", "$close_v1", "$vwap_v1", "$volume_v1", - ] -exchange: - limit_threshold: ['$close == 0', '$close == 0'] - deal_price: ["If($close == 0, $vwap, $close)", "If($close == 0, $vwap, $close)"] - volume_threshold: - all: ["cum", "0.2 * DayCumsum($volume, '9:45', '14:44')"] - buy: ["current", "$close"] - sell: ["current", "$close"] -strategies: {} # Placeholder -concurrency: 5 diff --git a/qlib/rl/__init__.py b/qlib/rl/__init__.py index 59e481eb93d..a12afc39960 100644 --- a/qlib/rl/__init__.py +++ b/qlib/rl/__init__.py @@ -1,2 +1,8 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. + +from .interpreter import Interpreter, StateInterpreter, ActionInterpreter +from .reward import Reward, RewardCombination +from .simulator import Simulator + +__all__ = ["Interpreter", "StateInterpreter", "ActionInterpreter", "Reward", "RewardCombination", "Simulator"] diff --git a/qlib/rl/order_execution/__init__.py b/qlib/rl/order_execution/__init__.py index b7b47c3d150..318c774230d 100644 --- a/qlib/rl/order_execution/__init__.py +++ b/qlib/rl/order_execution/__init__.py @@ -6,8 +6,33 @@ Multi-asset is on the way. """ -from .interpreter import * -from .network import * -from .policy import * -from .reward import * -from .simulator_simple import * +from .interpreter import ( + FullHistoryStateInterpreter, + CurrentStepStateInterpreter, + CategoricalActionInterpreter, + TwapRelativeActionInterpreter, +) +from .network import Recurrent +from .policy import AllOne, PPO +from .reward import PAPenaltyReward +from .simulator_simple import SingleAssetOrderExecutionSimple +from .state import SAOEStateAdapter, SAOEMetrics, SAOEState +from .strategy import SAOEStrategy, ProxySAOEStrategy, SAOEIntStrategy + +__all__ = [ + "FullHistoryStateInterpreter", + "CurrentStepStateInterpreter", + "CategoricalActionInterpreter", + "TwapRelativeActionInterpreter", + "Recurrent", + "AllOne", + "PPO", + "PAPenaltyReward", + "SingleAssetOrderExecutionSimple", + "SAOEStateAdapter", + "SAOEMetrics", + "SAOEState", + "SAOEStrategy", + "ProxySAOEStrategy", + "SAOEIntStrategy", +] diff --git a/qlib/rl/strategy/__init__.py b/qlib/rl/strategy/__init__.py index 59e481eb93d..26e12580bac 100644 --- a/qlib/rl/strategy/__init__.py +++ b/qlib/rl/strategy/__init__.py @@ -1,2 +1,5 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. +from .single_order import SingleOrderStrategy + +__all__ = ["SingleOrderStrategy"] diff --git a/qlib/rl/trainer/__init__.py b/qlib/rl/trainer/__init__.py index 0a197b37812..4c5121ececb 100644 --- a/qlib/rl/trainer/__init__.py +++ b/qlib/rl/trainer/__init__.py @@ -7,3 +7,5 @@ from .callbacks import Checkpoint, EarlyStopping from .trainer import Trainer from .vessel import TrainingVessel, TrainingVesselBase + +__all__ = ["Trainer", "TrainingVessel", "TrainingVesselBase", "Checkpoint", "EarlyStopping", "train", "backtest"] diff --git a/setup.py b/setup.py index 8ce3b93f6ec..a796ecf4b7a 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,5 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. -import io import os import numpy @@ -26,9 +25,6 @@ def get_version(rel_path: str) -> str: DESCRIPTION = "A Quantitative-research Platform" REQUIRES_PYTHON = ">=3.5.0" -from pathlib import Path -from shutil import copyfile - VERSION = get_version("qlib/__init__.py") # Detect Cython @@ -148,15 +144,16 @@ def get_version(rel_path: str) -> str: # References: https://github.com/python/typeshed/issues/8799 "mypy<0.981", "flake8", + # The 5.0.0 version of importlib-metadata removed the deprecated endpoint, + # which prevented flake8 from working properly, so we restricted the version of importlib-metadata. + # To help ensure the dependencies of flake8 https://github.com/python/importlib_metadata/issues/406 + "importlib-metadata<5.0.0", "readthedocs_sphinx_ext", "cmake", "lxml", "baostock", "yahooquery", "beautifulsoup4", - # The 5.0.0 version of importlib-metadata removed the deprecated endpoint, - # which prevented flake8 from working properly, so we restricted the version of importlib-metadata. - "importlib-metadata<5.0.0", "tianshou", "gym>=0.24", # If you do not put gym at the end, gym will degrade causing pytest results to fail. ], From b46bf8283d6b2564384871cc3df555d191fb1b5b Mon Sep 17 00:00:00 2001 From: you-n-g Date: Thu, 10 Nov 2022 21:13:05 +0800 Subject: [PATCH 10/15] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f1b75340233..fd1c4b70130 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ Recent released features | Feature | Status | | -- | ------ | -| RL Learning Framework | :hammer: :chart_with_upwards_trend: Released on Oct 20, 2022. [#1322](https://github.com/microsoft/qlib/pull/1322), [#1316](https://github.com/microsoft/qlib/pull/1316),[#1299](https://github.com/microsoft/qlib/pull/1299),[#1263](https://github.com/microsoft/qlib/pull/1263), [#1244](https://github.com/microsoft/qlib/pull/1244), [#1169](https://github.com/microsoft/qlib/pull/1169), [#1125](https://github.com/microsoft/qlib/pull/1125), [#1076](https://github.com/microsoft/qlib/pull/1076)| +| RL Learning Framework | :hammer: :chart_with_upwards_trend: Released on Nov 10, 2022. [#1332](https://github.com/microsoft/qlib/pull/1332), [#1322](https://github.com/microsoft/qlib/pull/1322), [#1316](https://github.com/microsoft/qlib/pull/1316),[#1299](https://github.com/microsoft/qlib/pull/1299),[#1263](https://github.com/microsoft/qlib/pull/1263), [#1244](https://github.com/microsoft/qlib/pull/1244), [#1169](https://github.com/microsoft/qlib/pull/1169), [#1125](https://github.com/microsoft/qlib/pull/1125), [#1076](https://github.com/microsoft/qlib/pull/1076)| | 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 | From 3b471a0fe3ad1db2eef0d298dc7329feeb8d7d2c Mon Sep 17 00:00:00 2001 From: you-n-g Date: Fri, 11 Nov 2022 10:25:04 +0800 Subject: [PATCH 11/15] Fix CI (#1347) --- qlib/rl/order_execution/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qlib/rl/order_execution/__init__.py b/qlib/rl/order_execution/__init__.py index 318c774230d..b985c13317b 100644 --- a/qlib/rl/order_execution/__init__.py +++ b/qlib/rl/order_execution/__init__.py @@ -16,8 +16,8 @@ from .policy import AllOne, PPO from .reward import PAPenaltyReward from .simulator_simple import SingleAssetOrderExecutionSimple -from .state import SAOEStateAdapter, SAOEMetrics, SAOEState -from .strategy import SAOEStrategy, ProxySAOEStrategy, SAOEIntStrategy +from .state import SAOEMetrics, SAOEState +from .strategy import SAOEStateAdapter, SAOEStrategy, ProxySAOEStrategy, SAOEIntStrategy __all__ = [ "FullHistoryStateInterpreter", From a82cc0b12963e989fd33527bce666aabfa21ce75 Mon Sep 17 00:00:00 2001 From: Xu Yang Date: Fri, 11 Nov 2022 19:35:10 +0800 Subject: [PATCH 12/15] update TSDataSampler refineing the memory layout of data array to speed up NN training (#1342) * update TSDataSampler * reformat code with black * use pre-commit to reformat the code * Add documents * More docstring * More Safety Co-authored-by: Young --- qlib/data/dataset/__init__.py | 151 ++++++++++++++++++++++++++++------ 1 file changed, 127 insertions(+), 24 deletions(-) diff --git a/qlib/data/dataset/__init__.py b/qlib/data/dataset/__init__.py index 5e98bfc97af..dcc9957ed63 100644 --- a/qlib/data/dataset/__init__.py +++ b/qlib/data/dataset/__init__.py @@ -82,7 +82,11 @@ class DatasetH(Dataset): """ def __init__( - self, handler: Union[Dict, DataHandler], segments: Dict[Text, Tuple], fetch_kwargs: Dict = {}, **kwargs + self, + handler: Union[Dict, DataHandler], + segments: Dict[Text, Tuple], + fetch_kwargs: Dict = {}, + **kwargs, ): """ Setup the underlying data. @@ -284,10 +288,69 @@ class TSDataSampler: - For performance issues, this Sampler will convert dataframe into arrays for better performance. This could result in a different data type + + Indices design: + TSDataSampler has a index mechanism to help users query time-series data efficiently. + + The definition of related variables: + data_arr: np.ndarray + The original data. it will contains all the original data. + The querying are often for time-series of a specific stock. + By leveraging this data charactoristics to speed up querying, the multi-index of data_arr is rearranged in (instrument, datetime) order + + data_index: pd.MultiIndex with index order + it has the same shape with `idx_map`. Each elements of them are expected to be aligned. + + idx_map: np.ndarray + It is the indexable data. It originates from data_arr, and then filtered by 1) `start` and `end` 2) `flt_data` + The extra data in data_arr is useful in following cases + 1) creating meaningful time series data before `start` instead of padding them with zeros + 2) some data are excluded by `flt_data` (e.g. no sample pair for that index). but they are still used in time-series in X + + Finnally, it will look like. + + array([[ 0, 0], + [ 1, 0], + [ 2, 0], + ..., + [241, 348], + [242, 348], + [243, 348]], dtype=int32) + + It list all indexable data(some data only used in historical time series data may not be indexabla), the values are the corresponding row and col in idx_df + idx_df: pd.DataFrame + It aims to map the key to the original position in data_arr + + For example, it may look like (NOTE: the index for a instrument time-series is continoues in memory) + + instrument SH600000 SH600008 SH600009 SH600010 SH600011 SH600015 ... + datetime + 2017-01-03 0 242 473 717 NaN 974 ... + 2017-01-04 1 243 474 718 NaN 975 ... + 2017-01-05 2 244 475 719 NaN 976 ... + 2017-01-06 3 245 476 720 NaN 977 ... + + With these two indices(idx_map, idx_df) and original data(data_arr), we can make the following queries fast (implemented in __getitem__) + (1) Get the i-th indexable sample(time-series): (indexable sample index) -> [idx_map] -> (row col) -> [idx_df] -> (index in data_arr) + (2) Get the specific sample by : (, i.e. ) -> [idx_df] -> (index in data_arr) + (3) Get the index of a time-series data: (get the , refer to (1), (2)) -> [idx_df] -> (all indices in data_arr for time-series) """ + # Please refer to the docstring of TSDataSampler for the definition of following attributes + data_arr: np.ndarray + data_index: pd.MultiIndex + idx_map: np.ndarray + idx_df: pd.DataFrame + def __init__( - self, data: pd.DataFrame, start, end, step_len: int, fillna_type: str = "none", dtype=None, flt_data=None + self, + data: pd.DataFrame, + start, + end, + step_len: int, + fillna_type: str = "none", + dtype=None, + flt_data=None, ): """ Build a dataset which looks like torch.data.utils.Dataset. @@ -295,7 +358,7 @@ def __init__( Parameters ---------- data : pd.DataFrame - The raw tabular data + The raw tabular data whose index order is <"datetime", "instrument"> start : The indexable start time end : @@ -311,7 +374,7 @@ def __init__( ffill+bfill: ffill with previous samples first and fill with later samples second flt_data : pd.Series - a column of data(True or False) to filter data. + a column of data(True or False) to filter data. Its index order is <"datetime", "instrument"> None: kepp all data @@ -321,7 +384,10 @@ def __init__( self.step_len = step_len self.fillna_type = fillna_type assert get_level_index(data, "datetime") == 0 - self.data = lazy_sort_index(data) + self.data = data.swaplevel().sort_index().copy() + data.drop( + data.columns, axis=1, inplace=True + ) # data is useless since it's passed to a transposed one, hard code to free the memory of this dataframe to avoid three big dataframe in the memory(including: data, self.data, self.data_arr) kwargs = {"object": self.data} if dtype is not None: @@ -332,7 +398,9 @@ def __init__( # - append last line with full NaN for better performance in `__getitem__` # - Keep the same dtype will result in a better performance self.data_arr = np.append( - self.data_arr, np.full((1, self.data_arr.shape[1]), np.nan, dtype=self.data_arr.dtype), axis=0 + self.data_arr, + np.full((1, self.data_arr.shape[1]), np.nan, dtype=self.data_arr.dtype), + axis=0, ) self.nan_idx = -1 # The last line is all NaN @@ -347,19 +415,36 @@ def __init__( flt_data = flt_data.iloc[:, 0] # NOTE: bool(np.nan) is True !!!!!!!! # make sure reindex comes first. Otherwise extra NaN may appear. + flt_data = flt_data.swaplevel() flt_data = flt_data.reindex(self.data_index).fillna(False).astype(np.bool) self.flt_data = flt_data.values self.idx_map = self.flt_idx_map(self.flt_data, self.idx_map) self.data_index = self.data_index[np.where(self.flt_data)[0]] self.idx_map = self.idx_map2arr(self.idx_map) - - self.start_idx, self.end_idx = self.data_index.slice_locs( - start=time_to_slc_point(start), end=time_to_slc_point(end) + self.idx_map, self.data_index = self.slice_idx_map_and_data_index( + self.idx_map, self.idx_df, self.data_index, start, end ) - self.idx_arr = np.array(self.idx_df.values, dtype=np.float64) # for better performance + self.idx_arr = np.array(self.idx_df.values, dtype=np.float64) # for better performance del self.data # save memory + @staticmethod + def slice_idx_map_and_data_index( + idx_map, + idx_df, + data_index, + start, + end, + ): + assert ( + len(idx_map) == data_index.shape[0] + ) # make sure idx_map and data_index is same so index of idx_map can be used on data_index + + start_row_idx, end_row_idx = idx_df.index.slice_locs(start=time_to_slc_point(start), end=time_to_slc_point(end)) + + time_flter_idx = (idx_map[:, 0] < end_row_idx) & (idx_map[:, 0] >= start_row_idx) + return idx_map[time_flter_idx], data_index[time_flter_idx] + @staticmethod def idx_map2arr(idx_map): # pytorch data sampler will have better memory control without large dict or list @@ -394,7 +479,7 @@ def get_index(self): Get the pandas index of the data, it will be useful in following scenarios - Special sampler will be used (e.g. user want to sample day by day) """ - return self.data_index[self.start_idx : self.end_idx] + return self.data_index.swaplevel() # to align the order of multiple index of original data received by __init__ def config(self, **kwargs): # Config the attributes @@ -409,25 +494,33 @@ def build_index(data: pd.DataFrame) -> Tuple[pd.DataFrame, dict]: Parameters ---------- data : pd.DataFrame - The dataframe with + A DataFrame with index in order + + RSQR5 RESI5 WVMA5 LABEL0 + instrument datetime + SH600000 2017-01-03 0.016389 0.461632 -1.154788 -0.048056 + 2017-01-04 0.884545 -0.110597 -1.059332 -0.030139 + 2017-01-05 0.507540 -0.535493 -1.099665 -0.644983 + 2017-01-06 -1.267771 -0.669685 -1.636733 0.295366 + 2017-01-09 0.339346 0.074317 -0.984989 0.765540 Returns ------- Tuple[pd.DataFrame, dict]: 1) the first element: reshape the original index into a 2D dataframe - instrument SH600000 SH600004 SH600006 SH600007 SH600008 SH600009 ... + instrument SH600000 SH600008 SH600009 SH600010 SH600011 SH600015 ... datetime - 2021-01-11 0 1 2 3 4 5 ... - 2021-01-12 4146 4147 4148 4149 4150 4151 ... - 2021-01-13 8293 8294 8295 8296 8297 8298 ... - 2021-01-14 12441 12442 12443 12444 12445 12446 ... + 2017-01-03 0 242 473 717 NaN 974 ... + 2017-01-04 1 243 474 718 NaN 975 ... + 2017-01-05 2 244 475 719 NaN 976 ... + 2017-01-06 3 245 476 720 NaN 977 ... 2) the second element: {: } """ # object incase of pandas converting int to float idx_df = pd.Series(range(data.shape[0]), index=data.index, dtype=object) idx_df = lazy_sort_index(idx_df.unstack()) # NOTE: the correctness of `__getitem__` depends on columns sorted here - idx_df = lazy_sort_index(idx_df, axis=1) + idx_df = lazy_sort_index(idx_df, axis=1).T idx_map = {} for i, (_, row) in enumerate(idx_df.iterrows()): @@ -485,11 +578,11 @@ def _get_row_col(self, idx) -> Tuple[int]: """ # The the right row number `i` and col number `j` in idx_df if isinstance(idx, (int, np.integer)): - real_idx = self.start_idx + idx - if self.start_idx <= real_idx < self.end_idx: + real_idx = idx + if 0 <= real_idx < len(self.idx_map): i, j = self.idx_map[real_idx] # TODO: The performance of this line is not good else: - raise KeyError(f"{real_idx} is out of [{self.start_idx}, {self.end_idx})") + raise KeyError(f"{real_idx} is out of [0, {len(self.idx_map)})") elif isinstance(idx, tuple): # ["datetime", "instruments"] date, inst = idx @@ -532,7 +625,10 @@ def __getitem__(self, idx: Union[int, Tuple[object, str], List[int]]): # precision problems. It will not cause any problems in my tests at least indices = np.nan_to_num(indices.astype(np.float64), nan=self.nan_idx).astype(int) - data = self.data_arr[indices] + if (np.diff(indices) == 1).all(): # slicing instead of indexing for speeding up. + data = self.data_arr[indices[0] : indices[-1] + 1] + else: + data = self.data_arr[indices] if isinstance(idx, mtit): # if we get multiple indexes, addition dimension should be added. # @@ -540,7 +636,7 @@ def __getitem__(self, idx: Union[int, Tuple[object, str], List[int]]): return data def __len__(self): - return self.end_idx - self.start_idx + return len(self.idx_map) class TSDatasetH(DatasetH): @@ -611,7 +707,14 @@ def _prepare_seg(self, slc: slice, **kwargs) -> TSDataSampler: else: flt_data = None - 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 From ff2154c618ae972f704164beb094dcc330c375c4 Mon Sep 17 00:00:00 2001 From: He Yi Date: Fri, 11 Nov 2022 19:53:33 +0800 Subject: [PATCH 13/15] fix bug in fix clip_outlier in class RobustZScoreNorm(Processor) (#1294) --- qlib/data/dataset/processor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qlib/data/dataset/processor.py b/qlib/data/dataset/processor.py index 26ff7e09dad..b7abb200029 100644 --- a/qlib/data/dataset/processor.py +++ b/qlib/data/dataset/processor.py @@ -289,9 +289,9 @@ def __call__(self, df): X = df[self.cols] X -= self.mean_train X /= self.std_train - df[self.cols] = X if self.clip_outlier: - df.clip(-3, 3, inplace=True) + X = np.clip(X, -3, 3) + df[self.cols] = X return df From 4001a5d1571cb622315dd6c7ff6034ad42d6cd17 Mon Sep 17 00:00:00 2001 From: qianyun210603 Date: Sun, 13 Nov 2022 19:03:23 +0800 Subject: [PATCH 14/15] Bug fix for Rank and WMA operators (#1228) * bug fix: 1) 100 should be used to scale down percentileofscore return to 0-1, not length of array; 2) for (linear) weighted MA(n), weight should be n, n-1, ..., 1 instead of n-1, ..., 0 * use native pandas fucntion for rank * remove useless import * require pandas 1.4+ * rank for py37+pandas 1.3.5 compatibility * lint improvement * lint black fix * use hasattr instead of version to check whether rolling.rank is implemented --- qlib/data/ops.py | 24 +++++++++--------------- 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/qlib/data/ops.py b/qlib/data/ops.py index 1cbb1d2e628..fe2ebc9f6d9 100644 --- a/qlib/data/ops.py +++ b/qlib/data/ops.py @@ -34,8 +34,6 @@ #################### Element-Wise Operator #################### - - class ElemOperator(ExpressionOps): """Element-wise Operator @@ -216,9 +214,7 @@ class Not(NpElemOperator): Parameters ---------- - feature_left : Expression - feature instance - feature_right : Expression + feature : Expression feature instance Returns @@ -241,8 +237,6 @@ class PairOperator(ExpressionOps): feature instance or numeric value feature_right : Expression feature instance or numeric value - func : str - operator function Returns ---------- @@ -1155,9 +1149,13 @@ class Rank(Rolling): def __init__(self, feature, N): super(Rank, self).__init__(feature, N, "rank") + # for compatiblity of python 3.7, which doesn't support pandas 1.4.0+ which implements Rolling.rank def _load_internal(self, instrument, start_index, end_index, *args): series = self.feature.load(instrument, start_index, end_index, *args) - # TODO: implement in Cython + + rolling_or_expending = series.expanding(min_periods=1) if self.N == 0 else series.rolling(self.N, min_periods=1) + if hasattr(rolling_or_expending, "rank"): + return rolling_or_expending.rank(pct=True) def rank(x): if np.isnan(x[-1]): @@ -1165,13 +1163,9 @@ def rank(x): x1 = x[~np.isnan(x)] if x1.shape[0] == 0: return np.nan - return percentileofscore(x1, x1[-1]) / len(x1) + return percentileofscore(x1, x1[-1]) / 100 - if self.N == 0: - series = series.expanding(min_periods=1).apply(rank, raw=True) - else: - series = series.rolling(self.N, min_periods=1).apply(rank, raw=True) - return series + return rolling_or_expending.apply(rank, raw=True) class Count(Rolling): @@ -1341,7 +1335,7 @@ def _load_internal(self, instrument, start_index, end_index, *args): # TODO: implement in Cython def weighted_mean(x): - w = np.arange(len(x)) + w = np.arange(len(x)) + 1 w = w / w.sum() return np.nanmean(w * x) From 82afd6a67aba1769ce3b03d1e600de378a8f7ec3 Mon Sep 17 00:00:00 2001 From: Maxim Smolskiy Date: Sun, 13 Nov 2022 17:07:08 +0300 Subject: [PATCH 15/15] Fix the Warnings in rst files when building Qlib's documentation (#1349) * Fix docs/advanced/alpha.rst * Fix docs/reference/api.rst * Fix docs/component/strategy.rst * Fix docs/start/integration.rst * Fix docs/component/report.rst * Fix docs/component/data.rst * Fix docs/component/rl/framework.rst * Fix docs/introduction/quick.rst * Fix docs/advanced/task_management.rst * Fix CHANGES.rst * Fix docs/developer/code_standard_and_dev_guide.rst * Fix docs/hidden/client.rst * Fix docs/component/online.rst * Fix docs/start/getdata.rst * Add docs/hidden to exclude patterns * Add docs/developer/code_standard_and_dev_guide.rst to index.rst * Change docs/developer/code_standard_and_dev_guide.rst place in index.rst --- CHANGES.rst | 12 +- docs/advanced/alpha.rst | 2 +- docs/advanced/task_management.rst | 6 +- docs/component/data.rst | 10 +- docs/component/online.rst | 2 +- docs/component/report.rst | 1 + docs/component/rl/framework.rst | 4 +- docs/component/strategy.rst | 1 + docs/conf.py | 2 +- .../developer/code_standard_and_dev_guide.rst | 9 +- docs/hidden/client.rst | 3 +- docs/index.rst | 6 + docs/introduction/quick.rst | 1 + docs/reference/api.rst | 1 + docs/start/getdata.rst | 34 ++-- docs/start/integration.rst | 152 +++++++++--------- 16 files changed, 131 insertions(+), 115 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 3e94dc44e38..76aa4829304 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -85,7 +85,7 @@ Version 0.4.0 ------------- - Add `data` package that holds all data-related codes - Reform the data provider structure -- Create a server for data centralized management `qlib-server`_ +- Create a server for data centralized management `qlib-server `_ - Add a `ClientProvider` to work with server - Add a pluggable cache mechanism - Add a recursive backtracking algorithm to inspect the furthest reference date for an expression @@ -166,12 +166,12 @@ Version 0.8.0 - Nested decision execution framework is supported - There are lots of changes for daily trading, it is hard to list all of them. But a few important changes could be noticed - The trading limitation is more accurate; - - In `previous version `_, longing and shorting actions share the same action. - - In `current version `_, the trading limitation is different between logging and shorting action. + - In `previous version `__, longing and shorting actions share the same action. + - In `current version `__, the trading limitation is different between logging and shorting action. - The constant is different when calculating annualized metrics. - - `Current version `_ uses more accurate constant than `previous version `_ - - `A new version `_ of data is released. Due to the unstability of Yahoo data source, the data may be different after downloading data again. - - Users could check out the backtesting results between `Current version `_ and `previous version `_ + - `Current version `_ uses more accurate constant than `previous version `__ + - `A new version `__ of data is released. Due to the unstability of Yahoo data source, the data may be different after downloading data again. + - Users could check out the backtesting results between `Current version `__ and `previous version `__ Other Versions diff --git a/docs/advanced/alpha.rst b/docs/advanced/alpha.rst index 797eb19da54..88d65074c65 100644 --- a/docs/advanced/alpha.rst +++ b/docs/advanced/alpha.rst @@ -38,7 +38,7 @@ Example DIF = \frac{EMA(CLOSE, 12) - EMA(CLOSE, 26)}{CLOSE} - `DEA`means a 9-period EMA of the DIF. + `DEA` means a 9-period EMA of the DIF. .. math:: diff --git a/docs/advanced/task_management.rst b/docs/advanced/task_management.rst index d45c7b97d15..70b6bcfc860 100644 --- a/docs/advanced/task_management.rst +++ b/docs/advanced/task_management.rst @@ -18,7 +18,7 @@ With this module, users can run their ``task`` automatically at different period This whole process can be used in `Online Serving <../component/online.html>`_. -An example of the entire process is shown `here `_. +An example of the entire process is shown `here `__. Task Generating =============== @@ -33,7 +33,7 @@ Here is the base class of ``TaskGen``: :members: ``Qlib`` provides a class `RollingGen `_ to generate a list of ``task`` of the dataset in different date segments. -This class allows users to verify the effect of data from different periods on the model in one experiment. More information is `here <../reference/api.html#TaskGen>`_. +This class allows users to verify the effect of data from different periods on the model in one experiment. More information is `here <../reference/api.html#TaskGen>`__. Task Storing ============ @@ -54,7 +54,7 @@ Users need to provide the MongoDB URL and database name for using ``TaskManager` .. autoclass:: qlib.workflow.task.manage.TaskManager :members: -More information of ``Task Manager`` can be found in `here <../reference/api.html#TaskManager>`_. +More information of ``Task Manager`` can be found in `here <../reference/api.html#TaskManager>`__. Task Training ============= diff --git a/docs/component/data.rst b/docs/component/data.rst index b8279432e70..d3b8cafed21 100644 --- a/docs/component/data.rst +++ b/docs/component/data.rst @@ -24,8 +24,8 @@ The introduction of ``Data Layer`` includes the following parts. Here is a typical example of Qlib data workflow - Users download data and converting data into Qlib format(with filename suffix `.bin`). In this step, typically only some basic data are stored on disk(such as OHLCV). -- Creating some basic features based on Qlib's expression Engine(e.g. "Ref($close, 60) / $close", the return of last 60 trading days). Supported operators in the expression engine can be found `here `_. This step is typically implemented in Qlib's `Data Loader `_ which is a component of `Data Handler `_ . -- If users require more complicated data processing (e.g. data normalization), `Data Handler `_ support user-customized processors to process data(some predefined processors can be found `here `_). The processors are different from operators in expression engine. It is designed for some complicated data processing methods which is hard to supported in operators in expression engine. +- Creating some basic features based on Qlib's expression Engine(e.g. "Ref($close, 60) / $close", the return of last 60 trading days). Supported operators in the expression engine can be found `here `__. This step is typically implemented in Qlib's `Data Loader `_ which is a component of `Data Handler `_ . +- If users require more complicated data processing (e.g. data normalization), `Data Handler `_ support user-customized processors to process data(some predefined processors can be found `here `__). The processors are different from operators in expression engine. It is designed for some complicated data processing methods which is hard to supported in operators in expression engine. - At last, `Dataset `_ is responsible to prepare model-specific dataset from the processed data of Data Handler Data Preparation @@ -37,7 +37,7 @@ Qlib Format Data We've specially designed a data structure to manage financial data, please refer to the `File storage design section in Qlib paper `_ for detailed information. Such data will be stored with filename suffix `.bin` (We'll call them `.bin` file, `.bin` format, or qlib format). `.bin` file is designed for scientific computing on finance data. -``Qlib`` provides two different off-the-shelf datasets, which can be accessed through this `link `_: +``Qlib`` provides two different off-the-shelf datasets, which can be accessed through this `link `__: ======================== ================= ================ Dataset US Market China Market @@ -47,7 +47,7 @@ Alpha360 √ √ Alpha158 √ √ ======================== ================= ================ -Also, ``Qlib`` provides a high-frequency dataset. Users can run a high-frequency dataset example through this `link `_. +Also, ``Qlib`` provides a high-frequency dataset. Users can run a high-frequency dataset example through this `link `__. Qlib Format Dataset ------------------- @@ -512,7 +512,7 @@ Data and Cache File Structure We've specially designed a file structure to manage data and cache, please refer to the `File storage design section in Qlib paper `_ for detailed information. The file structure of data and cache is listed as follows. -.. code-block:: json +.. code-block:: - data/ [raw data] updated by data providers diff --git a/docs/component/online.rst b/docs/component/online.rst index c72c77a873d..351098db4d4 100644 --- a/docs/component/online.rst +++ b/docs/component/online.rst @@ -1,4 +1,4 @@ -.. _online: +.. _online_serving: ============== Online Serving diff --git a/docs/component/report.rst b/docs/component/report.rst index 6ed87eef057..30fca078836 100644 --- a/docs/component/report.rst +++ b/docs/component/report.rst @@ -174,6 +174,7 @@ Graphical Result The `Information Ratio` without cost. - `excess_return_with_cost` The `Information Ratio` with cost. + To know more about `Information Ratio`, please refer to `Information Ratio – IR `_. - `max_drawdown` - `excess_return_without_cost` diff --git a/docs/component/rl/framework.rst b/docs/component/rl/framework.rst index 7edb08efd90..a31cb1b4701 100644 --- a/docs/component/rl/framework.rst +++ b/docs/component/rl/framework.rst @@ -28,7 +28,7 @@ In QlibRL, EnvWrapper is a subclass of gym.Env, so it implements all necessary i EnvWrapper will organically organize these components. Such decomposition allows for better flexibility in development. For example, if the developers want to train multiple types of policies in the same environment, they only need to design one simulator and design different state interpreters/action interpreters/reward functions for different types of policies. -QlibRL has well-defined base classes for all these 4 components. All the developers need to do is define their own components by inheriting the base classes and then implementing all interfaces required by the base classes. The API for the above base components can be found `here <../../reference/api.html#module-qlib.rl>`_. +QlibRL has well-defined base classes for all these 4 components. All the developers need to do is define their own components by inheriting the base classes and then implementing all interfaces required by the base classes. The API for the above base components can be found `here <../../reference/api.html#module-qlib.rl>`__. Policy ------------ @@ -42,4 +42,4 @@ As you may have noticed, a training vessel itself holds all the required compone With a training vessel, the trainer could finally launch the training pipeline by simple, Scikit-learn-like interfaces (i.e., ``trainer.fit()``). -The API for Trainer and TrainingVessel and can be found `here <../../reference/api.html#module-qlib.rl.trainer>`_. \ No newline at end of file +The API for Trainer and TrainingVessel and can be found `here <../../reference/api.html#module-qlib.rl.trainer>`__. \ No newline at end of file diff --git a/docs/component/strategy.rst b/docs/component/strategy.rst index 919551fb314..910ebf7083b 100644 --- a/docs/component/strategy.rst +++ b/docs/component/strategy.rst @@ -80,6 +80,7 @@ TopkDropoutStrategy 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 :alt: Topk-Drop diff --git a/docs/conf.py b/docs/conf.py index a7147a964a1..442c89da2da 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -77,7 +77,7 @@ # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This patterns also effect to html_static_path and html_extra_path -exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] +exclude_patterns = ["_build", "Thumbs.db", ".DS_Store", "hidden"] # The name of the Pygments (syntax highlighting) style to use. pygments_style = "sphinx" diff --git a/docs/developer/code_standard_and_dev_guide.rst b/docs/developer/code_standard_and_dev_guide.rst index ae5927c837e..79a7778ad1a 100644 --- a/docs/developer/code_standard_and_dev_guide.rst +++ b/docs/developer/code_standard_and_dev_guide.rst @@ -15,7 +15,8 @@ Continuous Integration (CI) tools help you stick to the quality standards by run When you submit a PR request, you can check whether your code passes the CI tests in the "check" section at the bottom of the web page. 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:: bash @@ -32,7 +33,8 @@ When you submit a PR request, you can check whether your code passes the CI test 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. + + You can fix the bug by inputing the following code in the command line. .. code-block:: bash @@ -40,7 +42,8 @@ When you submit a PR request, you can check whether your code passes the CI test 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. + + 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 diff --git a/docs/hidden/client.rst b/docs/hidden/client.rst index 7ca0d68013a..de6e2e681e8 100644 --- a/docs/hidden/client.rst +++ b/docs/hidden/client.rst @@ -81,6 +81,7 @@ If running on Windows, open **NFS** features and write correct **mount_path**, i * Open ``Programs and Features``. * Click ``Turn Windows features on or off``. * Scroll down and check the option ``Services for NFS``, then click OK + Reference address: https://graspingtech.com/mount-nfs-share-windows-10/ 2.config correct mount_path * In windows, mount path must be not exist path and root path, @@ -161,7 +162,7 @@ Limitations API *** -The client is based on `python-socketio`_ which is a framework that supports WebSocket client for Python language. The client can only propose requests and receive results, which do not include any calculating procedure. +The client is based on `python-socketio `_ which is a framework that supports WebSocket client for Python language. The client can only propose requests and receive results, which do not include any calculating procedure. Class ----- diff --git a/docs/index.rst b/docs/index.rst index 0d8cad81ada..3adf9049a76 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -56,6 +56,12 @@ Document Structure Task Management Point-In-Time database +.. toctree:: + :maxdepth: 3 + :caption: FOR DEVELOPERS: + + Code Standard & Development Guidance + .. toctree:: :maxdepth: 3 :caption: REFERENCE: diff --git a/docs/introduction/quick.rst b/docs/introduction/quick.rst index 364e58caf12..78d9c2083f6 100644 --- a/docs/introduction/quick.rst +++ b/docs/introduction/quick.rst @@ -21,6 +21,7 @@ Users can easily intsall ``Qlib`` according to the following steps: - Before installing ``Qlib`` from source, users need to install some dependencies: .. code-block:: + pip install numpy pip install --upgrade cython diff --git a/docs/reference/api.rst b/docs/reference/api.rst index 98f50fc281e..4e6a7a85432 100644 --- a/docs/reference/api.rst +++ b/docs/reference/api.rst @@ -1,4 +1,5 @@ .. _api: + ============= API Reference ============= diff --git a/docs/start/getdata.rst b/docs/start/getdata.rst index 8849eb87cc5..cea9c2b0dc4 100644 --- a/docs/start/getdata.rst +++ b/docs/start/getdata.rst @@ -83,15 +83,14 @@ Load features of certain instruments in a given time range: >> from qlib.data import D >> instruments = ['SH600000'] >> fields = ['$close', '$volume', 'Ref($close, 1)', 'Mean($close, 3)', '$high-$low'] - >> D.features(instruments, fields, start_time='2010-01-01', end_time='2017-12-31', freq='day').head() - - $close $volume Ref($close, 1) Mean($close, 3) $high-$low - instrument datetime - SH600000 2010-01-04 86.778313 16162960.0 88.825928 88.061483 2.907631 - 2010-01-05 87.433578 28117442.0 86.778313 87.679273 3.235252 - 2010-01-06 85.713585 23632884.0 87.433578 86.641825 1.720009 - 2010-01-07 83.788803 20813402.0 85.713585 85.645322 3.030487 - 2010-01-08 84.730675 16044853.0 83.788803 84.744354 2.047623 + >> D.features(instruments, fields, start_time='2010-01-01', end_time='2017-12-31', freq='day').head().to_string() + ' $close $volume Ref($close, 1) Mean($close, 3) $high-$low + ... instrument datetime + ... SH600000 2010-01-04 86.778313 16162960.0 88.825928 88.061483 2.907631 + ... 2010-01-05 87.433578 28117442.0 86.778313 87.679273 3.235252 + ... 2010-01-06 85.713585 23632884.0 87.433578 86.641825 1.720009 + ... 2010-01-07 83.788803 20813402.0 85.713585 85.645322 3.030487 + ... 2010-01-08 84.730675 16044853.0 83.788803 84.744354 2.047623' Load features of certain stock pool in a given time range: @@ -105,15 +104,14 @@ Load features of certain stock pool in a given time range: >> expressionDFilter = ExpressionDFilter(rule_expression='$close>Ref($close,1)') >> instruments = D.instruments(market='csi300', filter_pipe=[nameDFilter, expressionDFilter]) >> fields = ['$close', '$volume', 'Ref($close, 1)', 'Mean($close, 3)', '$high-$low'] - >> D.features(instruments, fields, start_time='2010-01-01', end_time='2017-12-31', freq='day').head() - - $close $volume Ref($close, 1) Mean($close, 3) $high-$low - instrument datetime - SH600655 2010-01-04 2699.567383 158193.328125 2619.070312 2626.097738 124.580566 - 2010-01-08 2612.359619 77501.406250 2584.567627 2623.220133 83.373047 - 2010-01-11 2712.982422 160852.390625 2612.359619 2636.636556 146.621582 - 2010-01-12 2788.688232 164587.937500 2712.982422 2704.676758 128.413818 - 2010-01-13 2790.604004 145460.453125 2788.688232 2764.091553 128.413818 + >> D.features(instruments, fields, start_time='2010-01-01', end_time='2017-12-31', freq='day').head().to_string() + ' $close $volume Ref($close, 1) Mean($close, 3) $high-$low + ... instrument datetime + ... SH600655 2010-01-04 2699.567383 158193.328125 2619.070312 2626.097738 124.580566 + ... 2010-01-08 2612.359619 77501.406250 2584.567627 2623.220133 83.373047 + ... 2010-01-11 2712.982422 160852.390625 2612.359619 2636.636556 146.621582 + ... 2010-01-12 2788.688232 164587.937500 2712.982422 2704.676758 128.413818 + ... 2010-01-13 2790.604004 145460.453125 2788.688232 2764.091553 128.413818' For more details about features, please refer `Feature API <../component/data.html>`_. diff --git a/docs/start/integration.rst b/docs/start/integration.rst index 801bb819d17..a9eecc4ead8 100644 --- a/docs/start/integration.rst +++ b/docs/start/integration.rst @@ -21,84 +21,88 @@ The Custom models need to inherit `qlib.model.base.Model <../reference/api.html# - ``Qlib`` passes the initialized parameters to the \_\_init\_\_ method. - The hyperparameters of model in the configuration must be consistent with those defined in the `__init__` method. - Code Example: In the following example, the hyperparameters of model in the configuration file should contain parameters such as `loss:mse`. - .. code-block:: Python - def __init__(self, loss='mse', **kwargs): - if loss not in {'mse', 'binary'}: - raise NotImplementedError - self._scorer = mean_squared_error if loss == 'mse' else roc_auc_score - self._params.update(objective=loss, **kwargs) - self._model = None + .. code-block:: Python + + def __init__(self, loss='mse', **kwargs): + if loss not in {'mse', 'binary'}: + raise NotImplementedError + self._scorer = mean_squared_error if loss == 'mse' else roc_auc_score + self._params.update(objective=loss, **kwargs) + self._model = None - Override the `fit` method - ``Qlib`` calls the fit method to train the model. - The parameters must include training feature `dataset`, which is designed in the interface. - The parameters could include some `optional` parameters with default values, such as `num_boost_round = 1000` for `GBDT`. - Code Example: In the following example, `num_boost_round = 1000` is an optional parameter. - .. code-block:: Python - - def fit(self, dataset: DatasetH, num_boost_round = 1000, **kwargs): - - # prepare dataset for lgb training and evaluation - df_train, df_valid = dataset.prepare( - ["train", "valid"], col_set=["feature", "label"], data_key=DataHandlerLP.DK_L - ) - x_train, y_train = df_train["feature"], df_train["label"] - x_valid, y_valid = df_valid["feature"], df_valid["label"] - - # Lightgbm need 1D array as its label - if y_train.values.ndim == 2 and y_train.values.shape[1] == 1: - y_train, y_valid = np.squeeze(y_train.values), np.squeeze(y_valid.values) - else: - raise ValueError("LightGBM doesn't support multi-label training") - - dtrain = lgb.Dataset(x_train.values, label=y_train) - dvalid = lgb.Dataset(x_valid.values, label=y_valid) - - # fit the model - self.model = lgb.train( - self.params, - dtrain, - num_boost_round=num_boost_round, - valid_sets=[dtrain, dvalid], - valid_names=["train", "valid"], - early_stopping_rounds=early_stopping_rounds, - verbose_eval=verbose_eval, - evals_result=evals_result, - **kwargs - ) + + .. code-block:: Python + + def fit(self, dataset: DatasetH, num_boost_round = 1000, **kwargs): + + # prepare dataset for lgb training and evaluation + df_train, df_valid = dataset.prepare( + ["train", "valid"], col_set=["feature", "label"], data_key=DataHandlerLP.DK_L + ) + x_train, y_train = df_train["feature"], df_train["label"] + x_valid, y_valid = df_valid["feature"], df_valid["label"] + + # Lightgbm need 1D array as its label + if y_train.values.ndim == 2 and y_train.values.shape[1] == 1: + y_train, y_valid = np.squeeze(y_train.values), np.squeeze(y_valid.values) + else: + raise ValueError("LightGBM doesn't support multi-label training") + + dtrain = lgb.Dataset(x_train.values, label=y_train) + dvalid = lgb.Dataset(x_valid.values, label=y_valid) + + # fit the model + self.model = lgb.train( + self.params, + dtrain, + num_boost_round=num_boost_round, + valid_sets=[dtrain, dvalid], + valid_names=["train", "valid"], + early_stopping_rounds=early_stopping_rounds, + verbose_eval=verbose_eval, + evals_result=evals_result, + **kwargs + ) - Override the `predict` method - The parameters must include the parameter `dataset`, which will be userd to get the test dataset. - Return the `prediction score`. - Please refer to `Model API <../reference/api.html#module-qlib.model.base>`_ for the parameter types of the fit method. - Code Example: In the following example, users need to use `LightGBM` to predict the label(such as `preds`) of test data `x_test` and return it. - .. code-block:: Python - def predict(self, dataset: DatasetH, **kwargs)-> pandas.Series: - if self.model is None: - raise ValueError("model is not fitted yet!") - x_test = dataset.prepare("test", col_set="feature", data_key=DataHandlerLP.DK_I) - return pd.Series(self.model.predict(x_test.values), index=x_test.index) + .. code-block:: Python + + def predict(self, dataset: DatasetH, **kwargs)-> pandas.Series: + if self.model is None: + raise ValueError("model is not fitted yet!") + x_test = dataset.prepare("test", col_set="feature", data_key=DataHandlerLP.DK_I) + return pd.Series(self.model.predict(x_test.values), index=x_test.index) - Override the `finetune` method (Optional) - This method is optional to the users. When users want to use this method on their own models, they should inherit the ``ModelFT`` base class, which includes the interface of `finetune`. - The parameters must include the parameter `dataset`. - Code Example: In the following example, users will use `LightGBM` as the model and finetune it. - .. code-block:: Python - - def finetune(self, dataset: DatasetH, num_boost_round=10, verbose_eval=20): - # Based on existing model and finetune by train more rounds - dtrain, _ = self._prepare_data(dataset) - self.model = lgb.train( - self.params, - dtrain, - num_boost_round=num_boost_round, - init_model=self.model, - valid_sets=[dtrain], - valid_names=["train"], - verbose_eval=verbose_eval, - ) + + .. code-block:: Python + + def finetune(self, dataset: DatasetH, num_boost_round=10, verbose_eval=20): + # Based on existing model and finetune by train more rounds + dtrain, _ = self._prepare_data(dataset) + self.model = lgb.train( + self.params, + dtrain, + num_boost_round=num_boost_round, + init_model=self.model, + valid_sets=[dtrain], + valid_names=["train"], + verbose_eval=verbose_eval, + ) Configuration File ================== @@ -107,21 +111,21 @@ The configuration file is described in detail in the `Workflow <../component/wor - Example: The following example describes the `model` field of configuration file about the custom lightgbm model mentioned above, where `module_path` is the module path, `class` is the class name, and `args` is the hyperparameter passed into the __init__ method. All parameters in the field is passed to `self._params` by `\*\*kwargs` in `__init__` except `loss = mse`. -.. code-block:: YAML - - model: - class: LGBModel - module_path: qlib.contrib.model.gbdt - args: - loss: mse - colsample_bytree: 0.8879 - learning_rate: 0.0421 - subsample: 0.8789 - lambda_l1: 205.6999 - lambda_l2: 580.9768 - max_depth: 8 - num_leaves: 210 - num_threads: 20 + .. code-block:: YAML + + model: + class: LGBModel + module_path: qlib.contrib.model.gbdt + args: + loss: mse + colsample_bytree: 0.8879 + learning_rate: 0.0421 + subsample: 0.8789 + lambda_l1: 205.6999 + lambda_l2: 580.9768 + max_depth: 8 + num_leaves: 210 + num_threads: 20 Users could find configuration file of the baselines of the ``Model`` in ``examples/benchmarks``. All the configurations of different models are listed under the corresponding model folder.