From 3db22452fb2c1d06d1c2e544a94be9a11c26f974 Mon Sep 17 00:00:00 2001 From: Chao Wang Date: Sun, 3 Jul 2022 20:45:26 -0400 Subject: [PATCH] Adding ChangeInstrument op (#1005) * add ChangeInstrument to ops Adding Change instrument OP. This op allows one to use features of a different instrument. * Update __init__.py update parse_field to accommodate ChangeInstrument * Propose test * Add test case and fix bug * Update ops.py * Update ops.py * simplify the operator further * implement abstract method * fix arg bug * clean test Co-authored-by: Young Co-authored-by: you-n-g --- .pre-commit-config.yaml | 2 +- qlib/data/ops.py | 35 +++++++++++++ tests/ops/test_special_ops.py | 92 +++++++++++++++++++++++++++++++++++ 3 files changed, 128 insertions(+), 1 deletion(-) create mode 100644 tests/ops/test_special_ops.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 18995241b..ea57aeb0e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/psf/black - rev: 22.1.0 + rev: 22.6.0 hooks: - id: black args: ["qlib", "-l 120"] diff --git a/qlib/data/ops.py b/qlib/data/ops.py index 682be7834..1cbb1d2e6 100644 --- a/qlib/data/ops.py +++ b/qlib/data/ops.py @@ -32,6 +32,7 @@ except ValueError: np.seterr(invalid="ignore") + #################### Element-Wise Operator #################### @@ -62,6 +63,39 @@ class ElemOperator(ExpressionOps): return self.feature.get_extended_window_size() +class ChangeInstrument(ElemOperator): + """Change Instrument Operator + In some case, one may want to change to another instrument when calculating, for example, to + calculate beta of a stock with respect to a market index. + This would require changing the calculation of features from the stock (original instrument) to + the index (reference instrument) + Parameters + ---------- + instrument: new instrument for which the downstream operations should be performed upon. + i.e., SH000300 (CSI300 index), or ^GPSC (SP500 index). + + feature: the feature to be calculated for the new instrument. + Returns + ---------- + Expression + feature operation output + """ + + def __init__(self, instrument, feature): + self.instrument = instrument + self.feature = feature + + def __str__(self): + return "{}('{}',{})".format(type(self).__name__, self.instrument, self.feature) + + def load(self, instrument, start_index, end_index, *args): + # the first `instrument` is ignored + return super().load(self.instrument, start_index, end_index, *args) + + def _load_internal(self, instrument, start_index, end_index, *args): + return self.feature.load(instrument, start_index, end_index, *args) + + class NpElemOperator(ElemOperator): """Numpy Element-wise Operator @@ -1535,6 +1569,7 @@ class TResample(ElemOperator): TOpsList = [TResample] OpsList = [ + ChangeInstrument, Rolling, Ref, Max, diff --git a/tests/ops/test_special_ops.py b/tests/ops/test_special_ops.py new file mode 100644 index 000000000..6c4a4ec49 --- /dev/null +++ b/tests/ops/test_special_ops.py @@ -0,0 +1,92 @@ +import unittest + +from qlib.data import D +from qlib.data.dataset.loader import QlibDataLoader +from qlib.data.ops import ChangeInstrument, Cov, Feature, Ref, Var +from qlib.tests import TestOperatorData + + +class TestOperatorDataSetting(TestOperatorData): + def test_setting(self): + # All the query below passes + df = D.features(["SH600519"], ["ChangeInstrument('SH000300', $close)"]) + + # get market return for "SH600519" + df = D.features(["SH600519"], ["ChangeInstrument('SH000300', Feature('close')/Ref(Feature('close'),1) -1)"]) + df = D.features(["SH600519"], ["ChangeInstrument('SH000300', $close/Ref($close,1) -1)"]) + # excess return + df = D.features( + ["SH600519"], ["($close/Ref($close,1) -1) - ChangeInstrument('SH000300', $close/Ref($close,1) -1)"] + ) + print(df) + + def test_case2(self): + def test_case(instruments, queries, note=None): + if note: + print(note) + print(f"checking {instruments} with queries {queries}") + df = D.features(instruments, queries) + print(df) + return df + + test_case(["SH600519"], ["ChangeInstrument('SH000300', $close)"], "get market index close") + test_case( + ["SH600519"], + ["ChangeInstrument('SH000300', Feature('close')/Ref(Feature('close'),1) -1)"], + "get market index return with Feature", + ) + test_case( + ["SH600519"], + ["ChangeInstrument('SH000300', $close/Ref($close,1) -1)"], + "get market index return with expression", + ) + test_case( + ["SH600519"], + ["($close/Ref($close,1) -1) - ChangeInstrument('SH000300', $close/Ref($close,1) -1)"], + "get excess return with expression with beta=1", + ) + + ret = "Feature('close') / Ref(Feature('close'), 1) - 1" + benchmark = "SH000300" + n_period = 252 + marketRet = f"ChangeInstrument('{benchmark}', Feature('close') / Ref(Feature('close'), 1) - 1)" + marketVar = f"ChangeInstrument('{benchmark}', Var({marketRet}, {n_period}))" + beta = f"Cov({ret}, {marketRet}, {n_period}) / {marketVar}" + excess_return = f"{ret} - {beta}*({marketRet})" + fields = [ + "Feature('close')", + f"ChangeInstrument('{benchmark}', Feature('close'))", + ret, + marketRet, + beta, + excess_return, + ] + test_case(["SH600519"], fields[5:], "get market beta and excess_return with estimated beta") + + instrument = "sh600519" + ret = Feature("close") / Ref(Feature("close"), 1) - 1 + benchmark = "sh000300" + n_period = 252 + marketRet = ChangeInstrument(benchmark, Feature("close") / Ref(Feature("close"), 1) - 1) + marketVar = ChangeInstrument(benchmark, Var(marketRet, n_period)) + beta = Cov(ret, marketRet, n_period) / marketVar + fields = [ + Feature("close"), + ChangeInstrument(benchmark, Feature("close")), + ret, + marketRet, + beta, + ret - beta * marketRet, + ] + names = ["close", "marketClose", "ret", "marketRet", f"beta_{n_period}", "excess_return"] + data_loader_config = {"feature": (fields, names)} + data_loader = QlibDataLoader(config=data_loader_config) + df = data_loader.load(instruments=[instrument]) # , start_time=start_time) + print(df) + + # test_case(["sh600519"],fields, + # "get market beta and excess_return with estimated beta") + + +if __name__ == "__main__": + unittest.main()