trade-database-manager 0.0.1.dev2__tar.gz → 0.0.3__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.1.dev2/trade_database_manager.egg-info → trade_database_manager-0.0.3}/PKG-INFO +1 -1
- {trade_database_manager-0.0.1.dev2 → trade_database_manager-0.0.3}/pyproject.toml +5 -1
- {trade_database_manager-0.0.1.dev2/trade_database_manager/manager → trade_database_manager-0.0.3/trade_database_manager/core/kdb}/__init__.py +2 -2
- trade_database_manager-0.0.3/trade_database_manager/core/kdb/kdbmanager.py +150 -0
- trade_database_manager-0.0.3/trade_database_manager/core/sql/__init__.py +5 -0
- {trade_database_manager-0.0.1.dev2 → trade_database_manager-0.0.3}/trade_database_manager/core/sql/sqlmanager.py +139 -13
- trade_database_manager-0.0.3/trade_database_manager/manager/__init__.py +9 -0
- {trade_database_manager-0.0.1.dev2 → trade_database_manager-0.0.3}/trade_database_manager/manager/metadata_sql.py +82 -19
- {trade_database_manager-0.0.1.dev2 → trade_database_manager-0.0.3/trade_database_manager.egg-info}/PKG-INFO +1 -1
- {trade_database_manager-0.0.1.dev2 → trade_database_manager-0.0.3}/trade_database_manager.egg-info/SOURCES.txt +1 -1
- trade_database_manager-0.0.1.dev2/setup.py +0 -3
- trade_database_manager-0.0.1.dev2/trade_database_manager/core/kdb/__init__.py +0 -0
- trade_database_manager-0.0.1.dev2/trade_database_manager/core/sql/__init__.py +0 -0
- {trade_database_manager-0.0.1.dev2 → trade_database_manager-0.0.3}/LICENSE +0 -0
- {trade_database_manager-0.0.1.dev2 → trade_database_manager-0.0.3}/README.md +0 -0
- {trade_database_manager-0.0.1.dev2 → trade_database_manager-0.0.3}/setup.cfg +0 -0
- {trade_database_manager-0.0.1.dev2 → trade_database_manager-0.0.3}/trade_database_manager/__init__.py +0 -0
- {trade_database_manager-0.0.1.dev2 → trade_database_manager-0.0.3}/trade_database_manager/config.py +0 -0
- {trade_database_manager-0.0.1.dev2 → trade_database_manager-0.0.3}/trade_database_manager/core/__init__.py +0 -0
- {trade_database_manager-0.0.1.dev2 → trade_database_manager-0.0.3}/trade_database_manager/core/sql/sqlreader.py +0 -0
- {trade_database_manager-0.0.1.dev2 → trade_database_manager-0.0.3}/trade_database_manager/core/sql/sqlwriter.py +0 -0
- {trade_database_manager-0.0.1.dev2 → trade_database_manager-0.0.3}/trade_database_manager/core/typedefs.py +0 -0
- {trade_database_manager-0.0.1.dev2 → trade_database_manager-0.0.3}/trade_database_manager/manager/fields_data_type.py +0 -0
- {trade_database_manager-0.0.1.dev2 → trade_database_manager-0.0.3}/trade_database_manager/manager/typedefs.py +0 -0
- {trade_database_manager-0.0.1.dev2 → trade_database_manager-0.0.3}/trade_database_manager.egg-info/dependency_links.txt +0 -0
- {trade_database_manager-0.0.1.dev2 → trade_database_manager-0.0.3}/trade_database_manager.egg-info/requires.txt +0 -0
- {trade_database_manager-0.0.1.dev2 → trade_database_manager-0.0.3}/trade_database_manager.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: trade_database_manager
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.3
|
|
4
4
|
Summary: A wrapper of kdb and sql for convenient trade data management.
|
|
5
5
|
Author-email: "Y.Q. Cui" <qianyun210603@hotmail.com>
|
|
6
6
|
Classifier: Operating System :: POSIX :: Linux
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "trade_database_manager"
|
|
7
|
-
version = "0.0.
|
|
7
|
+
version = "0.0.3"
|
|
8
8
|
description = "A wrapper of kdb and sql for convenient trade data management."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
authors = [
|
|
@@ -43,3 +43,7 @@ target-version = ['py310']
|
|
|
43
43
|
[tool.isort]
|
|
44
44
|
profile = "black"
|
|
45
45
|
line_length = 120
|
|
46
|
+
|
|
47
|
+
[tool.flake8]
|
|
48
|
+
max-line-length = 120
|
|
49
|
+
ignore = "E203,E501,W503"
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# @Time : 2024/4/27 12:26
|
|
3
|
+
# @Author : YQ Tsui
|
|
4
|
+
# @File : kdbmanager.py
|
|
5
|
+
# @Purpose :
|
|
6
|
+
import os.path
|
|
7
|
+
import pandas as pd
|
|
8
|
+
import pykx
|
|
9
|
+
from ...config import CONFIG
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class KdbManager:
|
|
13
|
+
|
|
14
|
+
_instance = None
|
|
15
|
+
|
|
16
|
+
@classmethod
|
|
17
|
+
def instance(cls):
|
|
18
|
+
if cls._instance is None:
|
|
19
|
+
cls._instance = cls()
|
|
20
|
+
cls._instance.__init__()
|
|
21
|
+
return cls._instance
|
|
22
|
+
|
|
23
|
+
def __init__(self):
|
|
24
|
+
self.host = CONFIG["kdbhost"]
|
|
25
|
+
self.port = CONFIG["kdbport"]
|
|
26
|
+
# self.username = CONFIG["username"]
|
|
27
|
+
# self.password = CONFIG["password"]
|
|
28
|
+
self.username = ""
|
|
29
|
+
self.password = ""
|
|
30
|
+
# print(self.host, self.port, self.username, self.password)
|
|
31
|
+
|
|
32
|
+
def path_exists(self, path: str):
|
|
33
|
+
"""
|
|
34
|
+
Checks if a path exists in the kdb database.
|
|
35
|
+
|
|
36
|
+
:param path: The path to check.
|
|
37
|
+
:type path: str
|
|
38
|
+
"""
|
|
39
|
+
with pykx.QConnection(self.host, self.port, username=self.username, password=self.password) as conn:
|
|
40
|
+
return bool(conn(f".path.exists `:{path}"))
|
|
41
|
+
|
|
42
|
+
def create_folder(self, path: str):
|
|
43
|
+
"""
|
|
44
|
+
Creates a folder in the kdb database.
|
|
45
|
+
|
|
46
|
+
:param path: The path of the folder.
|
|
47
|
+
:type path: str
|
|
48
|
+
"""
|
|
49
|
+
with pykx.QConnection(self.host, self.port, username=self.username, password=self.password) as conn:
|
|
50
|
+
conn(f".path.mkdir \"{path}\"")
|
|
51
|
+
|
|
52
|
+
def write(self, table_name: str, data: pd.DataFrame, path: str = "", splayed: bool = False):
|
|
53
|
+
"""
|
|
54
|
+
Writes data to the kdb database.
|
|
55
|
+
|
|
56
|
+
:param table_name: The table name in the kdb database.
|
|
57
|
+
:type table_name: str
|
|
58
|
+
:param data: The data to be written.
|
|
59
|
+
:type data: pd.DataFrame
|
|
60
|
+
:param path: The path of data
|
|
61
|
+
:type path: str
|
|
62
|
+
:param splayed: Whether the table is splayed.
|
|
63
|
+
:type splayed: bool
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
real_path = os.path.join(path, table_name) if path else table_name
|
|
67
|
+
if splayed:
|
|
68
|
+
real_path += "/"
|
|
69
|
+
with pykx.QConnection(self.host, self.port, username=self.username, password=self.password) as conn:
|
|
70
|
+
conn(f"{{`:{real_path}/ set x}}", data)
|
|
71
|
+
|
|
72
|
+
def write_partitioned(self, table_name: str, data: pd.DataFrame, path: str = "", partition_func=None, key_column=None):
|
|
73
|
+
"""
|
|
74
|
+
Writes data to the kdb database with partitioning.
|
|
75
|
+
|
|
76
|
+
:param table_name: The table name in the kdb database.
|
|
77
|
+
:type table_name: str
|
|
78
|
+
:param data: The data to be written.
|
|
79
|
+
:type data: pd.DataFrame
|
|
80
|
+
:param path: The path of data
|
|
81
|
+
:type path: str
|
|
82
|
+
:param partition_func: calculate the partition bucket from datetime column
|
|
83
|
+
:type partition_func: callable
|
|
84
|
+
:param key_column: Column which is sorted to server as key.
|
|
85
|
+
:type key_column: str
|
|
86
|
+
"""
|
|
87
|
+
if isinstance(data.index, pd.DatetimeIndex):
|
|
88
|
+
data.reset_index(inplace=True, names=["datetime"])
|
|
89
|
+
assert "datetime" in data.columns, "datetime column not found"
|
|
90
|
+
data.sort_values(by="datetime", inplace=True)
|
|
91
|
+
if key_column is not None and key_column in data.columns:
|
|
92
|
+
with pykx.QConnection(host=self.host, port=self.port, username=self.username, password=self.password) as conn:
|
|
93
|
+
for bucket, df in data.groupby(partition_func(data["datetime"])):
|
|
94
|
+
conn(f'{{`{table_name} set x; .partable.createOrAppend[`:{path};{bucket};`{key_column};`{table_name}]}}', df.reset_index(drop=True))
|
|
95
|
+
else:
|
|
96
|
+
with pykx.QConnection(host=self.host, port=self.port, username=self.username, password=self.password) as conn:
|
|
97
|
+
for bucket, df in data.groupby(by=partition_func(data["datetime"])):
|
|
98
|
+
conn(f'{{`{table_name} set x;.Q.dpt[`:{path};{bucket};`{table_name}]}}', df.reset_index(drop=True))
|
|
99
|
+
|
|
100
|
+
def read_partitioned(self, table_name: str, path: str = "", fields=None, start_time=None, end_time=None, partition_func=None,
|
|
101
|
+
other_conditions=None):
|
|
102
|
+
"""
|
|
103
|
+
Reads data from the kdb database with partitioning.
|
|
104
|
+
|
|
105
|
+
:param table_name: The table name in the kdb database.
|
|
106
|
+
:type table_name: str
|
|
107
|
+
:param path: The path of data
|
|
108
|
+
:type path: str
|
|
109
|
+
:param start_time: The start time of the data.
|
|
110
|
+
:type start_time: pd.Timestamp
|
|
111
|
+
:param end_time: The end time of the data.
|
|
112
|
+
:type end_time: pd.Timestamp
|
|
113
|
+
:param partition_func: calculate the partition bucket from datetime column
|
|
114
|
+
:type partition_func: callable
|
|
115
|
+
:param other_conditions: Other conditions for the query.
|
|
116
|
+
:type other_conditions: str
|
|
117
|
+
"""
|
|
118
|
+
time_format = "%Y.%m.%dD%H:%M:%S.%f"
|
|
119
|
+
start_time_str = start_time.strftime(time_format) if start_time is not None else None
|
|
120
|
+
end_time_str = end_time.strftime(time_format) if end_time is not None else None
|
|
121
|
+
if isinstance(fields, (bytes, str)):
|
|
122
|
+
fields = [fields]
|
|
123
|
+
select_clause = f"select from {table_name}" if fields is None else f"select {','.join(fields)} from {table_name}"
|
|
124
|
+
where_cond = ""
|
|
125
|
+
if start_time_str is not None and end_time_str is not None:
|
|
126
|
+
where_cond += f"datetime within ({start_time_str};{end_time_str})"
|
|
127
|
+
if partition_func is not None:
|
|
128
|
+
where_cond = f"int in {' '.join(str(x) for x in range(partition_func(start_time), partition_func(end_time) + 1))}" + ',' + where_cond
|
|
129
|
+
elif start_time_str is not None:
|
|
130
|
+
where_cond += f"datetime>={start_time_str}"
|
|
131
|
+
if partition_func is not None:
|
|
132
|
+
where_cond = f"int>={partition_func(start_time)}" + ',' + where_cond
|
|
133
|
+
elif end_time_str is not None:
|
|
134
|
+
where_cond += f"datetime<={end_time_str}"
|
|
135
|
+
if partition_func is not None:
|
|
136
|
+
where_cond = f"int<= {partition_func(end_time)}" + ',' + where_cond
|
|
137
|
+
if other_conditions is not None:
|
|
138
|
+
where_cond = other_conditions if where_cond == "" else where_cond + ',' + other_conditions
|
|
139
|
+
if where_cond:
|
|
140
|
+
where_cond = " where " + where_cond
|
|
141
|
+
final_query = select_clause + where_cond
|
|
142
|
+
|
|
143
|
+
with pykx.QConnection(host=self.host, port=self.port, username=self.username, password=self.password) as conn:
|
|
144
|
+
conn("`currpath__ set .path.pwd[]") # save current path to currpath__ as following command will change the path
|
|
145
|
+
try:
|
|
146
|
+
conn(f"\\l {path}") # load the path
|
|
147
|
+
q_table = conn(final_query)
|
|
148
|
+
return q_table.pd().set_index("datetime")
|
|
149
|
+
finally:
|
|
150
|
+
conn('system "cd ", currpath__')
|
|
@@ -1,10 +1,16 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# @Time : 2024/4/15 20:28
|
|
3
|
+
# @Author : YQ Tsui
|
|
4
|
+
# @File : sqlmanager.py
|
|
5
|
+
# @Purpose : Core SQL operations
|
|
6
|
+
|
|
1
7
|
import re
|
|
2
8
|
from collections.abc import Container
|
|
3
9
|
from functools import partial, reduce
|
|
4
10
|
from typing import Any, Literal, Sequence, Type, Union
|
|
5
11
|
|
|
6
12
|
import pandas as pd
|
|
7
|
-
from sqlalchemy import
|
|
13
|
+
from sqlalchemy import DOUBLE_PRECISION, Index, Integer, MetaData, String, Table, create_engine, inspect, select, sql, text
|
|
8
14
|
from sqlalchemy.dialects.postgresql import insert
|
|
9
15
|
|
|
10
16
|
from ...config import CONFIG
|
|
@@ -27,6 +33,12 @@ def _insert_on_conflict_nothing(table, conn, keys, data_iter):
|
|
|
27
33
|
|
|
28
34
|
|
|
29
35
|
class SqlManager:
|
|
36
|
+
"""
|
|
37
|
+
This class is used to manage SQL operations.
|
|
38
|
+
|
|
39
|
+
:ivar sqlalchemy.engine.Engine engine: An instance of the SQLAlchemy Engine class for executing SQL operations.
|
|
40
|
+
"""
|
|
41
|
+
|
|
30
42
|
def __init__(self):
|
|
31
43
|
self.engine = create_engine(CONFIG["sqlconnstr"])
|
|
32
44
|
|
|
@@ -37,12 +49,20 @@ class SqlManager:
|
|
|
37
49
|
return conn.execute(sql_executable)
|
|
38
50
|
|
|
39
51
|
def add_index(self, table_name: str, columns: Union[str, list[str]], unique: bool = True):
|
|
52
|
+
"""
|
|
53
|
+
Adds an index to a table.
|
|
54
|
+
|
|
55
|
+
:param table_name: The name of the table to add the index to.
|
|
56
|
+
:type table_name: str
|
|
57
|
+
:param columns: The column(s) to include in the index. It can be a single column name or a list of column names.
|
|
58
|
+
:type columns: Union[str, list[str]]
|
|
59
|
+
:param unique: Whether the index should enforce unique values. Defaults to True.
|
|
60
|
+
:type unique: bool
|
|
61
|
+
"""
|
|
40
62
|
if isinstance(columns, str):
|
|
41
63
|
columns = [columns]
|
|
42
64
|
|
|
43
65
|
index_name = f"uix_{table_name}_{'_'.join(columns)}"
|
|
44
|
-
columns_str = ", ".join(columns)
|
|
45
|
-
unique_str = "UNIQUE" if unique else ""
|
|
46
66
|
table_meta = MetaData()
|
|
47
67
|
table = Table(table_name, table_meta, autoload_with=self.engine)
|
|
48
68
|
columns = [getattr(table.c, colname) for colname in columns if colname in table.c.keys()]
|
|
@@ -57,6 +77,22 @@ class SqlManager:
|
|
|
57
77
|
other_unique_index_columns: Sequence[str] = (),
|
|
58
78
|
other_non_unique_index_columns: Sequence[str] = (),
|
|
59
79
|
):
|
|
80
|
+
"""
|
|
81
|
+
Inserts data into a table.
|
|
82
|
+
|
|
83
|
+
:param table_name: The name of the table to insert data into.
|
|
84
|
+
:type table_name: str
|
|
85
|
+
:param df: The data to insert. It should be a DataFrame where the column names match the table columns.
|
|
86
|
+
:type df: pd.DataFrame
|
|
87
|
+
:param upsert: Whether to update the table if the data already exists. Defaults to True.
|
|
88
|
+
:type upsert: bool
|
|
89
|
+
:param other_unique_index_columns: Other columns to enforce unique values on. Defaults to an empty sequence.
|
|
90
|
+
:type other_unique_index_columns: Sequence[str]
|
|
91
|
+
:param other_non_unique_index_columns: Other columns to add non-unique indexes to. Defaults to an empty sequence.
|
|
92
|
+
:type other_non_unique_index_columns: Sequence[str]
|
|
93
|
+
:return: The number of rows inserted.
|
|
94
|
+
:rtype: int
|
|
95
|
+
"""
|
|
60
96
|
if_exists: Literal["replace", "append"] = "append"
|
|
61
97
|
inspector = inspect(self.engine)
|
|
62
98
|
new_table = not inspector.has_table(table_name)
|
|
@@ -77,26 +113,54 @@ class SqlManager:
|
|
|
77
113
|
self.add_index(table_name, column, unique=False)
|
|
78
114
|
return num_rows
|
|
79
115
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
116
|
+
@staticmethod
|
|
117
|
+
def _convert_to_sqlalchemy_type(column_type: Type, **kwargs):
|
|
118
|
+
if isinstance(column_type, type):
|
|
119
|
+
column_type = column_type.__name__
|
|
120
|
+
if column_type == "str":
|
|
84
121
|
return String(**kwargs)
|
|
85
|
-
if column_type == int:
|
|
122
|
+
if column_type == "int":
|
|
86
123
|
return Integer()
|
|
87
|
-
if column_type == float:
|
|
88
|
-
return
|
|
124
|
+
if column_type == "float":
|
|
125
|
+
return DOUBLE_PRECISION()
|
|
89
126
|
raise ValueError(f"Unsupported column type {column_type}")
|
|
90
127
|
|
|
91
|
-
def insert_column(self, table_name: str, column_name: str, column_type:
|
|
92
|
-
|
|
93
|
-
|
|
128
|
+
def insert_column(self, table_name: str, column_name: str, column_type: str):
|
|
129
|
+
"""
|
|
130
|
+
Inserts a new column into a table.
|
|
131
|
+
|
|
132
|
+
:param table_name: The name of the table to insert the column into.
|
|
133
|
+
:type table_name: str
|
|
134
|
+
:param column_name: The name of the new column.
|
|
135
|
+
:type column_name: str
|
|
136
|
+
:param column_type: The data type of the new column. It is a string representation of the data type in PostgreSQL.
|
|
137
|
+
:type column_type: str
|
|
138
|
+
"""
|
|
139
|
+
sql_code = f"ALTER TABLE {table_name} ADD COLUMN {column_name} {column_type}"
|
|
94
140
|
self._execute(sql_code)
|
|
95
141
|
|
|
96
142
|
def delete_column(self, table_name: str, column_name: str):
|
|
143
|
+
"""
|
|
144
|
+
Deletes a column from a table.
|
|
145
|
+
|
|
146
|
+
:param table_name: The name of the table to delete the column from.
|
|
147
|
+
:type table_name: str
|
|
148
|
+
:param column_name: The name of the column to delete.
|
|
149
|
+
:type column_name: str
|
|
150
|
+
"""
|
|
97
151
|
self._execute(f"ALTER TABLE {table_name} DROP COLUMN {column_name}")
|
|
98
152
|
|
|
99
153
|
def rename_column(self, table_name: str, old_column_name: str, new_column_name: str):
|
|
154
|
+
"""
|
|
155
|
+
Renames a column in a table.
|
|
156
|
+
|
|
157
|
+
:param table_name: The name of the table containing the column to rename.
|
|
158
|
+
:type table_name: str
|
|
159
|
+
:param old_column_name: The current name of the column.
|
|
160
|
+
:type old_column_name: str
|
|
161
|
+
:param new_column_name: The new name for the column.
|
|
162
|
+
:type new_column_name: str
|
|
163
|
+
"""
|
|
100
164
|
# check if any index is referring to the column
|
|
101
165
|
sql_code = f"SELECT indexname, indexdef FROM pg_indexes WHERE indexdef LIKE '%%(%%{old_column_name}%%)%%' and tablename = '{table_name}'"
|
|
102
166
|
index_refering_column = dict(self.engine.execute(sql_code).fetchall())
|
|
@@ -154,6 +218,19 @@ class SqlManager:
|
|
|
154
218
|
return pd.DataFrame(res.fetchall(), columns=res.keys())
|
|
155
219
|
|
|
156
220
|
def read_data(self, table_name: str, query_fields: QUERYFIELD_TYPE = "*", filter_fields=None):
|
|
221
|
+
"""
|
|
222
|
+
Reads data from a table.
|
|
223
|
+
|
|
224
|
+
:param table_name: The name of the table to read data from.
|
|
225
|
+
:type table_name: str
|
|
226
|
+
:param query_fields: The fields to query. By default, it queries all fields. Defaults to "*".
|
|
227
|
+
:type query_fields: QUERYFIELD_TYPE, optional
|
|
228
|
+
:param filter_fields: Additional fields to filter by. The keys are the field names and the values are the filter values. Defaults to None.
|
|
229
|
+
:type filter_fields: dict, optional
|
|
230
|
+
:return: A DataFrame containing the queried data.
|
|
231
|
+
:rtype: pd.DataFrame
|
|
232
|
+
"""
|
|
233
|
+
|
|
157
234
|
meta = MetaData()
|
|
158
235
|
table = Table(table_name, meta, autoload_with=self.engine)
|
|
159
236
|
|
|
@@ -186,6 +263,20 @@ class SqlManager:
|
|
|
186
263
|
query_fields: QUERYFIELD_TYPE = "*",
|
|
187
264
|
filter_fields: FILTERFIELD_TYPE = None,
|
|
188
265
|
):
|
|
266
|
+
"""
|
|
267
|
+
Reads data from multiple tables.
|
|
268
|
+
|
|
269
|
+
:param table_names: The names of the tables to read data from.
|
|
270
|
+
:type table_names: Sequence[str]
|
|
271
|
+
:param joined_columns: The columns to join the tables on.
|
|
272
|
+
:type joined_columns: Sequence[str]
|
|
273
|
+
:param query_fields: The fields to query. By default, it queries all fields. Defaults to "*".
|
|
274
|
+
:type query_fields: QUERYFIELD_TYPE, optional
|
|
275
|
+
:param filter_fields: Additional fields to filter by. The keys are the field names and the values are the filter values. Defaults to None.
|
|
276
|
+
:type filter_fields: FILTERFIELD_TYPE, optional
|
|
277
|
+
:return: A DataFrame containing the queried data.
|
|
278
|
+
:rtype: pd.DataFrame
|
|
279
|
+
"""
|
|
189
280
|
meta = MetaData()
|
|
190
281
|
tables = {table_name: Table(table_name, meta, autoload_with=self.engine) for table_name in table_names}
|
|
191
282
|
|
|
@@ -222,3 +313,38 @@ class SqlManager:
|
|
|
222
313
|
# cannot use pandas.read_sql here as it discards timezone info
|
|
223
314
|
res = self._execute(stmt)
|
|
224
315
|
return pd.DataFrame(res.fetchall(), columns=res.keys())
|
|
316
|
+
|
|
317
|
+
def create_ts_table(self, table_name: str, time_column: str, table_columns: dict[str, str],
|
|
318
|
+
time_chunk_interval: str = "1 year", other_chunkers: dict[str, int] = None):
|
|
319
|
+
"""
|
|
320
|
+
Creates a table with a timestamp column.
|
|
321
|
+
|
|
322
|
+
:param table_name: The name of the table to create.
|
|
323
|
+
:type table_name: str
|
|
324
|
+
:param time_column: The name of the timestamp column.
|
|
325
|
+
:type time_column: str
|
|
326
|
+
: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 in PostgreSQL.
|
|
327
|
+
:type table_columns: Dict[str, str]
|
|
328
|
+
:param time_chunk_interval: The interval to chunk the time column by. Defaults to "1 year".
|
|
329
|
+
:type time_chunk_interval: str
|
|
330
|
+
:param other_chunkers: Other columns to chunk by. The keys are the column names and the values are the num of hashes. Defaults to None.
|
|
331
|
+
:type other_chunkers: Dict[str, int], optional
|
|
332
|
+
"""
|
|
333
|
+
query_create_sensordata_table = f"""
|
|
334
|
+
CREATE TABLE {table_name} (
|
|
335
|
+
{time_column} TIMESTAMP NOT NULL,
|
|
336
|
+
{", ".join([f"{column_name} {column_type}{' NOT NULL' if column_name in other_chunkers else ''}" for column_name, column_type in table_columns.items()])}
|
|
337
|
+
);
|
|
338
|
+
"""
|
|
339
|
+
query_create_sensordata_hypertable = f"SELECT create_hypertable('{table_name}', by_range('{time_column}', INTERVAL '{time_chunk_interval}'));"
|
|
340
|
+
if other_chunkers is not None:
|
|
341
|
+
for column_name, num_hash in other_chunkers.items():
|
|
342
|
+
query_create_sensordata_hypertable += f"\nSELECT add_dimension('{table_name}', by_hash('{column_name}', {num_hash}));"
|
|
343
|
+
|
|
344
|
+
uidx_name = f"uix_{table_name}_{'_'.join(other_chunkers.keys())}_{time_column}"
|
|
345
|
+
query_unique_index = f"CREATE UNIQUE INDEX {uidx_name} ON {table_name} ({', '.join(other_chunkers.keys())}, {time_column});"
|
|
346
|
+
with self.engine.connect() as conn:
|
|
347
|
+
conn.execute(text(query_create_sensordata_table))
|
|
348
|
+
conn.execute(text(query_create_sensordata_hypertable))
|
|
349
|
+
conn.execute(text(query_unique_index))
|
|
350
|
+
conn.commit()
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
# @Purpose : Instrument metadata stored in SQL database
|
|
6
6
|
|
|
7
7
|
from collections.abc import Container
|
|
8
|
-
from typing import Union
|
|
8
|
+
from typing import Union, cast
|
|
9
9
|
|
|
10
10
|
import pandas as pd
|
|
11
11
|
|
|
@@ -14,6 +14,7 @@ from .typedefs import EXCHANGE_LITERALS, INST_TYPE_LITERALS, Opt_T_SeqT, T_DictT
|
|
|
14
14
|
|
|
15
15
|
COMMON_METADATA_COLUMNS = [
|
|
16
16
|
"name",
|
|
17
|
+
"trading_code",
|
|
17
18
|
"inst_type",
|
|
18
19
|
"currency",
|
|
19
20
|
"timezone",
|
|
@@ -25,15 +26,33 @@ COMMON_METADATA_COLUMNS = [
|
|
|
25
26
|
"delisted_date",
|
|
26
27
|
]
|
|
27
28
|
TYPE_METADATA_COLUMNS = {
|
|
28
|
-
"STK": ["sector", "industry", "country", "state", "board_type"],
|
|
29
|
+
"STK": ["sector", "industry", "country", "state", "board_type", "issue_price"],
|
|
29
30
|
}
|
|
30
31
|
|
|
31
32
|
|
|
32
33
|
class MetadataSql:
|
|
33
|
-
|
|
34
|
-
|
|
34
|
+
"""
|
|
35
|
+
This class is used to manage instrument metadata stored in an SQL database.
|
|
36
|
+
|
|
37
|
+
This is a singleton class. Just call MetadataSql() to get the instance.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
_instance = None
|
|
41
|
+
_manager = None
|
|
42
|
+
|
|
43
|
+
def __new__(cls):
|
|
44
|
+
if not isinstance(cls._instance, cls):
|
|
45
|
+
cls._instance = super(MetadataSql, cls).__new__(cls)
|
|
46
|
+
cls._manager = SqlManager()
|
|
47
|
+
return cls._instance
|
|
35
48
|
|
|
36
49
|
def update_instrument_metadata(self, data: Union[pd.DataFrame, list[dict], dict]):
|
|
50
|
+
"""
|
|
51
|
+
Updates the instrument metadata in the database.
|
|
52
|
+
|
|
53
|
+
:param data: The data to be updated. It can be a DataFrame, a list of dictionaries, or a single dictionary.
|
|
54
|
+
:type data: Union[pd.DataFrame, list[dict], dict]
|
|
55
|
+
"""
|
|
37
56
|
type_specific_columns = set(data.columns) - set(COMMON_METADATA_COLUMNS)
|
|
38
57
|
if "inst_type" not in data.columns and bool(type_specific_columns):
|
|
39
58
|
raise ValueError(
|
|
@@ -43,15 +62,23 @@ class MetadataSql:
|
|
|
43
62
|
data = [data]
|
|
44
63
|
if isinstance(data, list):
|
|
45
64
|
data = pd.DataFrame(data)
|
|
46
|
-
|
|
65
|
+
if "ticker" in data.columns and "exchange" in data.columns:
|
|
66
|
+
data.set_index(["ticker", "exchange"], inplace=True)
|
|
67
|
+
else:
|
|
68
|
+
assert set(data.index.names) == {"ticker", "exchange"}, "Index names must be 'ticker' and 'exchange'."
|
|
47
69
|
data_common = data[data.columns.intersection(COMMON_METADATA_COLUMNS)]
|
|
48
70
|
|
|
49
|
-
self.
|
|
71
|
+
self._manager.insert("instruments", data_common, upsert=True)
|
|
50
72
|
if "inst_type" in data.columns:
|
|
51
73
|
for inst_type, columns in TYPE_METADATA_COLUMNS.items():
|
|
52
74
|
data_type_df = data.loc[data.inst_type == inst_type, data.columns.intersection(columns)]
|
|
53
75
|
if not data_type_df.empty:
|
|
54
|
-
self.
|
|
76
|
+
self._manager.insert(f"instruments_{inst_type.lower()}", data_type_df, upsert=True)
|
|
77
|
+
|
|
78
|
+
def _convert_datetime_columns(self, data: pd.DataFrame):
|
|
79
|
+
for col in ["listed_date", "delisted_date"]:
|
|
80
|
+
if col in data.columns:
|
|
81
|
+
data[col] = data[col].apply(pd.to_datetime)
|
|
55
82
|
|
|
56
83
|
def read_metadata(
|
|
57
84
|
self,
|
|
@@ -60,11 +87,25 @@ class MetadataSql:
|
|
|
60
87
|
query_fields="*",
|
|
61
88
|
filter_fields=None,
|
|
62
89
|
) -> T_DictT[pd.DataFrame]:
|
|
90
|
+
"""
|
|
91
|
+
Reads metadata from the database based on the provided filters.
|
|
92
|
+
|
|
93
|
+
:param ticker: The ticker(s) to filter by. It can be a single ticker or a sequence of tickers. Defaults to None.
|
|
94
|
+
:type ticker: Opt_T_SeqT[str], optional
|
|
95
|
+
:param exchange: The exchange(s) to filter by. It can be a single exchange or a sequence of exchanges. Defaults to None.
|
|
96
|
+
:type exchange: Opt_T_SeqT[EXCHANGE_LITERALS], optional
|
|
97
|
+
:param query_fields: The fields to query. By default, it queries all fields. Defaults to "*".
|
|
98
|
+
:type query_fields: str, optional
|
|
99
|
+
:param filter_fields: Additional fields to filter by. The keys are the field names and the values are the filter values. Defaults to None.
|
|
100
|
+
:type filter_fields: dict, optional
|
|
101
|
+
:return: A dictionary of DataFrames containing the queried metadata.
|
|
102
|
+
:rtype: T_DictT[pd.DataFrame]
|
|
103
|
+
"""
|
|
63
104
|
filter_fields = filter_fields or {}
|
|
64
105
|
if ticker is not None:
|
|
65
106
|
filter_fields["ticker"] = ticker
|
|
66
107
|
if exchange is not None:
|
|
67
|
-
if not isinstance(exchange, str) and isinstance(exchange, Container):
|
|
108
|
+
if not isinstance(exchange, str) and isinstance(exchange, Container) and ticker is not None:
|
|
68
109
|
assert len(exchange) == len(ticker), "Exchange must be a single value or the same length as ticker."
|
|
69
110
|
filter_fields["exchange"] = exchange
|
|
70
111
|
query_fields_common = (
|
|
@@ -82,13 +123,17 @@ class MetadataSql:
|
|
|
82
123
|
)
|
|
83
124
|
if not (query_fields == "*" or "inst_type" in query_fields):
|
|
84
125
|
query_fields_common.append("inst_type")
|
|
85
|
-
common_df = self.
|
|
126
|
+
common_df = self._manager.read_data(
|
|
127
|
+
"instruments", query_fields=query_fields_common, filter_fields=filter_fields
|
|
128
|
+
)
|
|
129
|
+
self._convert_datetime_columns(common_df)
|
|
86
130
|
if common_df.empty:
|
|
87
131
|
return {}
|
|
88
132
|
res = {}
|
|
89
133
|
for inst_type, common_df_by_type in common_df.groupby("inst_type"):
|
|
134
|
+
inst_type = cast(INST_TYPE_LITERALS, inst_type)
|
|
90
135
|
if all_fields_common:
|
|
91
|
-
res[inst_type] = common_df_by_type
|
|
136
|
+
res[inst_type] = common_df_by_type.set_index(["ticker", "exchange"]) if len(common_df_by_type.columns) > 2 else common_df_by_type
|
|
92
137
|
continue
|
|
93
138
|
query_fields_type = (
|
|
94
139
|
["ticker", "exchange"] + [f for f in query_fields if f in TYPE_METADATA_COLUMNS[inst_type]]
|
|
@@ -98,11 +143,11 @@ class MetadataSql:
|
|
|
98
143
|
filter_fields_type = {k: v for k, v in filter_fields.items() if k in TYPE_METADATA_COLUMNS[inst_type]}
|
|
99
144
|
filter_fields_type["ticker"] = common_df_by_type["ticker"].to_list()
|
|
100
145
|
filter_fields_type["exchange"] = common_df_by_type["exchange"].to_list()
|
|
101
|
-
type_df = self.
|
|
146
|
+
type_df = self._manager.read_data(
|
|
102
147
|
f"instruments_{inst_type.lower()}", query_fields=query_fields_type, filter_fields=filter_fields_type
|
|
103
148
|
)
|
|
104
149
|
type_df = common_df_by_type.merge(type_df, on=["ticker", "exchange"], how="inner")
|
|
105
|
-
res[inst_type] = type_df.set_index(["ticker", "exchange"])
|
|
150
|
+
res[inst_type] = type_df.set_index(["ticker", "exchange"]) if len(type_df.columns) > 2 else type_df
|
|
106
151
|
return res
|
|
107
152
|
|
|
108
153
|
def read_metadata_for_insttype(
|
|
@@ -113,12 +158,28 @@ class MetadataSql:
|
|
|
113
158
|
query_fields="*",
|
|
114
159
|
filter_fields=None,
|
|
115
160
|
) -> pd.DataFrame:
|
|
161
|
+
"""
|
|
162
|
+
Reads metadata for a specific instrument type from the database based on the provided filters.
|
|
163
|
+
|
|
164
|
+
:param inst_type: The instrument type to filter by.
|
|
165
|
+
:type inst_type: INST_TYPE_LITERALS
|
|
166
|
+
:param ticker: The ticker(s) to filter by. It can be a single ticker or a sequence of tickers. Defaults to None.
|
|
167
|
+
:type ticker: Opt_T_SeqT[str], optional
|
|
168
|
+
:param exchange: The exchange(s) to filter by. It can be a single exchange or a sequence of exchanges. Defaults to None.
|
|
169
|
+
:type exchange: Opt_T_SeqT[EXCHANGE_LITERALS], optional
|
|
170
|
+
:param query_fields: The fields to query. By default, it queries all fields. Defaults to "*".
|
|
171
|
+
:type query_fields: str, optional
|
|
172
|
+
:param filter_fields: Additional fields to filter by. The keys are the field names and the values are the filter values. Defaults to None.
|
|
173
|
+
:type filter_fields: dict, optional
|
|
174
|
+
:return: A DataFrame containing the queried metadata for the specified instrument type.
|
|
175
|
+
:rtype: pd.DataFrame
|
|
176
|
+
"""
|
|
116
177
|
filter_fields = filter_fields or {}
|
|
117
178
|
filter_fields["inst_type"] = inst_type
|
|
118
179
|
if ticker is not None:
|
|
119
180
|
filter_fields["ticker"] = ticker
|
|
120
181
|
if exchange is not None:
|
|
121
|
-
if not isinstance(exchange, str) and isinstance(exchange, Container):
|
|
182
|
+
if not isinstance(exchange, str) and isinstance(exchange, Container) and ticker is not None:
|
|
122
183
|
assert len(exchange) == len(ticker), "Exchange must be a single value or the same length as ticker."
|
|
123
184
|
filter_fields["exchange"] = exchange
|
|
124
185
|
|
|
@@ -126,9 +187,7 @@ class MetadataSql:
|
|
|
126
187
|
query_fields_cross = "*"
|
|
127
188
|
else:
|
|
128
189
|
query_fields_common = ["ticker", "exchange"] + [f for f in query_fields if f in COMMON_METADATA_COLUMNS]
|
|
129
|
-
query_fields_type = [
|
|
130
|
-
f for f in query_fields if f in TYPE_METADATA_COLUMNS[inst_type]
|
|
131
|
-
]
|
|
190
|
+
query_fields_type = [f for f in query_fields if f in TYPE_METADATA_COLUMNS[inst_type]]
|
|
132
191
|
query_fields_cross = {
|
|
133
192
|
"instruments": query_fields_common,
|
|
134
193
|
f"instruments_{inst_type.lower()}": query_fields_type,
|
|
@@ -144,12 +203,16 @@ class MetadataSql:
|
|
|
144
203
|
"instruments": filter_fields_common,
|
|
145
204
|
f"instruments_{inst_type.lower()}": filter_fields_type,
|
|
146
205
|
}
|
|
147
|
-
df = self.
|
|
206
|
+
df = self._manager.read_data_across_tables(
|
|
148
207
|
["instruments", f"instruments_{inst_type.lower()}"],
|
|
149
208
|
joined_columns=["ticker", "exchange"],
|
|
150
209
|
query_fields=query_fields_cross,
|
|
151
210
|
filter_fields=filter_fields_cross,
|
|
152
211
|
)
|
|
212
|
+
|
|
153
213
|
if isinstance(df.columns, pd.Index):
|
|
154
|
-
|
|
155
|
-
|
|
214
|
+
df = df.loc[:, ~df.columns.duplicated()]
|
|
215
|
+
if len(df.columns) > 2:
|
|
216
|
+
df.set_index(["ticker", "exchange"], inplace=True)
|
|
217
|
+
self._convert_datetime_columns(df)
|
|
218
|
+
return df
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: trade_database_manager
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.3
|
|
4
4
|
Summary: A wrapper of kdb and sql for convenient trade data management.
|
|
5
5
|
Author-email: "Y.Q. Cui" <qianyun210603@hotmail.com>
|
|
6
6
|
Classifier: Operating System :: POSIX :: Linux
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
LICENSE
|
|
2
2
|
README.md
|
|
3
3
|
pyproject.toml
|
|
4
|
-
setup.py
|
|
5
4
|
trade_database_manager/__init__.py
|
|
6
5
|
trade_database_manager/config.py
|
|
7
6
|
trade_database_manager.egg-info/PKG-INFO
|
|
@@ -12,6 +11,7 @@ trade_database_manager.egg-info/top_level.txt
|
|
|
12
11
|
trade_database_manager/core/__init__.py
|
|
13
12
|
trade_database_manager/core/typedefs.py
|
|
14
13
|
trade_database_manager/core/kdb/__init__.py
|
|
14
|
+
trade_database_manager/core/kdb/kdbmanager.py
|
|
15
15
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{trade_database_manager-0.0.1.dev2 → trade_database_manager-0.0.3}/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
|
|
File without changes
|
|
File without changes
|