Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding ChangeInstrument op #1005

Merged
merged 13 commits into from
Jul 4, 2022
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -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"]
Expand Down
35 changes: 35 additions & 0 deletions qlib/data/ops.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@

np.seterr(invalid="ignore")


#################### Element-Wise Operator ####################


Expand Down Expand Up @@ -62,6 +63,39 @@ def get_extended_window_size(self):
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

Expand Down Expand Up @@ -1535,6 +1569,7 @@ def _load_internal(self, instrument, start_index, end_index, *args):

TOpsList = [TResample]
OpsList = [
ChangeInstrument,
you-n-g marked this conversation as resolved.
Show resolved Hide resolved
Rolling,
Ref,
Max,
Expand Down
92 changes: 92 additions & 0 deletions tests/ops/test_special_ops.py
Original file line number Diff line number Diff line change
@@ -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()