trade-database-manager 0.0.3.dev1__tar.gz → 0.0.6__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {trade_database_manager-0.0.3.dev1/trade_database_manager.egg-info → trade_database_manager-0.0.6}/PKG-INFO +23 -2
- {trade_database_manager-0.0.3.dev1 → trade_database_manager-0.0.6}/README.md +20 -0
- {trade_database_manager-0.0.3.dev1 → trade_database_manager-0.0.6}/pyproject.toml +4 -7
- {trade_database_manager-0.0.3.dev1 → trade_database_manager-0.0.6}/trade_database_manager/core/kdb/__init__.py +1 -1
- {trade_database_manager-0.0.3.dev1 → trade_database_manager-0.0.6}/trade_database_manager/core/kdb/kdbmanager.py +43 -17
- {trade_database_manager-0.0.3.dev1 → trade_database_manager-0.0.6}/trade_database_manager/core/sql/__init__.py +2 -1
- {trade_database_manager-0.0.3.dev1 → trade_database_manager-0.0.6}/trade_database_manager/core/sql/sqlmanager.py +164 -54
- trade_database_manager-0.0.6/trade_database_manager/core/sql/utils.py +35 -0
- {trade_database_manager-0.0.3.dev1 → trade_database_manager-0.0.6}/trade_database_manager/manager/__init__.py +2 -1
- trade_database_manager-0.0.6/trade_database_manager/manager/fields_data_type.py +55 -0
- {trade_database_manager-0.0.3.dev1 → trade_database_manager-0.0.6}/trade_database_manager/manager/metadata_sql.py +77 -23
- trade_database_manager-0.0.6/trade_database_manager/manager/metadata_sql_cb.py +108 -0
- {trade_database_manager-0.0.3.dev1 → trade_database_manager-0.0.6}/trade_database_manager/manager/typedefs.py +1 -1
- {trade_database_manager-0.0.3.dev1 → trade_database_manager-0.0.6/trade_database_manager.egg-info}/PKG-INFO +23 -2
- {trade_database_manager-0.0.3.dev1 → trade_database_manager-0.0.6}/trade_database_manager.egg-info/SOURCES.txt +2 -0
- trade_database_manager-0.0.3.dev1/trade_database_manager/manager/fields_data_type.py +0 -23
- {trade_database_manager-0.0.3.dev1 → trade_database_manager-0.0.6}/LICENSE +0 -0
- {trade_database_manager-0.0.3.dev1 → trade_database_manager-0.0.6}/setup.cfg +0 -0
- {trade_database_manager-0.0.3.dev1 → trade_database_manager-0.0.6}/trade_database_manager/__init__.py +0 -0
- {trade_database_manager-0.0.3.dev1 → trade_database_manager-0.0.6}/trade_database_manager/config.py +0 -0
- {trade_database_manager-0.0.3.dev1 → trade_database_manager-0.0.6}/trade_database_manager/core/__init__.py +0 -0
- {trade_database_manager-0.0.3.dev1 → trade_database_manager-0.0.6}/trade_database_manager/core/sql/sqlreader.py +0 -0
- {trade_database_manager-0.0.3.dev1 → trade_database_manager-0.0.6}/trade_database_manager/core/sql/sqlwriter.py +0 -0
- {trade_database_manager-0.0.3.dev1 → trade_database_manager-0.0.6}/trade_database_manager/core/typedefs.py +0 -0
- {trade_database_manager-0.0.3.dev1 → trade_database_manager-0.0.6}/trade_database_manager.egg-info/dependency_links.txt +0 -0
- {trade_database_manager-0.0.3.dev1 → trade_database_manager-0.0.6}/trade_database_manager.egg-info/requires.txt +0 -0
- {trade_database_manager-0.0.3.dev1 → trade_database_manager-0.0.6}/trade_database_manager.egg-info/top_level.txt +0 -0
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: trade_database_manager
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.6
|
|
4
4
|
Summary: A wrapper of kdb and sql for convenient trade data management.
|
|
5
|
-
Author-email: "Y.Q.
|
|
5
|
+
Author-email: "Y.Q. Tsui" <qianyun210603@hotmail.com>
|
|
6
6
|
Classifier: Operating System :: POSIX :: Linux
|
|
7
7
|
Classifier: Operating System :: Microsoft :: Windows
|
|
8
8
|
Classifier: Development Status :: 3 - Alpha
|
|
@@ -12,6 +12,7 @@ Classifier: Programming Language :: Python :: 3
|
|
|
12
12
|
Classifier: Programming Language :: Python :: 3.9
|
|
13
13
|
Classifier: Programming Language :: Python :: 3.10
|
|
14
14
|
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
16
|
Description-Content-Type: text/markdown
|
|
16
17
|
License-File: LICENSE
|
|
17
18
|
Requires-Dist: pandas>=2.0.0
|
|
@@ -75,3 +76,23 @@ then the system will send an email with the license file and a base64 key (Eithe
|
|
|
75
76
|
- `-T 1000`: This sets the timeout in seconds for client queries. In this case, it's set to 1000 seconds.
|
|
76
77
|
|
|
77
78
|
- `-U /opt/l64/trade.q`: This sets the access control list file. In this case, the file is located at `/opt/l64/trade.q`. This file contains a list of usernames and passwords for clients that are allowed to connect to the kdb+ process.
|
|
79
|
+
|
|
80
|
+
### MetaData Initialization
|
|
81
|
+
|
|
82
|
+
Allowed instruments types are given in "Instrument Types" section of [meta_enumerations.md](doc/meta_enumerations.md).
|
|
83
|
+
|
|
84
|
+
### Data Initialization for Instrument Type(s)
|
|
85
|
+
```python
|
|
86
|
+
from trade_database_manager.manager import MetadataSql
|
|
87
|
+
|
|
88
|
+
metadatalib = MetadataSql()
|
|
89
|
+
metadatalib.initialize(for_inst_types="CB")
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
This will try to create two tables, `instruments` and `instruments_cb` in the database if not yet exists. The `instruments` table will store the common information of all instruments, and the `instruments_cb` table will store the type-specific information of the instruments of type `CB`.
|
|
93
|
+
|
|
94
|
+
The table fields are listed in the [data_organization.md](doc/data_organization.md) file.
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
|
|
@@ -53,3 +53,23 @@ then the system will send an email with the license file and a base64 key (Eithe
|
|
|
53
53
|
- `-T 1000`: This sets the timeout in seconds for client queries. In this case, it's set to 1000 seconds.
|
|
54
54
|
|
|
55
55
|
- `-U /opt/l64/trade.q`: This sets the access control list file. In this case, the file is located at `/opt/l64/trade.q`. This file contains a list of usernames and passwords for clients that are allowed to connect to the kdb+ process.
|
|
56
|
+
|
|
57
|
+
### MetaData Initialization
|
|
58
|
+
|
|
59
|
+
Allowed instruments types are given in "Instrument Types" section of [meta_enumerations.md](doc/meta_enumerations.md).
|
|
60
|
+
|
|
61
|
+
### Data Initialization for Instrument Type(s)
|
|
62
|
+
```python
|
|
63
|
+
from trade_database_manager.manager import MetadataSql
|
|
64
|
+
|
|
65
|
+
metadatalib = MetadataSql()
|
|
66
|
+
metadatalib.initialize(for_inst_types="CB")
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
This will try to create two tables, `instruments` and `instruments_cb` in the database if not yet exists. The `instruments` table will store the common information of all instruments, and the `instruments_cb` table will store the type-specific information of the instruments of type `CB`.
|
|
70
|
+
|
|
71
|
+
The table fields are listed in the [data_organization.md](doc/data_organization.md) file.
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
|
|
@@ -4,11 +4,11 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "trade_database_manager"
|
|
7
|
-
version = "0.0.
|
|
7
|
+
version = "0.0.6"
|
|
8
8
|
description = "A wrapper of kdb and sql for convenient trade data management."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
authors = [
|
|
11
|
-
{name = "Y.Q.
|
|
11
|
+
{name = "Y.Q. Tsui", email = "qianyun210603@hotmail.com"},
|
|
12
12
|
]
|
|
13
13
|
dependencies = ["pandas>=2.0.0", "numpy", "ruamel.yaml", "sqlalchemy", "psycopg2"]
|
|
14
14
|
classifiers = [
|
|
@@ -21,6 +21,7 @@ classifiers = [
|
|
|
21
21
|
"Programming Language :: Python :: 3.9",
|
|
22
22
|
"Programming Language :: Python :: 3.10",
|
|
23
23
|
"Programming Language :: Python :: 3.11",
|
|
24
|
+
"Programming Language :: Python :: 3.12",
|
|
24
25
|
]
|
|
25
26
|
|
|
26
27
|
[tool.setuptools.packages.find]
|
|
@@ -30,7 +31,7 @@ include = ["trade_database_manager*"]
|
|
|
30
31
|
const-rgx='[a-z_][a-z0-9_]{2,30}$'
|
|
31
32
|
|
|
32
33
|
[tool.pylint.main]
|
|
33
|
-
disable="C0104,C0114,C0115,C0116,C0301,C0302,C0411,C0413,C1802,R0401,R0801,R0902,R0903,R0904,R0911,R0912,R0913,R0914,R0915,R1702,R1720,W0105,W0123,W0201,W0511,W0613,W1113,W1514,E0401,E1121,C0103,C0209,R0402,R1705,R1710,R1725,R1735,W0102,W0212,W0221,W0223,W0231,W0237,W0612,W0621,W0622,W0703,W1309,E1102,E1136"
|
|
34
|
+
disable="C0104,C0114,C0115,C0116,C0301,C0302,C0411,C0413,C1802,R0401,R0801,R0902,R0903,R0904,R0911,R0912,R0913,R0914,R0915,R1702,R1720,W0105,W0123,W0201,W0511,W0613,W1113,W1514,E0401,E1121,C0103,C0209,R0402,R0917,R1705,R1710,R1725,R1735,W0102,W0212,W0221,W0223,W0231,W0237,W0612,W0621,W0622,W0703,W1309,E1102,E1136"
|
|
34
35
|
ignore-paths="\\.ipynb_checkpoints/*"
|
|
35
36
|
|
|
36
37
|
[tool.pylint.format]
|
|
@@ -43,7 +44,3 @@ target-version = ['py310']
|
|
|
43
44
|
[tool.isort]
|
|
44
45
|
profile = "black"
|
|
45
46
|
line_length = 120
|
|
46
|
-
|
|
47
|
-
[tool.flake8]
|
|
48
|
-
max-line-length = 120
|
|
49
|
-
ignore = "E203,E501,W503"
|
|
@@ -4,8 +4,10 @@
|
|
|
4
4
|
# @File : kdbmanager.py
|
|
5
5
|
# @Purpose :
|
|
6
6
|
import os.path
|
|
7
|
+
|
|
7
8
|
import pandas as pd
|
|
8
9
|
import pykx
|
|
10
|
+
|
|
9
11
|
from ...config import CONFIG
|
|
10
12
|
|
|
11
13
|
|
|
@@ -17,7 +19,6 @@ class KdbManager:
|
|
|
17
19
|
def instance(cls):
|
|
18
20
|
if cls._instance is None:
|
|
19
21
|
cls._instance = cls()
|
|
20
|
-
cls._instance.__init__()
|
|
21
22
|
return cls._instance
|
|
22
23
|
|
|
23
24
|
def __init__(self):
|
|
@@ -47,7 +48,7 @@ class KdbManager:
|
|
|
47
48
|
:type path: str
|
|
48
49
|
"""
|
|
49
50
|
with pykx.QConnection(self.host, self.port, username=self.username, password=self.password) as conn:
|
|
50
|
-
conn(f
|
|
51
|
+
conn(f'.path.mkdir "{path}"')
|
|
51
52
|
|
|
52
53
|
def write(self, table_name: str, data: pd.DataFrame, path: str = "", splayed: bool = False):
|
|
53
54
|
"""
|
|
@@ -69,7 +70,9 @@ class KdbManager:
|
|
|
69
70
|
with pykx.QConnection(self.host, self.port, username=self.username, password=self.password) as conn:
|
|
70
71
|
conn(f"{{`:{real_path}/ set x}}", data)
|
|
71
72
|
|
|
72
|
-
def write_partitioned(
|
|
73
|
+
def write_partitioned(
|
|
74
|
+
self, table_name: str, data: pd.DataFrame, path: str = "", partition_func=None, key_column=None
|
|
75
|
+
):
|
|
73
76
|
"""
|
|
74
77
|
Writes data to the kdb database with partitioning.
|
|
75
78
|
|
|
@@ -89,16 +92,31 @@ class KdbManager:
|
|
|
89
92
|
assert "datetime" in data.columns, "datetime column not found"
|
|
90
93
|
data.sort_values(by="datetime", inplace=True)
|
|
91
94
|
if key_column is not None and key_column in data.columns:
|
|
92
|
-
with pykx.QConnection(
|
|
95
|
+
with pykx.QConnection(
|
|
96
|
+
host=self.host, port=self.port, username=self.username, password=self.password
|
|
97
|
+
) as conn:
|
|
93
98
|
for bucket, df in data.groupby(partition_func(data["datetime"])):
|
|
94
|
-
conn(
|
|
99
|
+
conn(
|
|
100
|
+
f"{{`{table_name} set x; .partable.createOrAppend[`:{path};{bucket};`{key_column};`{table_name}]}}",
|
|
101
|
+
df.reset_index(drop=True),
|
|
102
|
+
)
|
|
95
103
|
else:
|
|
96
|
-
with pykx.QConnection(
|
|
104
|
+
with pykx.QConnection(
|
|
105
|
+
host=self.host, port=self.port, username=self.username, password=self.password
|
|
106
|
+
) as conn:
|
|
97
107
|
for bucket, df in data.groupby(by=partition_func(data["datetime"])):
|
|
98
|
-
conn(f
|
|
99
|
-
|
|
100
|
-
def read_partitioned(
|
|
101
|
-
|
|
108
|
+
conn(f"{{`{table_name} set x;.Q.dpt[`:{path};{bucket};`{table_name}]}}", df.reset_index(drop=True))
|
|
109
|
+
|
|
110
|
+
def read_partitioned(
|
|
111
|
+
self,
|
|
112
|
+
table_name: str,
|
|
113
|
+
path: str = "",
|
|
114
|
+
fields=None,
|
|
115
|
+
start_time=None,
|
|
116
|
+
end_time=None,
|
|
117
|
+
partition_func=None,
|
|
118
|
+
other_conditions=None,
|
|
119
|
+
):
|
|
102
120
|
"""
|
|
103
121
|
Reads data from the kdb database with partitioning.
|
|
104
122
|
|
|
@@ -120,31 +138,39 @@ class KdbManager:
|
|
|
120
138
|
end_time_str = end_time.strftime(time_format) if end_time is not None else None
|
|
121
139
|
if isinstance(fields, (bytes, str)):
|
|
122
140
|
fields = [fields]
|
|
123
|
-
select_clause =
|
|
141
|
+
select_clause = (
|
|
142
|
+
f"select from {table_name}" if fields is None else f"select {','.join(fields)} from {table_name}"
|
|
143
|
+
)
|
|
124
144
|
where_cond = ""
|
|
125
145
|
if start_time_str is not None and end_time_str is not None:
|
|
126
146
|
where_cond += f"datetime within ({start_time_str};{end_time_str})"
|
|
127
147
|
if partition_func is not None:
|
|
128
|
-
where_cond =
|
|
148
|
+
where_cond = (
|
|
149
|
+
f"int in {' '.join(str(x) for x in range(partition_func(start_time), partition_func(end_time) + 1))}"
|
|
150
|
+
+ ","
|
|
151
|
+
+ where_cond
|
|
152
|
+
)
|
|
129
153
|
elif start_time_str is not None:
|
|
130
154
|
where_cond += f"datetime>={start_time_str}"
|
|
131
155
|
if partition_func is not None:
|
|
132
|
-
where_cond = f"int>={partition_func(start_time)}" +
|
|
156
|
+
where_cond = f"int>={partition_func(start_time)}" + "," + where_cond
|
|
133
157
|
elif end_time_str is not None:
|
|
134
158
|
where_cond += f"datetime<={end_time_str}"
|
|
135
159
|
if partition_func is not None:
|
|
136
|
-
where_cond = f"int<= {partition_func(end_time)}" +
|
|
160
|
+
where_cond = f"int<= {partition_func(end_time)}" + "," + where_cond
|
|
137
161
|
if other_conditions is not None:
|
|
138
|
-
where_cond = other_conditions if where_cond == "" else where_cond +
|
|
162
|
+
where_cond = other_conditions if where_cond == "" else where_cond + "," + other_conditions
|
|
139
163
|
if where_cond:
|
|
140
164
|
where_cond = " where " + where_cond
|
|
141
165
|
final_query = select_clause + where_cond
|
|
142
166
|
|
|
143
167
|
with pykx.QConnection(host=self.host, port=self.port, username=self.username, password=self.password) as conn:
|
|
144
|
-
conn(
|
|
168
|
+
conn(
|
|
169
|
+
"`currpath__ set .path.pwd[]"
|
|
170
|
+
) # save current path to currpath__ as following command will change the path
|
|
145
171
|
try:
|
|
146
172
|
conn(f"\\l {path}") # load the path
|
|
147
173
|
q_table = conn(final_query)
|
|
148
174
|
return q_table.pd().set_index("datetime")
|
|
149
175
|
finally:
|
|
150
|
-
conn('system "cd ", currpath__')
|
|
176
|
+
conn('system "cd ", currpath__')
|
|
@@ -7,14 +7,30 @@
|
|
|
7
7
|
import re
|
|
8
8
|
from collections.abc import Container
|
|
9
9
|
from functools import partial, reduce
|
|
10
|
-
from typing import Any, Literal, Sequence,
|
|
10
|
+
from typing import Any, Callable, Literal, Sequence, Union
|
|
11
11
|
|
|
12
12
|
import pandas as pd
|
|
13
|
-
from sqlalchemy import
|
|
13
|
+
from sqlalchemy import (
|
|
14
|
+
Column,
|
|
15
|
+
Executable,
|
|
16
|
+
Index,
|
|
17
|
+
MetaData,
|
|
18
|
+
PrimaryKeyConstraint,
|
|
19
|
+
Table,
|
|
20
|
+
and_,
|
|
21
|
+
create_engine,
|
|
22
|
+
func,
|
|
23
|
+
inspect,
|
|
24
|
+
select,
|
|
25
|
+
text,
|
|
26
|
+
)
|
|
14
27
|
from sqlalchemy.dialects.postgresql import insert
|
|
28
|
+
from sqlalchemy.orm import aliased, sessionmaker
|
|
29
|
+
from sqlalchemy.types import TypeEngine
|
|
15
30
|
|
|
16
31
|
from ...config import CONFIG
|
|
17
32
|
from ..typedefs import FILTERFIELD_TYPE, QUERYFIELD_TYPE
|
|
33
|
+
from .utils import infer_sql_type
|
|
18
34
|
|
|
19
35
|
|
|
20
36
|
def _insert_on_conflict_update(table, conn, keys, data_iter, indexes):
|
|
@@ -37,12 +53,22 @@ class SqlManager:
|
|
|
37
53
|
This class is used to manage SQL operations.
|
|
38
54
|
|
|
39
55
|
:ivar sqlalchemy.engine.Engine engine: An instance of the SQLAlchemy Engine class for executing SQL operations.
|
|
56
|
+
:ivar sqlalchemy.orm.Session _session: An instance of the SQLAlchemy Session class for executing SQL operations.
|
|
57
|
+
:ivar sqlalchemy.engine.reflection.Inspector _inspector: An instance of the SQLAlchemy Inspector class for inspecting the database.
|
|
40
58
|
"""
|
|
41
59
|
|
|
42
60
|
def __init__(self):
|
|
43
61
|
self.engine = create_engine(CONFIG["sqlconnstr"])
|
|
62
|
+
self._session = sessionmaker(bind=self.engine)() # TODO: is it better to be a class attribute?
|
|
63
|
+
self._inspector = None
|
|
44
64
|
|
|
45
|
-
|
|
65
|
+
@property
|
|
66
|
+
def inspector(self):
|
|
67
|
+
if self._inspector is None:
|
|
68
|
+
self._inspector = inspect(self.engine)
|
|
69
|
+
return self._inspector
|
|
70
|
+
|
|
71
|
+
def _execute(self, sql_executable: Union[str, Executable]) -> Any:
|
|
46
72
|
if isinstance(sql_executable, str):
|
|
47
73
|
sql_executable = text(sql_executable)
|
|
48
74
|
with self.engine.begin() as conn:
|
|
@@ -94,8 +120,7 @@ class SqlManager:
|
|
|
94
120
|
:rtype: int
|
|
95
121
|
"""
|
|
96
122
|
if_exists: Literal["replace", "append"] = "append"
|
|
97
|
-
|
|
98
|
-
new_table = not inspector.has_table(table_name)
|
|
123
|
+
new_table = not self.inspector.has_table(table_name)
|
|
99
124
|
method = partial(_insert_on_conflict_update, indexes=df.index.names) if upsert and not new_table else None
|
|
100
125
|
num_rows = df.to_sql(
|
|
101
126
|
table_name,
|
|
@@ -113,17 +138,48 @@ class SqlManager:
|
|
|
113
138
|
self.add_index(table_name, column, unique=False)
|
|
114
139
|
return num_rows
|
|
115
140
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
if
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
if
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
141
|
+
def table_exists(self, table_name: str) -> bool:
|
|
142
|
+
"""
|
|
143
|
+
Checks if a table exists.
|
|
144
|
+
|
|
145
|
+
:param table_name: The name of the table to check for.
|
|
146
|
+
:type table_name: str
|
|
147
|
+
:return: True if the table exists, False otherwise.
|
|
148
|
+
:rtype: bool
|
|
149
|
+
"""
|
|
150
|
+
return self.inspector.has_table(table_name)
|
|
151
|
+
|
|
152
|
+
def create_table(
|
|
153
|
+
self,
|
|
154
|
+
table_name: str,
|
|
155
|
+
table_columns: list[tuple[str, TypeEngine | type | tuple[type, tuple]]],
|
|
156
|
+
unique_index_columns: Sequence[str] = (),
|
|
157
|
+
primary_key: str | set[str] = set(),
|
|
158
|
+
):
|
|
159
|
+
"""
|
|
160
|
+
Creates a table.
|
|
161
|
+
|
|
162
|
+
:param table_name: The name of the table to create.
|
|
163
|
+
:type table_name: str
|
|
164
|
+
:param table_columns: The names of the columns to create with their data types. The keys are the column names and the values are the data types.
|
|
165
|
+
:type table_columns: dict[str, Type]
|
|
166
|
+
:param unique_index_columns: Columns to enforce unique values on. Defaults to an empty sequence.
|
|
167
|
+
:type unique_index_columns: Sequence[str]
|
|
168
|
+
:param primary_key: The primary key(s) of the table. Defaults to an empty set.
|
|
169
|
+
:type primary_key: Union[str, set[str|tuple[str]]]
|
|
170
|
+
|
|
171
|
+
"""
|
|
172
|
+
table_meta = MetaData()
|
|
173
|
+
columns = [Column(name, infer_sql_type(col_type)) for name, col_type in table_columns]
|
|
174
|
+
if isinstance(primary_key, str) and primary_key != "":
|
|
175
|
+
columns.append(PrimaryKeyConstraint(primary_key))
|
|
176
|
+
elif primary_key:
|
|
177
|
+
columns.append(PrimaryKeyConstraint(*primary_key, name=f"pk_{table_name}"))
|
|
178
|
+
|
|
179
|
+
table = Table(table_name, table_meta, *columns)
|
|
180
|
+
table.create(self.engine)
|
|
181
|
+
for column in unique_index_columns:
|
|
182
|
+
self.add_index(table_name, column, unique=True)
|
|
127
183
|
|
|
128
184
|
def insert_column(self, table_name: str, column_name: str, column_type: str):
|
|
129
185
|
"""
|
|
@@ -163,7 +219,7 @@ class SqlManager:
|
|
|
163
219
|
"""
|
|
164
220
|
# check if any index is referring to the column
|
|
165
221
|
sql_code = f"SELECT indexname, indexdef FROM pg_indexes WHERE indexdef LIKE '%%(%%{old_column_name}%%)%%' and tablename = '{table_name}'"
|
|
166
|
-
index_refering_column = dict(self.
|
|
222
|
+
index_refering_column = dict(self._execute(sql_code).fetchall())
|
|
167
223
|
|
|
168
224
|
# drop indexes referring to the column
|
|
169
225
|
for index_name in index_refering_column:
|
|
@@ -213,7 +269,7 @@ class SqlManager:
|
|
|
213
269
|
for field, filter_values in filter_fields.items()
|
|
214
270
|
]
|
|
215
271
|
)
|
|
216
|
-
stmt = stmt.where(
|
|
272
|
+
stmt = stmt.where(and_(*conditions))
|
|
217
273
|
res = self._execute(stmt)
|
|
218
274
|
return pd.DataFrame(res.fetchall(), columns=res.keys())
|
|
219
275
|
|
|
@@ -250,7 +306,7 @@ class SqlManager:
|
|
|
250
306
|
)
|
|
251
307
|
for field, filter_values in filter_fields.items()
|
|
252
308
|
]
|
|
253
|
-
stmt = stmt.where(
|
|
309
|
+
stmt = stmt.where(and_(*conditions))
|
|
254
310
|
|
|
255
311
|
# cannot use pandas.read_sql here as it discards timezone info
|
|
256
312
|
res = self._execute(stmt)
|
|
@@ -281,7 +337,7 @@ class SqlManager:
|
|
|
281
337
|
tables = {table_name: Table(table_name, meta, autoload_with=self.engine) for table_name in table_names}
|
|
282
338
|
|
|
283
339
|
joined_table = reduce(
|
|
284
|
-
lambda x, y: x.join(y,
|
|
340
|
+
lambda x, y: x.join(y, and_(*[x.columns[col] == y.columns[col] for col in joined_columns])),
|
|
285
341
|
tables.values(),
|
|
286
342
|
)
|
|
287
343
|
|
|
@@ -308,43 +364,97 @@ class SqlManager:
|
|
|
308
364
|
else table.columns[field] == filter_values
|
|
309
365
|
)
|
|
310
366
|
|
|
311
|
-
stmt = stmt.where(
|
|
367
|
+
stmt = stmt.where(and_(*conditions))
|
|
312
368
|
|
|
313
369
|
# cannot use pandas.read_sql here as it discards timezone info
|
|
314
370
|
res = self._execute(stmt)
|
|
315
371
|
return pd.DataFrame(res.fetchall(), columns=res.keys())
|
|
316
372
|
|
|
317
|
-
def
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
373
|
+
def _read_extremum_in_group(
|
|
374
|
+
self,
|
|
375
|
+
table_name: str,
|
|
376
|
+
target_column: str | list[str],
|
|
377
|
+
group_column: str | list[str],
|
|
378
|
+
extremum_column: str,
|
|
379
|
+
method: str | Callable,
|
|
380
|
+
filter_fields=None,
|
|
381
|
+
):
|
|
382
|
+
if isinstance(target_column, str):
|
|
383
|
+
target_column = [target_column]
|
|
384
|
+
if isinstance(group_column, str):
|
|
385
|
+
group_column = [group_column]
|
|
386
|
+
target_column_c = [Column(col) for col in target_column]
|
|
387
|
+
group_column_c = [Column(col) for col in group_column]
|
|
388
|
+
extremum_column_c = Column(extremum_column)
|
|
321
389
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
390
|
+
table_meta = MetaData()
|
|
391
|
+
|
|
392
|
+
full_table = (
|
|
393
|
+
Table(table_name, table_meta, *target_column_c, *group_column_c)
|
|
394
|
+
if extremum_column in target_column
|
|
395
|
+
else Table(table_name, table_meta, *target_column_c, *group_column_c, extremum_column_c)
|
|
396
|
+
)
|
|
397
|
+
|
|
398
|
+
if isinstance(method, str):
|
|
399
|
+
method = getattr(func, method)
|
|
400
|
+
|
|
401
|
+
if filter_fields:
|
|
402
|
+
conditions = [
|
|
403
|
+
(
|
|
404
|
+
full_table.columns[field].in_(filter_values)
|
|
405
|
+
if isinstance(filter_values, Container) and not isinstance(filter_values, (str, bytes))
|
|
406
|
+
else full_table.columns[field] == filter_values
|
|
407
|
+
)
|
|
408
|
+
for field, filter_values in filter_fields.items()
|
|
409
|
+
]
|
|
410
|
+
subquery = (
|
|
411
|
+
self._session.query(*group_column_c, method(extremum_column_c).label("ColExtremum"))
|
|
412
|
+
.where(and_(*conditions))
|
|
413
|
+
.group_by(*group_column_c)
|
|
414
|
+
.subquery()
|
|
415
|
+
)
|
|
416
|
+
else:
|
|
417
|
+
subquery = (
|
|
418
|
+
self._session.query(*group_column_c, method(extremum_column_c).label("ColExtremum"))
|
|
419
|
+
.group_by(*group_column_c)
|
|
420
|
+
.subquery()
|
|
421
|
+
)
|
|
422
|
+
|
|
423
|
+
t_full = aliased(full_table)
|
|
424
|
+
t_sub = aliased(subquery)
|
|
425
|
+
|
|
426
|
+
cols_res = [getattr(t_full.c, col) for col in group_column + target_column]
|
|
427
|
+
|
|
428
|
+
query = self._session.query(*cols_res).join(
|
|
429
|
+
t_sub,
|
|
430
|
+
and_(
|
|
431
|
+
getattr(t_full.c, extremum_column) == t_sub.c.ColExtremum,
|
|
432
|
+
*[getattr(t_full.c, col) == getattr(t_sub.c, col) for col in group_column],
|
|
433
|
+
),
|
|
434
|
+
)
|
|
435
|
+
|
|
436
|
+
return pd.DataFrame(query.all(), columns=group_column + target_column)
|
|
437
|
+
|
|
438
|
+
def read_max_in_group(
|
|
439
|
+
self,
|
|
440
|
+
table_name: str,
|
|
441
|
+
target_column: str | list[str],
|
|
442
|
+
group_column: str | list[str],
|
|
443
|
+
extremum_column: str,
|
|
444
|
+
filter_fields=None,
|
|
445
|
+
):
|
|
446
|
+
return self._read_extremum_in_group(
|
|
447
|
+
table_name, target_column, group_column, extremum_column, "max", filter_fields
|
|
448
|
+
)
|
|
449
|
+
|
|
450
|
+
def read_min_in_group(
|
|
451
|
+
self,
|
|
452
|
+
table_name: str,
|
|
453
|
+
target_column: str | list[str],
|
|
454
|
+
group_column: str | list[str],
|
|
455
|
+
extremum_column: str,
|
|
456
|
+
filter_fields=None,
|
|
457
|
+
):
|
|
458
|
+
return self._read_extremum_in_group(
|
|
459
|
+
table_name, target_column, group_column, extremum_column, "min", filter_fields
|
|
460
|
+
)
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# @Time : 2024/10/31 21:07
|
|
3
|
+
# @Author : YQ Tsui
|
|
4
|
+
# @File : utils.py
|
|
5
|
+
# @Purpose : utility functions for sqlmanager
|
|
6
|
+
|
|
7
|
+
from datetime import date, datetime
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
import pandas as pd
|
|
11
|
+
from sqlalchemy.types import DOUBLE_PRECISION, Date, DateTime, Integer, String, Text, TypeEngine
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def infer_sql_type(input_type):
|
|
15
|
+
if isinstance(input_type, TypeEngine):
|
|
16
|
+
return input_type
|
|
17
|
+
if isinstance(input_type, (tuple, list)):
|
|
18
|
+
input_type, *args = input_type
|
|
19
|
+
else:
|
|
20
|
+
args = ()
|
|
21
|
+
if issubclass(input_type, str):
|
|
22
|
+
if args:
|
|
23
|
+
return String(*args)
|
|
24
|
+
return Text()
|
|
25
|
+
if issubclass(input_type, (int, np.signedinteger)):
|
|
26
|
+
return Integer()
|
|
27
|
+
if issubclass(input_type, (np.floating, float)):
|
|
28
|
+
return DOUBLE_PRECISION()
|
|
29
|
+
if issubclass(input_type, (bool, np.bool_)):
|
|
30
|
+
return Integer()
|
|
31
|
+
if issubclass(input_type, (np.datetime64, pd.Timestamp, datetime)):
|
|
32
|
+
return Date() if args and args[0] else DateTime()
|
|
33
|
+
if issubclass(input_type, date):
|
|
34
|
+
return Date()
|
|
35
|
+
raise ValueError(f"Unsupported type {input_type}")
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# @Time : 2024/4/19 16:44
|
|
3
|
+
# @Author : YQ Tsui
|
|
4
|
+
# @File : fields_data_type.py
|
|
5
|
+
# @Purpose :
|
|
6
|
+
|
|
7
|
+
from sqlalchemy import DOUBLE_PRECISION, Date, Integer, String, Text
|
|
8
|
+
|
|
9
|
+
FIELD_DATA_TYPE_SQL = {
|
|
10
|
+
"ticker": String(20),
|
|
11
|
+
"name": String(20),
|
|
12
|
+
"currency": String(6),
|
|
13
|
+
"exchange": String(10),
|
|
14
|
+
"timezone": String(30),
|
|
15
|
+
"tick_size": DOUBLE_PRECISION(),
|
|
16
|
+
"lot_size": DOUBLE_PRECISION(),
|
|
17
|
+
"min_lots": DOUBLE_PRECISION(),
|
|
18
|
+
"market_tplus": Integer(),
|
|
19
|
+
"listed_date": Date(),
|
|
20
|
+
"delisted_date": Date(),
|
|
21
|
+
"country": String(6),
|
|
22
|
+
"state": String(36),
|
|
23
|
+
# STK
|
|
24
|
+
"sector": String(30),
|
|
25
|
+
"industry": String(36),
|
|
26
|
+
"board_type": String(200),
|
|
27
|
+
# LOF & ETF
|
|
28
|
+
"issuer": String(60),
|
|
29
|
+
"current_mgr": String(60),
|
|
30
|
+
"custodian": String(60),
|
|
31
|
+
"issuer_country": String(6),
|
|
32
|
+
"fund_type": String(20),
|
|
33
|
+
"benchmark": String(60),
|
|
34
|
+
# Convertible Bond (CB)
|
|
35
|
+
"stock_ticker": String(20),
|
|
36
|
+
"stock_exchange": String(10),
|
|
37
|
+
"maturity_date": Date(),
|
|
38
|
+
"issue_price": DOUBLE_PRECISION(),
|
|
39
|
+
"total_issue_size": DOUBLE_PRECISION(),
|
|
40
|
+
"par_value": DOUBLE_PRECISION(),
|
|
41
|
+
"redeem_price": DOUBLE_PRECISION(),
|
|
42
|
+
"conversion_start_date": Date(),
|
|
43
|
+
"conversion_end_date": Date(),
|
|
44
|
+
"callback_terms": Text(),
|
|
45
|
+
"callback_type": String(20),
|
|
46
|
+
"adjust_terms": Text(),
|
|
47
|
+
"adjust_type": String(20),
|
|
48
|
+
"putback_terms": Text(),
|
|
49
|
+
"putback_type": String(20),
|
|
50
|
+
"callback_level": DOUBLE_PRECISION(),
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
DATE_TIME_COLS = {"listed_date", "delisted_date", "maturity_date", "conversion_start_date", "conversion_end_date"}
|
|
54
|
+
|
|
55
|
+
BASE_COLUMNS = [("ticker", String(10)), ("exchange", String(10))]
|
|
@@ -10,6 +10,7 @@ from typing import Union, cast
|
|
|
10
10
|
import pandas as pd
|
|
11
11
|
|
|
12
12
|
from ..core.sql.sqlmanager import SqlManager
|
|
13
|
+
from .fields_data_type import BASE_COLUMNS, DATE_TIME_COLS, FIELD_DATA_TYPE_SQL
|
|
13
14
|
from .typedefs import EXCHANGE_LITERALS, INST_TYPE_LITERALS, Opt_T_SeqT, T_DictT
|
|
14
15
|
|
|
15
16
|
COMMON_METADATA_COLUMNS = [
|
|
@@ -26,7 +27,28 @@ COMMON_METADATA_COLUMNS = [
|
|
|
26
27
|
"delisted_date",
|
|
27
28
|
]
|
|
28
29
|
TYPE_METADATA_COLUMNS = {
|
|
29
|
-
"STK": ["
|
|
30
|
+
"STK": ["country", "state", "board_type", "issue_price"],
|
|
31
|
+
"ETF": ["issuer", "current_mgr", "custodian", "issuer_country", "fund_type", "benchmark"],
|
|
32
|
+
"LOF": ["issuer", "current_mgr", "custodian", "issuer_country", "fund_type", "benchmark"],
|
|
33
|
+
"CB": [
|
|
34
|
+
"country",
|
|
35
|
+
"state",
|
|
36
|
+
"stock_ticker",
|
|
37
|
+
"stock_exchange",
|
|
38
|
+
"maturity_date",
|
|
39
|
+
"issue_price",
|
|
40
|
+
"total_issue_size",
|
|
41
|
+
"par_value",
|
|
42
|
+
"redemption_price",
|
|
43
|
+
"conversion_start_date",
|
|
44
|
+
"conversion_end_date",
|
|
45
|
+
"callback_terms",
|
|
46
|
+
"callback_type",
|
|
47
|
+
"adjust_terms",
|
|
48
|
+
"adjust_type",
|
|
49
|
+
"putback_terms",
|
|
50
|
+
"putback_type",
|
|
51
|
+
],
|
|
30
52
|
}
|
|
31
53
|
|
|
32
54
|
|
|
@@ -46,6 +68,21 @@ class MetadataSql:
|
|
|
46
68
|
cls._manager = SqlManager()
|
|
47
69
|
return cls._instance
|
|
48
70
|
|
|
71
|
+
def initialize(self, for_inst_types="all"):
|
|
72
|
+
if for_inst_types == "all":
|
|
73
|
+
for_inst_types = list(TYPE_METADATA_COLUMNS.keys())
|
|
74
|
+
elif isinstance(for_inst_types, str):
|
|
75
|
+
for_inst_types = [for_inst_types]
|
|
76
|
+
if not self._manager.table_exists("instruments"):
|
|
77
|
+
columns = BASE_COLUMNS + [(col, FIELD_DATA_TYPE_SQL[col]) for col in COMMON_METADATA_COLUMNS]
|
|
78
|
+
self._manager.create_table("instruments", columns, {"primary_key": ["ticker", "exchange"]})
|
|
79
|
+
for inst_type in for_inst_types:
|
|
80
|
+
if not self._manager.table_exists(f"instruments_{inst_type.lower()}"):
|
|
81
|
+
columns = BASE_COLUMNS + [(col, FIELD_DATA_TYPE_SQL[col]) for col in TYPE_METADATA_COLUMNS[inst_type]]
|
|
82
|
+
self._manager.create_table(
|
|
83
|
+
f"instruments_{inst_type.lower()}", columns, primary_key={"ticker", "exchange"}
|
|
84
|
+
)
|
|
85
|
+
|
|
49
86
|
def update_instrument_metadata(self, data: Union[pd.DataFrame, list[dict], dict]):
|
|
50
87
|
"""
|
|
51
88
|
Updates the instrument metadata in the database.
|
|
@@ -70,13 +107,16 @@ class MetadataSql:
|
|
|
70
107
|
|
|
71
108
|
self._manager.insert("instruments", data_common, upsert=True)
|
|
72
109
|
if "inst_type" in data.columns:
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
if not data_type_df.empty:
|
|
76
|
-
|
|
110
|
+
g = data.groupby("inst_type", group_keys=False)
|
|
111
|
+
for inst_type, data_type_df in g:
|
|
112
|
+
if not data_type_df.empty and inst_type in TYPE_METADATA_COLUMNS:
|
|
113
|
+
columns = data_type_df.columns.intersection(TYPE_METADATA_COLUMNS[inst_type])
|
|
114
|
+
if not columns.empty:
|
|
115
|
+
self._manager.insert(f"instruments_{inst_type.lower()}", data_type_df[columns], upsert=True)
|
|
77
116
|
|
|
78
|
-
|
|
79
|
-
|
|
117
|
+
@staticmethod
|
|
118
|
+
def _convert_datetime_columns(data: pd.DataFrame):
|
|
119
|
+
for col in DATE_TIME_COLS:
|
|
80
120
|
if col in data.columns:
|
|
81
121
|
data[col] = data[col].apply(pd.to_datetime)
|
|
82
122
|
|
|
@@ -132,8 +172,12 @@ class MetadataSql:
|
|
|
132
172
|
res = {}
|
|
133
173
|
for inst_type, common_df_by_type in common_df.groupby("inst_type"):
|
|
134
174
|
inst_type = cast(INST_TYPE_LITERALS, inst_type)
|
|
135
|
-
if all_fields_common:
|
|
136
|
-
res[inst_type] =
|
|
175
|
+
if all_fields_common or inst_type not in TYPE_METADATA_COLUMNS:
|
|
176
|
+
res[inst_type] = (
|
|
177
|
+
common_df_by_type.set_index(["ticker", "exchange"])
|
|
178
|
+
if len(common_df_by_type.columns) > 2
|
|
179
|
+
else common_df_by_type
|
|
180
|
+
)
|
|
137
181
|
continue
|
|
138
182
|
query_fields_type = (
|
|
139
183
|
["ticker", "exchange"] + [f for f in query_fields if f in TYPE_METADATA_COLUMNS[inst_type]]
|
|
@@ -185,9 +229,11 @@ class MetadataSql:
|
|
|
185
229
|
|
|
186
230
|
if query_fields == "*":
|
|
187
231
|
query_fields_cross = "*"
|
|
232
|
+
query_fields_type = []
|
|
233
|
+
query_fields_common = query_fields
|
|
188
234
|
else:
|
|
189
235
|
query_fields_common = ["ticker", "exchange"] + [f for f in query_fields if f in COMMON_METADATA_COLUMNS]
|
|
190
|
-
query_fields_type = [f for f in query_fields if f in TYPE_METADATA_COLUMNS[
|
|
236
|
+
query_fields_type = [f for f in query_fields if f in TYPE_METADATA_COLUMNS.get(inst_type, [])]
|
|
191
237
|
query_fields_cross = {
|
|
192
238
|
"instruments": query_fields_common,
|
|
193
239
|
f"instruments_{inst_type.lower()}": query_fields_type,
|
|
@@ -196,19 +242,27 @@ class MetadataSql:
|
|
|
196
242
|
filter_fields_common = {
|
|
197
243
|
k: v for k, v in filter_fields.items() if k in ["ticker", "exchange"] + COMMON_METADATA_COLUMNS
|
|
198
244
|
}
|
|
199
|
-
filter_fields_type = {
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
245
|
+
filter_fields_type = {k: v for k, v in filter_fields.items() if k in TYPE_METADATA_COLUMNS.get(inst_type, [])}
|
|
246
|
+
|
|
247
|
+
if (
|
|
248
|
+
(query_fields != "*" or inst_type not in TYPE_METADATA_COLUMNS)
|
|
249
|
+
and not bool(query_fields_type)
|
|
250
|
+
and not bool(filter_fields_type)
|
|
251
|
+
):
|
|
252
|
+
df = self._manager.read_data(
|
|
253
|
+
"instruments", query_fields=query_fields_common, filter_fields=filter_fields_common
|
|
254
|
+
)
|
|
255
|
+
else:
|
|
256
|
+
filter_fields_cross = {
|
|
257
|
+
"instruments": filter_fields_common,
|
|
258
|
+
f"instruments_{inst_type.lower()}": filter_fields_type,
|
|
259
|
+
}
|
|
260
|
+
df = self._manager.read_data_across_tables(
|
|
261
|
+
["instruments", f"instruments_{inst_type.lower()}"],
|
|
262
|
+
joined_columns=["ticker", "exchange"],
|
|
263
|
+
query_fields=query_fields_cross,
|
|
264
|
+
filter_fields=filter_fields_cross,
|
|
265
|
+
)
|
|
212
266
|
|
|
213
267
|
if isinstance(df.columns, pd.Index):
|
|
214
268
|
df = df.loc[:, ~df.columns.duplicated()]
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# @Time : 2024/11/12 13:49
|
|
3
|
+
# @Author : YQ Tsui
|
|
4
|
+
# @File : metadata_sql_cb.py
|
|
5
|
+
# @Purpose :
|
|
6
|
+
|
|
7
|
+
import pandas as pd
|
|
8
|
+
|
|
9
|
+
from .metadata_sql import MetadataSql
|
|
10
|
+
from .typedefs import EXCHANGE_LITERALS, Opt_T_SeqT, T_SeqT
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class CBMetadataSql(MetadataSql):
|
|
14
|
+
|
|
15
|
+
def read_latest_conversion_price(
|
|
16
|
+
self,
|
|
17
|
+
fields: T_SeqT[str] = ("conversion_price", "effective_date"),
|
|
18
|
+
tickers: Opt_T_SeqT[str] = None,
|
|
19
|
+
exchanges: Opt_T_SeqT[EXCHANGE_LITERALS] = None,
|
|
20
|
+
latest_by: str = "announcement_date",
|
|
21
|
+
) -> pd.DataFrame:
|
|
22
|
+
"""
|
|
23
|
+
Reads the convert price metadata from the database.
|
|
24
|
+
|
|
25
|
+
:param fields: The fields to query.
|
|
26
|
+
:type fields: T_SeqT[str]
|
|
27
|
+
:param tickers: The tickers to query, None for all.
|
|
28
|
+
:type tickers: Opt_T_SeqT[str]
|
|
29
|
+
:param exchanges: The exchanges to query, None for all.
|
|
30
|
+
:type exchanges: Opt_T_SeqT[EXCHANGE_LITERALS]
|
|
31
|
+
:param latest_by: The field to use for latest by.
|
|
32
|
+
:type latest_by: str
|
|
33
|
+
"""
|
|
34
|
+
if isinstance(fields, str):
|
|
35
|
+
fields = [fields]
|
|
36
|
+
elif isinstance(fields, tuple):
|
|
37
|
+
fields = list(fields)
|
|
38
|
+
if tickers is None and exchanges is None:
|
|
39
|
+
filter_fields = None
|
|
40
|
+
else:
|
|
41
|
+
filter_fields = {}
|
|
42
|
+
if tickers is not None:
|
|
43
|
+
filter_fields["ticker"] = tickers
|
|
44
|
+
if exchanges is not None:
|
|
45
|
+
filter_fields["exchange"] = exchanges
|
|
46
|
+
|
|
47
|
+
df = self._manager.read_max_in_group(
|
|
48
|
+
"cb_convert_price_history", fields, ["ticker", "exchange"], latest_by, filter_fields=filter_fields
|
|
49
|
+
)
|
|
50
|
+
return df.set_index(["ticker", "exchange"])
|
|
51
|
+
|
|
52
|
+
def update_conversion_price(self, data: pd.DataFrame):
|
|
53
|
+
"""
|
|
54
|
+
Updates the convert price metadata in the database.
|
|
55
|
+
|
|
56
|
+
:param data: The data to update.
|
|
57
|
+
:type data: pd.DataFrame
|
|
58
|
+
"""
|
|
59
|
+
self._manager.insert("cb_convert_price_history", data, upsert=True)
|
|
60
|
+
|
|
61
|
+
def read_bond_coupon(
|
|
62
|
+
self, fields: T_SeqT[str], tickers: Opt_T_SeqT[str] = None, exchanges: Opt_T_SeqT[EXCHANGE_LITERALS] = None
|
|
63
|
+
) -> pd.DataFrame:
|
|
64
|
+
"""
|
|
65
|
+
Reads the convert coupon metadata from the database.
|
|
66
|
+
|
|
67
|
+
:param fields: The fields to query. available fields: ["pay_date", "coupon", "coupon_type", "period_start", "period_end", "remaining_principle", "principle_repayment"]
|
|
68
|
+
:type fields: T_SeqT[str]
|
|
69
|
+
:param tickers: The tickers to query, None for all.
|
|
70
|
+
:type tickers: Opt_T_SeqT[str]
|
|
71
|
+
:param exchanges: The exchanges to query, None for all.
|
|
72
|
+
:type exchanges: Opt_T_SeqT[EXCHANGE_LITERALS]
|
|
73
|
+
"""
|
|
74
|
+
filter_fields = {}
|
|
75
|
+
if tickers is not None:
|
|
76
|
+
filter_fields["ticker"] = tickers
|
|
77
|
+
if exchanges is not None:
|
|
78
|
+
filter_fields["exchange"] = exchanges
|
|
79
|
+
|
|
80
|
+
df = self._manager.read_data("cb_coupon_schedule", query_fields=fields, filter_fields=filter_fields)
|
|
81
|
+
return df.set_index(["ticker", "exchange"])
|
|
82
|
+
|
|
83
|
+
def update_bond_coupon(self, data):
|
|
84
|
+
"""
|
|
85
|
+
Updates the convert coupon metadata in the database.
|
|
86
|
+
|
|
87
|
+
:param data: The data to update.
|
|
88
|
+
:type data: pd.DataFrame
|
|
89
|
+
"""
|
|
90
|
+
self._manager.insert("cb_coupon_schedule", data, upsert=True)
|
|
91
|
+
|
|
92
|
+
def update_convert_bond_cashflow(self, data):
|
|
93
|
+
"""
|
|
94
|
+
Updates the convert bond cashflow metadata in the database.
|
|
95
|
+
|
|
96
|
+
:param data: The data to update.
|
|
97
|
+
:type data: pd.DataFrame
|
|
98
|
+
"""
|
|
99
|
+
self._manager.insert("cb_realized_cash_flow", data, upsert=True)
|
|
100
|
+
|
|
101
|
+
def update_auxiliary(self, auxiliary_type: str, data: pd.DataFrame):
|
|
102
|
+
"""
|
|
103
|
+
Updates the auxiliary data in the database.
|
|
104
|
+
|
|
105
|
+
:param data: The data to update.
|
|
106
|
+
:type data: pd.DataFrame
|
|
107
|
+
"""
|
|
108
|
+
self._manager.insert("cb_auxiliary_" + auxiliary_type, data, upsert=True)
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
from typing import Dict, Literal, Optional, Sequence, TypeVar, Union
|
|
8
8
|
|
|
9
|
-
INST_TYPE_LITERALS = Literal["STK", "FUT", "OPT", "IDX", "ETF", "
|
|
9
|
+
INST_TYPE_LITERALS = Literal["STK", "FUT", "OPT", "IDX", "ETF", "LOF", "FUND", "BOND", "CASH", "CRYPTO", "CB"]
|
|
10
10
|
EXCHANGE_LITERALS = Literal[
|
|
11
11
|
"SSE",
|
|
12
12
|
"SZSE",
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: trade_database_manager
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.6
|
|
4
4
|
Summary: A wrapper of kdb and sql for convenient trade data management.
|
|
5
|
-
Author-email: "Y.Q.
|
|
5
|
+
Author-email: "Y.Q. Tsui" <qianyun210603@hotmail.com>
|
|
6
6
|
Classifier: Operating System :: POSIX :: Linux
|
|
7
7
|
Classifier: Operating System :: Microsoft :: Windows
|
|
8
8
|
Classifier: Development Status :: 3 - Alpha
|
|
@@ -12,6 +12,7 @@ Classifier: Programming Language :: Python :: 3
|
|
|
12
12
|
Classifier: Programming Language :: Python :: 3.9
|
|
13
13
|
Classifier: Programming Language :: Python :: 3.10
|
|
14
14
|
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
16
|
Description-Content-Type: text/markdown
|
|
16
17
|
License-File: LICENSE
|
|
17
18
|
Requires-Dist: pandas>=2.0.0
|
|
@@ -75,3 +76,23 @@ then the system will send an email with the license file and a base64 key (Eithe
|
|
|
75
76
|
- `-T 1000`: This sets the timeout in seconds for client queries. In this case, it's set to 1000 seconds.
|
|
76
77
|
|
|
77
78
|
- `-U /opt/l64/trade.q`: This sets the access control list file. In this case, the file is located at `/opt/l64/trade.q`. This file contains a list of usernames and passwords for clients that are allowed to connect to the kdb+ process.
|
|
79
|
+
|
|
80
|
+
### MetaData Initialization
|
|
81
|
+
|
|
82
|
+
Allowed instruments types are given in "Instrument Types" section of [meta_enumerations.md](doc/meta_enumerations.md).
|
|
83
|
+
|
|
84
|
+
### Data Initialization for Instrument Type(s)
|
|
85
|
+
```python
|
|
86
|
+
from trade_database_manager.manager import MetadataSql
|
|
87
|
+
|
|
88
|
+
metadatalib = MetadataSql()
|
|
89
|
+
metadatalib.initialize(for_inst_types="CB")
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
This will try to create two tables, `instruments` and `instruments_cb` in the database if not yet exists. The `instruments` table will store the common information of all instruments, and the `instruments_cb` table will store the type-specific information of the instruments of type `CB`.
|
|
93
|
+
|
|
94
|
+
The table fields are listed in the [data_organization.md](doc/data_organization.md) file.
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
|
|
@@ -16,7 +16,9 @@ trade_database_manager/core/sql/__init__.py
|
|
|
16
16
|
trade_database_manager/core/sql/sqlmanager.py
|
|
17
17
|
trade_database_manager/core/sql/sqlreader.py
|
|
18
18
|
trade_database_manager/core/sql/sqlwriter.py
|
|
19
|
+
trade_database_manager/core/sql/utils.py
|
|
19
20
|
trade_database_manager/manager/__init__.py
|
|
20
21
|
trade_database_manager/manager/fields_data_type.py
|
|
21
22
|
trade_database_manager/manager/metadata_sql.py
|
|
23
|
+
trade_database_manager/manager/metadata_sql_cb.py
|
|
22
24
|
trade_database_manager/manager/typedefs.py
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
# -*- coding: utf-8 -*-
|
|
2
|
-
# @Time : 2024/4/19 16:44
|
|
3
|
-
# @Author : YQ Tsui
|
|
4
|
-
# @File : fields_data_type.py
|
|
5
|
-
# @Purpose :
|
|
6
|
-
|
|
7
|
-
FIELD_DATA_TYPE_SQL = {
|
|
8
|
-
"ticker": "VARCHAR(20)",
|
|
9
|
-
"name": "VARCHAR(20)",
|
|
10
|
-
"currency": "VARCHAR(6)",
|
|
11
|
-
"exchange": "VARCHAR(10)",
|
|
12
|
-
"timezone": "VARCHAR(30)",
|
|
13
|
-
"tick_size": "REAL",
|
|
14
|
-
"lot_size": "REAL",
|
|
15
|
-
"min_lots": "REAL",
|
|
16
|
-
"market_tplus": "INTEGER",
|
|
17
|
-
"listed_date": "DATE",
|
|
18
|
-
"delisted_date": "DATE",
|
|
19
|
-
"sector": "VARCHAR(30)",
|
|
20
|
-
"industry": "VARCHAR(36)",
|
|
21
|
-
"country": "VARCHAR(36)",
|
|
22
|
-
"board_type": "VARCHAR(20)",
|
|
23
|
-
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{trade_database_manager-0.0.3.dev1 → trade_database_manager-0.0.6}/trade_database_manager/config.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|