trade-database-manager 0.0.1.dev1__tar.gz → 0.0.1.dev4__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.
Files changed (28) hide show
  1. {trade_database_manager-0.0.1.dev1/trade_database_manager.egg-info → trade_database_manager-0.0.1.dev4}/PKG-INFO +23 -2
  2. trade_database_manager-0.0.1.dev4/README.md +55 -0
  3. {trade_database_manager-0.0.1.dev1 → trade_database_manager-0.0.1.dev4}/pyproject.toml +6 -1
  4. {trade_database_manager-0.0.1.dev1 → trade_database_manager-0.0.1.dev4}/setup.py +0 -1
  5. trade_database_manager-0.0.1.dev4/trade_database_manager/core/sql/sqlmanager.py +312 -0
  6. trade_database_manager-0.0.1.dev4/trade_database_manager/core/typedefs.py +17 -0
  7. trade_database_manager-0.0.1.dev4/trade_database_manager/manager/__init__.py +9 -0
  8. trade_database_manager-0.0.1.dev4/trade_database_manager/manager/fields_data_type.py +23 -0
  9. trade_database_manager-0.0.1.dev4/trade_database_manager/manager/metadata_sql.py +220 -0
  10. trade_database_manager-0.0.1.dev4/trade_database_manager/manager/typedefs.py +48 -0
  11. {trade_database_manager-0.0.1.dev1 → trade_database_manager-0.0.1.dev4/trade_database_manager.egg-info}/PKG-INFO +23 -2
  12. {trade_database_manager-0.0.1.dev1 → trade_database_manager-0.0.1.dev4}/trade_database_manager.egg-info/SOURCES.txt +5 -2
  13. trade_database_manager-0.0.1.dev1/README.md +0 -34
  14. trade_database_manager-0.0.1.dev1/trade_database_manager/core/sql/sqlmanager.py +0 -102
  15. trade_database_manager-0.0.1.dev1/trade_database_manager/tools/__init__.py +0 -0
  16. trade_database_manager-0.0.1.dev1/trade_database_manager/tools/init_config.py +0 -42
  17. {trade_database_manager-0.0.1.dev1 → trade_database_manager-0.0.1.dev4}/LICENSE +0 -0
  18. {trade_database_manager-0.0.1.dev1 → trade_database_manager-0.0.1.dev4}/setup.cfg +0 -0
  19. {trade_database_manager-0.0.1.dev1 → trade_database_manager-0.0.1.dev4}/trade_database_manager/__init__.py +0 -0
  20. {trade_database_manager-0.0.1.dev1 → trade_database_manager-0.0.1.dev4}/trade_database_manager/config.py +0 -0
  21. {trade_database_manager-0.0.1.dev1 → trade_database_manager-0.0.1.dev4}/trade_database_manager/core/__init__.py +0 -0
  22. {trade_database_manager-0.0.1.dev1 → trade_database_manager-0.0.1.dev4}/trade_database_manager/core/kdb/__init__.py +0 -0
  23. {trade_database_manager-0.0.1.dev1 → trade_database_manager-0.0.1.dev4}/trade_database_manager/core/sql/__init__.py +0 -0
  24. {trade_database_manager-0.0.1.dev1 → trade_database_manager-0.0.1.dev4}/trade_database_manager/core/sql/sqlreader.py +0 -0
  25. {trade_database_manager-0.0.1.dev1 → trade_database_manager-0.0.1.dev4}/trade_database_manager/core/sql/sqlwriter.py +0 -0
  26. {trade_database_manager-0.0.1.dev1 → trade_database_manager-0.0.1.dev4}/trade_database_manager.egg-info/dependency_links.txt +0 -0
  27. {trade_database_manager-0.0.1.dev1 → trade_database_manager-0.0.1.dev4}/trade_database_manager.egg-info/requires.txt +0 -0
  28. {trade_database_manager-0.0.1.dev1 → trade_database_manager-0.0.1.dev4}/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.1.dev1
3
+ Version: 0.0.1.dev4
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
@@ -52,5 +52,26 @@ host all tradedbadmin ::/0 scram-sha-256
52
52
 
53
53
  ### KDB+ Initialization
54
54
 
55
+ #### get license
56
+ Request a 64-bit license from [Kx Systems](https://kx.com/kdb-insights-personal-edition-license-download). Fill the form
57
+ then the system will send an email with the license file and a base64 key (Either can be used to license the product).
55
58
 
56
- ```q
59
+ #### Server side (Linux system)
60
+ 1. Put the executable `q` and license in desired folder, say `/opt/l64` directory.
61
+ 2. Create a folder to store the data, say `/opt/data` directory.
62
+ 3. Set the environment variable `QLIC` to the directory where the license file is stored. And `QHOME` to the data directory.
63
+ 4. Run the `q` executable with the following command:
64
+ ```bash
65
+ $ q -p 5000 -s 1 -w 100 -t 1000 -T 1000 -U /opt/l64/trade.q
66
+ ```
67
+ - `-p 5000`: This sets the port number for the kdb+ process. In this case, the port number is 5000.
68
+
69
+ - `-s 1`: This sets the number of secondary threads. In this case, it's set to 1.
70
+
71
+ - `-w 100`: This sets the workspace heap size. In this case, it's set to 100 MB.
72
+
73
+ - `-t 1000`: This sets the timer interval in milliseconds. In this case, it's set to 1000 milliseconds, or 1 second.
74
+
75
+ - `-T 1000`: This sets the timeout in seconds for client queries. In this case, it's set to 1000 seconds.
76
+
77
+ - `-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.
@@ -0,0 +1,55 @@
1
+ <table border=1 cellpadding=10>
2
+ <tr>
3
+ <td style="color: red;">
4
+
5
+ #### \*\*\* IMPORTANT NOTICE \*\*\*
6
+
7
+ <p style="color: red">This package is <b>not</b> in a usable stage. It is only uploaded for convenience of developing and testing.</p>
8
+
9
+ </td></tr></table>
10
+
11
+
12
+
13
+ ### Postgreslq Initialization
14
+
15
+ ```sql
16
+ CREATE DATABASE trade_data;
17
+ CREATE USER tradedbadmin WITH PASSWORD 'trade_password';
18
+ GRANT ALL PRIVILEGES ON DATABASE trade_data TO tradedbadmin;
19
+ \connect trade_data;
20
+ GRANT ALL ON SCHEMA public TO tradedbadmin;
21
+ ```
22
+
23
+ Add the following lines in the `pg_hba.conf` file to allow password authentication for the `tradedbadmin` role.
24
+
25
+ ```
26
+ # trade database
27
+ host all tradedbadmin 0.0.0.0/0 scram-sha-256
28
+ host all tradedbadmin ::/0 scram-sha-256
29
+ ```
30
+
31
+ ### KDB+ Initialization
32
+
33
+ #### get license
34
+ Request a 64-bit license from [Kx Systems](https://kx.com/kdb-insights-personal-edition-license-download). Fill the form
35
+ then the system will send an email with the license file and a base64 key (Either can be used to license the product).
36
+
37
+ #### Server side (Linux system)
38
+ 1. Put the executable `q` and license in desired folder, say `/opt/l64` directory.
39
+ 2. Create a folder to store the data, say `/opt/data` directory.
40
+ 3. Set the environment variable `QLIC` to the directory where the license file is stored. And `QHOME` to the data directory.
41
+ 4. Run the `q` executable with the following command:
42
+ ```bash
43
+ $ q -p 5000 -s 1 -w 100 -t 1000 -T 1000 -U /opt/l64/trade.q
44
+ ```
45
+ - `-p 5000`: This sets the port number for the kdb+ process. In this case, the port number is 5000.
46
+
47
+ - `-s 1`: This sets the number of secondary threads. In this case, it's set to 1.
48
+
49
+ - `-w 100`: This sets the workspace heap size. In this case, it's set to 100 MB.
50
+
51
+ - `-t 1000`: This sets the timer interval in milliseconds. In this case, it's set to 1000 milliseconds, or 1 second.
52
+
53
+ - `-T 1000`: This sets the timeout in seconds for client queries. In this case, it's set to 1000 seconds.
54
+
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.
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "trade_database_manager"
7
- version = "0.0.1.dev1"
7
+ version = "0.0.1.dev4"
8
8
  description = "A wrapper of kdb and sql for convenient trade data management."
9
9
  readme = "README.md"
10
10
  authors = [
@@ -42,3 +42,8 @@ target-version = ['py310']
42
42
 
43
43
  [tool.isort]
44
44
  profile = "black"
45
+ line_length = 120
46
+
47
+ [tool.flake8]
48
+ max-line-length = 120
49
+ ignore = "E203,E501,W503"
@@ -1,4 +1,3 @@
1
1
  from setuptools import setup
2
2
 
3
-
4
3
  setup()
@@ -0,0 +1,312 @@
1
+ import re
2
+ from collections.abc import Container
3
+ from functools import partial, reduce
4
+ from typing import Any, Literal, Sequence, Type, Union
5
+
6
+ import pandas as pd
7
+ from sqlalchemy import REAL, Index, Integer, MetaData, String, Table, create_engine, inspect, select, sql, text
8
+ from sqlalchemy.dialects.postgresql import insert
9
+
10
+ from ...config import CONFIG
11
+ from ..typedefs import FILTERFIELD_TYPE, QUERYFIELD_TYPE
12
+
13
+
14
+ def _insert_on_conflict_update(table, conn, keys, data_iter, indexes):
15
+ data = [dict(zip(keys, row)) for row in data_iter]
16
+ stmt = insert(table.table).values(data)
17
+ stmt = stmt.on_conflict_do_update(index_elements=indexes, set_={k: getattr(stmt.excluded, k) for k in keys})
18
+ result = conn.execute(stmt)
19
+ return result.rowcount
20
+
21
+
22
+ def _insert_on_conflict_nothing(table, conn, keys, data_iter):
23
+ data = [dict(zip(keys, row)) for row in data_iter]
24
+ stmt = insert(table.table).values(data).on_conflict_do_nothing(index_elements=keys)
25
+ result = conn.execute(stmt)
26
+ return result.rowcount
27
+
28
+
29
+ class SqlManager:
30
+ """
31
+ This class is used to manage SQL operations.
32
+
33
+ :ivar sqlalchemy.engine.Engine engine: An instance of the SQLAlchemy Engine class for executing SQL operations.
34
+ """
35
+
36
+ def __init__(self):
37
+ self.engine = create_engine(CONFIG["sqlconnstr"])
38
+
39
+ def _execute(self, sql_executable: Union[str, sql.base.Executable]) -> Any:
40
+ if isinstance(sql_executable, str):
41
+ sql_executable = text(sql_executable)
42
+ with self.engine.begin() as conn:
43
+ return conn.execute(sql_executable)
44
+
45
+ def add_index(self, table_name: str, columns: Union[str, list[str]], unique: bool = True):
46
+ """
47
+ Adds an index to a table.
48
+
49
+ :param table_name: The name of the table to add the index to.
50
+ :type table_name: str
51
+ :param columns: The column(s) to include in the index. It can be a single column name or a list of column names.
52
+ :type columns: Union[str, list[str]]
53
+ :param unique: Whether the index should enforce unique values. Defaults to True.
54
+ :type unique: bool
55
+ """
56
+ if isinstance(columns, str):
57
+ columns = [columns]
58
+
59
+ index_name = f"uix_{table_name}_{'_'.join(columns)}"
60
+ table_meta = MetaData()
61
+ table = Table(table_name, table_meta, autoload_with=self.engine)
62
+ columns = [getattr(table.c, colname) for colname in columns if colname in table.c.keys()]
63
+ index = Index(index_name, *columns, unique=unique)
64
+ index.create(bind=self.engine)
65
+
66
+ def insert(
67
+ self,
68
+ table_name: str,
69
+ df: pd.DataFrame,
70
+ upsert: bool = True,
71
+ other_unique_index_columns: Sequence[str] = (),
72
+ other_non_unique_index_columns: Sequence[str] = (),
73
+ ):
74
+ """
75
+ Inserts data into a table.
76
+
77
+ :param table_name: The name of the table to insert data into.
78
+ :type table_name: str
79
+ :param df: The data to insert. It should be a DataFrame where the column names match the table columns.
80
+ :type df: pd.DataFrame
81
+ :param upsert: Whether to update the table if the data already exists. Defaults to True.
82
+ :type upsert: bool
83
+ :param other_unique_index_columns: Other columns to enforce unique values on. Defaults to an empty sequence.
84
+ :type other_unique_index_columns: Sequence[str]
85
+ :param other_non_unique_index_columns: Other columns to add non-unique indexes to. Defaults to an empty sequence.
86
+ :type other_non_unique_index_columns: Sequence[str]
87
+ :return: The number of rows inserted.
88
+ :rtype: int
89
+ """
90
+ if_exists: Literal["replace", "append"] = "append"
91
+ inspector = inspect(self.engine)
92
+ new_table = not inspector.has_table(table_name)
93
+ method = partial(_insert_on_conflict_update, indexes=df.index.names) if upsert and not new_table else None
94
+ num_rows = df.to_sql(
95
+ table_name,
96
+ self.engine,
97
+ if_exists=if_exists,
98
+ index=True,
99
+ index_label=df.index.names,
100
+ method=method,
101
+ )
102
+ if new_table:
103
+ self.add_index(table_name, df.index.names, unique=True)
104
+ for column in other_unique_index_columns:
105
+ self.add_index(table_name, column, unique=True)
106
+ for column in other_non_unique_index_columns:
107
+ self.add_index(table_name, column, unique=False)
108
+ return num_rows
109
+
110
+ def _convert_to_sqlalchemy_type(self, column_type: Type, **kwargs):
111
+ if isinstance(column_type, type):
112
+ column_type = column_type.__name__
113
+ if column_type == "str":
114
+ return String(**kwargs)
115
+ if column_type == "int":
116
+ return Integer()
117
+ if column_type == "float":
118
+ return REAL()
119
+ raise ValueError(f"Unsupported column type {column_type}")
120
+
121
+ def insert_column(self, table_name: str, column_name: str, column_type: Union[str, Type], type_kwargs: dict = None):
122
+ """
123
+ Inserts a new column into a table.
124
+
125
+ :param table_name: The name of the table to insert the column into.
126
+ :type table_name: str
127
+ :param column_name: The name of the new column.
128
+ :type column_name: str
129
+ :param column_type: The data type of the new column. It can be a string or a Python type.
130
+ :type column_type: Union[str, Type]
131
+ :param type_kwargs: Additional keyword arguments for the data type. Defaults to None.
132
+ :type type_kwargs: dict, optional
133
+ """
134
+ type_kwargs = type_kwargs or {}
135
+ sql_code = f"ALTER TABLE {table_name} ADD COLUMN {column_name} {str(self._convert_to_sqlalchemy_type(column_type, **type_kwargs))}"
136
+ self._execute(sql_code)
137
+
138
+ def delete_column(self, table_name: str, column_name: str):
139
+ """
140
+ Deletes a column from a table.
141
+
142
+ :param table_name: The name of the table to delete the column from.
143
+ :type table_name: str
144
+ :param column_name: The name of the column to delete.
145
+ :type column_name: str
146
+ """
147
+ self._execute(f"ALTER TABLE {table_name} DROP COLUMN {column_name}")
148
+
149
+ def rename_column(self, table_name: str, old_column_name: str, new_column_name: str):
150
+ """
151
+ Renames a column in a table.
152
+
153
+ :param table_name: The name of the table containing the column to rename.
154
+ :type table_name: str
155
+ :param old_column_name: The current name of the column.
156
+ :type old_column_name: str
157
+ :param new_column_name: The new name for the column.
158
+ :type new_column_name: str
159
+ """
160
+ # check if any index is referring to the column
161
+ sql_code = f"SELECT indexname, indexdef FROM pg_indexes WHERE indexdef LIKE '%%(%%{old_column_name}%%)%%' and tablename = '{table_name}'"
162
+ index_refering_column = dict(self.engine.execute(sql_code).fetchall())
163
+
164
+ # drop indexes referring to the column
165
+ for index_name in index_refering_column:
166
+ self._execute(f"DROP INDEX IF EXISTS {index_name}")
167
+
168
+ # rename the column
169
+ self._execute(f"ALTER TABLE {table_name} RENAME COLUMN {old_column_name} TO {new_column_name}")
170
+
171
+ # recreate the indexes
172
+ for index_name, index_def in index_refering_column.items():
173
+ new_index_name = index_name.replace(old_column_name, new_column_name)
174
+ new_index_def = re.sub(rf"\(([^)]*?){old_column_name}([^)]*?)\)", rf"(\1{new_column_name}\2)", index_def)
175
+ new_index_def = new_index_def.replace(index_name, new_index_name)
176
+ self._execute(new_index_def)
177
+
178
+ def read_range_data(
179
+ self,
180
+ table_name: str,
181
+ query_fields="*",
182
+ start_time: pd.Timestamp = None,
183
+ end_time: pd.Timestamp = None,
184
+ filter_fields=None,
185
+ ):
186
+ meta = MetaData()
187
+ table = Table(table_name, meta, autoload_with=self.engine)
188
+
189
+ if query_fields != "*":
190
+ query_fields = [table.columns[field] for field in query_fields]
191
+ else:
192
+ query_fields = table
193
+
194
+ stmt = select(*query_fields)
195
+
196
+ conditions = []
197
+ if start_time:
198
+ conditions.append(getattr(table.c, "timestamp", table.c.end_time) >= start_time)
199
+ if end_time:
200
+ conditions.append(getattr(table.c, "timestamp", table.c.start_time) <= end_time)
201
+ if filter_fields:
202
+ conditions.extend(
203
+ [
204
+ (
205
+ table.columns[field].in_(filter_values)
206
+ if isinstance(filter_values, Container) and not isinstance(filter_values, (str, bytes))
207
+ else table.columns[field] == filter_values
208
+ )
209
+ for field, filter_values in filter_fields.items()
210
+ ]
211
+ )
212
+ stmt = stmt.where(sql.and_(*conditions))
213
+ res = self._execute(stmt)
214
+ return pd.DataFrame(res.fetchall(), columns=res.keys())
215
+
216
+ def read_data(self, table_name: str, query_fields: QUERYFIELD_TYPE = "*", filter_fields=None):
217
+ """
218
+ Reads data from a table.
219
+
220
+ :param table_name: The name of the table to read data from.
221
+ :type table_name: str
222
+ :param query_fields: The fields to query. By default, it queries all fields. Defaults to "*".
223
+ :type query_fields: QUERYFIELD_TYPE, optional
224
+ :param filter_fields: Additional fields to filter by. The keys are the field names and the values are the filter values. Defaults to None.
225
+ :type filter_fields: dict, optional
226
+ :return: A DataFrame containing the queried data.
227
+ :rtype: pd.DataFrame
228
+ """
229
+
230
+ meta = MetaData()
231
+ table = Table(table_name, meta, autoload_with=self.engine)
232
+
233
+ if query_fields != "*":
234
+ query_fields = [table.columns[field] for field in query_fields]
235
+ stmt = select(*query_fields)
236
+ else:
237
+ query_fields = table
238
+ stmt = select(table)
239
+
240
+ if filter_fields:
241
+ conditions = [
242
+ (
243
+ table.columns[field].in_(filter_values)
244
+ if isinstance(filter_values, Container) and not isinstance(filter_values, (str, bytes))
245
+ else table.columns[field] == filter_values
246
+ )
247
+ for field, filter_values in filter_fields.items()
248
+ ]
249
+ stmt = stmt.where(sql.and_(*conditions))
250
+
251
+ # cannot use pandas.read_sql here as it discards timezone info
252
+ res = self._execute(stmt)
253
+ pd.read_sql_query()
254
+ return pd.DataFrame(res.fetchall(), columns=res.keys())
255
+
256
+ def read_data_across_tables(
257
+ self,
258
+ table_names: Sequence[str],
259
+ joined_columns: Sequence[str],
260
+ query_fields: QUERYFIELD_TYPE = "*",
261
+ filter_fields: FILTERFIELD_TYPE = None,
262
+ ):
263
+ """
264
+ Reads data from multiple tables.
265
+
266
+ :param table_names: The names of the tables to read data from.
267
+ :type table_names: Sequence[str]
268
+ :param joined_columns: The columns to join the tables on.
269
+ :type joined_columns: Sequence[str]
270
+ :param query_fields: The fields to query. By default, it queries all fields. Defaults to "*".
271
+ :type query_fields: QUERYFIELD_TYPE, optional
272
+ :param filter_fields: Additional fields to filter by. The keys are the field names and the values are the filter values. Defaults to None.
273
+ :type filter_fields: FILTERFIELD_TYPE, optional
274
+ :return: A DataFrame containing the queried data.
275
+ :rtype: pd.DataFrame
276
+ """
277
+ meta = MetaData()
278
+ tables = {table_name: Table(table_name, meta, autoload_with=self.engine) for table_name in table_names}
279
+
280
+ joined_table = reduce(
281
+ lambda x, y: x.join(y, sql.and_(*[x.columns[col] == y.columns[col] for col in joined_columns])),
282
+ tables.values(),
283
+ )
284
+
285
+ query_fields_rel = []
286
+ if query_fields != "*":
287
+ for table_name, colnames in query_fields.items():
288
+ if isinstance(colnames, str):
289
+ query_fields_rel.append(tables[table_name].columns[colnames])
290
+ else:
291
+ query_fields_rel.extend([tables[table_name].columns[colname] for colname in colnames])
292
+ else:
293
+ query_fields_rel = [text("*")]
294
+
295
+ stmt = select(*query_fields_rel).select_from(joined_table)
296
+
297
+ if filter_fields:
298
+ conditions = []
299
+ for table_name, colnames in filter_fields.items():
300
+ table = tables[table_name]
301
+ for field, filter_values in colnames.items():
302
+ conditions.append(
303
+ table.columns[field].in_(filter_values)
304
+ if isinstance(filter_values, Container) and not isinstance(filter_values, (str, bytes))
305
+ else table.columns[field] == filter_values
306
+ )
307
+
308
+ stmt = stmt.where(sql.and_(*conditions))
309
+
310
+ # cannot use pandas.read_sql here as it discards timezone info
311
+ res = self._execute(stmt)
312
+ return pd.DataFrame(res.fetchall(), columns=res.keys())
@@ -0,0 +1,17 @@
1
+ # -*- coding: utf-8 -*-
2
+ # @Time : 2024/4/22 17:48
3
+ # @Author : YQ Tsui
4
+ # @File : typedefs.py
5
+ # @Purpose :
6
+
7
+ from typing import Dict, Literal, Optional, Sequence, TypeVar, Union
8
+
9
+ T = TypeVar("T")
10
+ T_SeqT = Union[T, Sequence[T]]
11
+ Opt_T_SeqT = Optional[Union[T, Sequence[T]]]
12
+
13
+ T_DictT = Union[T, Dict[str, T]]
14
+ Opt_T_DictT = Optional[Union[T, Dict[str, T]]]
15
+
16
+ QUERYFIELD_TYPE = T_DictT[Union[Literal["*"], Sequence[str]]]
17
+ FILTERFIELD_TYPE = Opt_T_DictT[Dict[str, Union[str, Sequence[str], int, float, bool]]]
@@ -0,0 +1,9 @@
1
+ # -*- coding: utf-8 -*-
2
+ # @Time : 2024/4/21 12:41
3
+ # @Author : YQ Tsui
4
+ # @File : __init__.py
5
+ # @Purpose :
6
+
7
+ from .metadata_sql import MetadataSql
8
+
9
+ __all__ = ("MetadataSql",)
@@ -0,0 +1,23 @@
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
+ }
@@ -0,0 +1,220 @@
1
+ # -*- coding: utf-8 -*-
2
+ # @Time : 2024/4/15 20:28
3
+ # @Author : YQ Tsui
4
+ # @File : metadata_sql.py
5
+ # @Purpose : Instrument metadata stored in SQL database
6
+
7
+ from collections.abc import Container
8
+ from typing import Union, cast
9
+
10
+ import pandas as pd
11
+
12
+ from ..core.sql.sqlmanager import SqlManager
13
+ from .typedefs import EXCHANGE_LITERALS, INST_TYPE_LITERALS, Opt_T_SeqT, T_DictT
14
+
15
+ COMMON_METADATA_COLUMNS = [
16
+ "name",
17
+ "trading_code",
18
+ "inst_type",
19
+ "currency",
20
+ "timezone",
21
+ "tick_size",
22
+ "lot_size",
23
+ "min_lots",
24
+ "market_tplus",
25
+ "listed_date",
26
+ "delisted_date",
27
+ ]
28
+ TYPE_METADATA_COLUMNS = {
29
+ "STK": ["sector", "industry", "country", "state", "board_type", "issue_price"],
30
+ }
31
+
32
+
33
+ class MetadataSql:
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
48
+
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
+ """
56
+ type_specific_columns = set(data.columns) - set(COMMON_METADATA_COLUMNS)
57
+ if "inst_type" not in data.columns and bool(type_specific_columns):
58
+ raise ValueError(
59
+ f"Non-common columns found ({','.join(type_specific_columns)}) but inst_type column not provided."
60
+ )
61
+ if isinstance(data, dict):
62
+ data = [data]
63
+ if isinstance(data, list):
64
+ data = pd.DataFrame(data)
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'."
69
+ data_common = data[data.columns.intersection(COMMON_METADATA_COLUMNS)]
70
+
71
+ self._manager.insert("instruments", data_common, upsert=True)
72
+ if "inst_type" in data.columns:
73
+ for inst_type, columns in TYPE_METADATA_COLUMNS.items():
74
+ data_type_df = data.loc[data.inst_type == inst_type, data.columns.intersection(columns)]
75
+ if not data_type_df.empty:
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)
82
+
83
+ def read_metadata(
84
+ self,
85
+ ticker: Opt_T_SeqT[str] = None,
86
+ exchange: Opt_T_SeqT[EXCHANGE_LITERALS] = None,
87
+ query_fields="*",
88
+ filter_fields=None,
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
+ """
104
+ filter_fields = filter_fields or {}
105
+ if ticker is not None:
106
+ filter_fields["ticker"] = ticker
107
+ if exchange is not None:
108
+ if not isinstance(exchange, str) and isinstance(exchange, Container) and ticker is not None:
109
+ assert len(exchange) == len(ticker), "Exchange must be a single value or the same length as ticker."
110
+ filter_fields["exchange"] = exchange
111
+ query_fields_common = (
112
+ ["ticker", "exchange"] + [f for f in query_fields if f in COMMON_METADATA_COLUMNS]
113
+ if query_fields != "*"
114
+ else "*"
115
+ )
116
+ filter_fields_common = {
117
+ k: v for k, v in filter_fields.items() if k in ["ticker", "exchange"] + COMMON_METADATA_COLUMNS
118
+ }
119
+ all_fields_common = (
120
+ len(query_fields_common) == len(query_fields)
121
+ and len(filter_fields_common) == len(filter_fields)
122
+ and query_fields != "*"
123
+ )
124
+ if not (query_fields == "*" or "inst_type" in query_fields):
125
+ query_fields_common.append("inst_type")
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)
130
+ if common_df.empty:
131
+ return {}
132
+ res = {}
133
+ for inst_type, common_df_by_type in common_df.groupby("inst_type"):
134
+ inst_type = cast(INST_TYPE_LITERALS, inst_type)
135
+ if all_fields_common:
136
+ res[inst_type] = common_df_by_type
137
+ continue
138
+ query_fields_type = (
139
+ ["ticker", "exchange"] + [f for f in query_fields if f in TYPE_METADATA_COLUMNS[inst_type]]
140
+ if query_fields != "*"
141
+ else "*"
142
+ )
143
+ filter_fields_type = {k: v for k, v in filter_fields.items() if k in TYPE_METADATA_COLUMNS[inst_type]}
144
+ filter_fields_type["ticker"] = common_df_by_type["ticker"].to_list()
145
+ filter_fields_type["exchange"] = common_df_by_type["exchange"].to_list()
146
+ type_df = self._manager.read_data(
147
+ f"instruments_{inst_type.lower()}", query_fields=query_fields_type, filter_fields=filter_fields_type
148
+ )
149
+ type_df = common_df_by_type.merge(type_df, on=["ticker", "exchange"], how="inner")
150
+ res[inst_type] = type_df.set_index(["ticker", "exchange"])
151
+ return res
152
+
153
+ def read_metadata_for_insttype(
154
+ self,
155
+ inst_type: INST_TYPE_LITERALS,
156
+ ticker: Opt_T_SeqT[str] = None,
157
+ exchange: Opt_T_SeqT[EXCHANGE_LITERALS] = None,
158
+ query_fields="*",
159
+ filter_fields=None,
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
+ """
177
+ filter_fields = filter_fields or {}
178
+ filter_fields["inst_type"] = inst_type
179
+ if ticker is not None:
180
+ filter_fields["ticker"] = ticker
181
+ if exchange is not None:
182
+ if not isinstance(exchange, str) and isinstance(exchange, Container) and ticker is not None:
183
+ assert len(exchange) == len(ticker), "Exchange must be a single value or the same length as ticker."
184
+ filter_fields["exchange"] = exchange
185
+
186
+ if query_fields == "*":
187
+ query_fields_cross = "*"
188
+ else:
189
+ query_fields_common = ["ticker", "exchange"] + [f for f in query_fields if f in COMMON_METADATA_COLUMNS]
190
+ query_fields_type = ["ticker", "exchange"] + [
191
+ f for f in query_fields if f in TYPE_METADATA_COLUMNS[inst_type]
192
+ ]
193
+ query_fields_cross = {
194
+ "instruments": query_fields_common,
195
+ f"instruments_{inst_type.lower()}": query_fields_type,
196
+ }
197
+
198
+ filter_fields_common = {
199
+ k: v for k, v in filter_fields.items() if k in ["ticker", "exchange"] + COMMON_METADATA_COLUMNS
200
+ }
201
+ filter_fields_type = {
202
+ k: v for k, v in filter_fields.items() if k in ["ticker", "exchange"] + TYPE_METADATA_COLUMNS[inst_type]
203
+ }
204
+ filter_fields_cross = {
205
+ "instruments": filter_fields_common,
206
+ f"instruments_{inst_type.lower()}": filter_fields_type,
207
+ }
208
+ df = self._manager.read_data_across_tables(
209
+ ["instruments", f"instruments_{inst_type.lower()}"],
210
+ joined_columns=["ticker", "exchange"],
211
+ query_fields=query_fields_cross,
212
+ filter_fields=filter_fields_cross,
213
+ )
214
+
215
+ if isinstance(df.columns, pd.Index):
216
+ df = df.loc[:, ~df.columns.duplicated()].set_index(["ticker", "exchange"])
217
+ else:
218
+ df = df.set_index(["ticker", "exchange"])
219
+ self._convert_datetime_columns(df)
220
+ return df
@@ -0,0 +1,48 @@
1
+ # -*- coding: utf-8 -*-
2
+ # @Time : 2024/4/22 16:18
3
+ # @Author : YQ Tsui
4
+ # @File : typedefs.py
5
+ # @Purpose : Type hints for convenience
6
+
7
+ from typing import Dict, Literal, Optional, Sequence, TypeVar, Union
8
+
9
+ INST_TYPE_LITERALS = Literal["STK", "FUT", "OPT", "IDX", "ETF", "FND", "BND", "CASH", "CRYPTO"]
10
+ EXCHANGE_LITERALS = Literal[
11
+ "SSE",
12
+ "SZSE",
13
+ "HKEX",
14
+ "CFFEX",
15
+ "SHFE",
16
+ "DCE",
17
+ "CZCE",
18
+ "SGX",
19
+ "CBOT",
20
+ "CME",
21
+ "COMEX",
22
+ "NYMEX",
23
+ "ICE",
24
+ "LME",
25
+ "TOCOM",
26
+ "JPX",
27
+ "KRX",
28
+ "ASX",
29
+ "NSE",
30
+ "BSE",
31
+ "NSE",
32
+ "BSE",
33
+ "MCX",
34
+ "MOEX",
35
+ "TSE",
36
+ "TWSE",
37
+ "SET",
38
+ "IDX",
39
+ "CRYPTO",
40
+ "SMART",
41
+ ]
42
+
43
+ T = TypeVar("T")
44
+ T_SeqT = Union[T, Sequence[T]]
45
+ Opt_T_SeqT = Optional[Union[T, Sequence[T]]]
46
+
47
+ T_DictT = Union[T, Dict[str, T]]
48
+ Opt_T_DictT = Optional[Union[T, Dict[str, T]]]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: trade_database_manager
3
- Version: 0.0.1.dev1
3
+ Version: 0.0.1.dev4
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
@@ -52,5 +52,26 @@ host all tradedbadmin ::/0 scram-sha-256
52
52
 
53
53
  ### KDB+ Initialization
54
54
 
55
+ #### get license
56
+ Request a 64-bit license from [Kx Systems](https://kx.com/kdb-insights-personal-edition-license-download). Fill the form
57
+ then the system will send an email with the license file and a base64 key (Either can be used to license the product).
55
58
 
56
- ```q
59
+ #### Server side (Linux system)
60
+ 1. Put the executable `q` and license in desired folder, say `/opt/l64` directory.
61
+ 2. Create a folder to store the data, say `/opt/data` directory.
62
+ 3. Set the environment variable `QLIC` to the directory where the license file is stored. And `QHOME` to the data directory.
63
+ 4. Run the `q` executable with the following command:
64
+ ```bash
65
+ $ q -p 5000 -s 1 -w 100 -t 1000 -T 1000 -U /opt/l64/trade.q
66
+ ```
67
+ - `-p 5000`: This sets the port number for the kdb+ process. In this case, the port number is 5000.
68
+
69
+ - `-s 1`: This sets the number of secondary threads. In this case, it's set to 1.
70
+
71
+ - `-w 100`: This sets the workspace heap size. In this case, it's set to 100 MB.
72
+
73
+ - `-t 1000`: This sets the timer interval in milliseconds. In this case, it's set to 1000 milliseconds, or 1 second.
74
+
75
+ - `-T 1000`: This sets the timeout in seconds for client queries. In this case, it's set to 1000 seconds.
76
+
77
+ - `-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.
@@ -10,10 +10,13 @@ trade_database_manager.egg-info/dependency_links.txt
10
10
  trade_database_manager.egg-info/requires.txt
11
11
  trade_database_manager.egg-info/top_level.txt
12
12
  trade_database_manager/core/__init__.py
13
+ trade_database_manager/core/typedefs.py
13
14
  trade_database_manager/core/kdb/__init__.py
14
15
  trade_database_manager/core/sql/__init__.py
15
16
  trade_database_manager/core/sql/sqlmanager.py
16
17
  trade_database_manager/core/sql/sqlreader.py
17
18
  trade_database_manager/core/sql/sqlwriter.py
18
- trade_database_manager/tools/__init__.py
19
- trade_database_manager/tools/init_config.py
19
+ trade_database_manager/manager/__init__.py
20
+ trade_database_manager/manager/fields_data_type.py
21
+ trade_database_manager/manager/metadata_sql.py
22
+ trade_database_manager/manager/typedefs.py
@@ -1,34 +0,0 @@
1
- <table border=1 cellpadding=10>
2
- <tr>
3
- <td style="color: red;">
4
-
5
- #### \*\*\* IMPORTANT NOTICE \*\*\*
6
-
7
- <p style="color: red">This package is <b>not</b> in a usable stage. It is only uploaded for convenience of developing and testing.</p>
8
-
9
- </td></tr></table>
10
-
11
-
12
-
13
- ### Postgreslq Initialization
14
-
15
- ```sql
16
- CREATE DATABASE trade_data;
17
- CREATE USER tradedbadmin WITH PASSWORD 'trade_password';
18
- GRANT ALL PRIVILEGES ON DATABASE trade_data TO tradedbadmin;
19
- \connect trade_data;
20
- GRANT ALL ON SCHEMA public TO tradedbadmin;
21
- ```
22
-
23
- Add the following lines in the `pg_hba.conf` file to allow password authentication for the `tradedbadmin` role.
24
-
25
- ```
26
- # trade database
27
- host all tradedbadmin 0.0.0.0/0 scram-sha-256
28
- host all tradedbadmin ::/0 scram-sha-256
29
- ```
30
-
31
- ### KDB+ Initialization
32
-
33
-
34
- ```q
@@ -1,102 +0,0 @@
1
- from functools import partial
2
- from typing import Literal, Sequence, Union
3
-
4
- import pandas as pd
5
- from sqlalchemy import create_engine, inspect
6
- from sqlalchemy.dialects.postgresql import insert
7
- import re
8
- from ...config import CONFIG
9
-
10
-
11
- def _insert_on_conflict_update(table, conn, keys, data_iter, indexes):
12
- data = [dict(zip(keys, row)) for row in data_iter]
13
- stmt = insert(table.table).values(data)
14
- stmt = stmt.on_conflict_do_update(index_elements=indexes, set_={k: getattr(stmt.excluded, k) for k in keys})
15
- result = conn.execute(stmt)
16
- return result.rowcount
17
-
18
-
19
- def _insert_on_conflict_nothing(table, conn, keys, data_iter):
20
- data = [dict(zip(keys, row)) for row in data_iter]
21
- stmt = insert(table.table).values(data).on_conflict_do_nothing(index_elements=keys)
22
- result = conn.execute(stmt)
23
- return result.rowcount
24
-
25
-
26
- class SqlManager:
27
- def __init__(
28
- self,
29
- ):
30
- self.engine = create_engine(CONFIG["sqlconnstr"])
31
-
32
- def add_index(self, table_name: str, columns: Union[str, Sequence[str]], unique: bool = True):
33
- if isinstance(columns, str):
34
- columns = [columns]
35
- index_name = f"uix_{table_name}_{'_'.join(columns)}"
36
- columns_str = ", ".join(columns)
37
- unique_str = "UNIQUE" if unique else ""
38
- self.engine.execute(f"CREATE {unique_str} INDEX {index_name} ON {table_name} ({columns_str})")
39
-
40
- def insert(
41
- self,
42
- table_name: str,
43
- df: pd.DataFrame,
44
- upsert: bool = True,
45
- other_unique_index_columns: Sequence[str] = (),
46
- other_non_unique_index_columns: Sequence[str] = (),
47
- ):
48
- if_exists: Literal["replace", "append"] = "append"
49
- inspector = inspect(self.engine)
50
- new_table = not inspector.has_table(table_name)
51
- method = (
52
- partial(_insert_on_conflict_update, indexes=df.index.names)
53
- if upsert and not new_table
54
- else None
55
- )
56
- num_rows = df.to_sql(
57
- table_name,
58
- self.engine,
59
- if_exists=if_exists,
60
- index=True,
61
- index_label=df.index.names,
62
- method=method,
63
- )
64
- if new_table:
65
- self.add_index(table_name, df.index.names)
66
- for column in other_unique_index_columns:
67
- self.add_index(table_name, column, unique=True)
68
- for column in other_non_unique_index_columns:
69
- self.add_index(table_name, column, unique=False)
70
- return num_rows
71
-
72
- def rename_column(self, table_name: str, old_column_name: str, new_column_name: str):
73
- # check if any index is referring to the column
74
- sql_code = f"SELECT indexname, indexdef FROM pg_indexes WHERE indexdef LIKE '%%(%%{old_column_name}%%)%%' and tablename = '{table_name}'"
75
- index_refering_column = dict(self.engine.execute(sql_code).fetchall())
76
-
77
- # drop indexes referring to the column
78
- for index_name in index_refering_column:
79
- self.engine.execute(f"DROP INDEX IF EXISTS {index_name}")
80
-
81
- # rename the column
82
- self.engine.execute(f"ALTER TABLE {table_name} RENAME COLUMN {old_column_name} TO {new_column_name}")
83
-
84
- # recreate the indexes
85
- for index_name, index_def in index_refering_column.items():
86
- new_index_name = index_name.replace(old_column_name, new_column_name)
87
- new_index_def = re.sub(rf"\(([^)]*?){old_column_name}([^)]*?)\)", rf"(\1{new_column_name}\2)", index_def)
88
- new_index_def = new_index_def.replace(index_name, new_index_name)
89
- self.engine.execute(new_index_def)
90
-
91
- def read_range_data(
92
- self, table_name: str, query_fields, start_time: pd.Timestamp, end_time: pd.Timestamp, filter_fields=None
93
- ):
94
- sql_code = f"SELECT {', '.join(query_fields)} FROM {table_name} WHERE end_time >= '{start_time}' AND start_time <= '{end_time}'"
95
- if filter_fields:
96
- sql_code += (
97
- f" AND {' AND '.join([f'{field} IN {tuple(filter_values)}' for field, filter_values in filter_fields])}"
98
- )
99
-
100
- # cannot use pandas.read_sql here as it discards timezone info
101
- res = self.engine.execute(sql_code)
102
- return pd.DataFrame(res.fetchall(), columns=res.keys())
@@ -1,42 +0,0 @@
1
- import argparse
2
- import importlib.util
3
- from pathlib import Path
4
-
5
- from ruamel.yaml import YAML
6
-
7
- if __name__ == "__main__":
8
- parser = argparse.ArgumentParser()
9
- parser.add_argument("-u", "--username", type=str)
10
- parser.add_argument("-pw", "--password", type=str)
11
- parser.add_argument(
12
- "--host",
13
- type=str,
14
- default="localhost",
15
- help="host of both kdb and sql, will be overwritten if sqlhost or kdbhost is specified",
16
- )
17
- parser.add_argument("--sqlhost", type=str, default="", help="host of sql")
18
- parser.add_argument("--sqldb", type=str, default="trade_data", help="database name of sql")
19
- parser.add_argument("--kdbhost", type=str, default="", help="host of kdb")
20
- parser.add_argument("--sqlport", type=str, default="5432", help="port of sql")
21
- parser.add_argument("--kdbport", type=str, default="5000", help="port of kdb")
22
-
23
- args = parser.parse_args()
24
-
25
- kdb_host = args.host if args.kdbhost == "" else args.kdbhost
26
- sql_host = args.host if args.sqlhost == "" else args.sqlhost
27
-
28
- sql_protocol = "postgresql" if importlib.util.find_spec("psycopg2") is None else "postgresql+psycopg2"
29
-
30
- config = {
31
- "username": args.username,
32
- "password": args.password,
33
- "kdbhost": kdb_host,
34
- "kdbport": args.kdbport,
35
- "sqlconnstr": f"{sql_protocol}://{args.username}:{args.password}@{sql_host}:{args.sqlport}/{args.sqldb}",
36
- }
37
-
38
- yaml = YAML(typ="safe")
39
- config_folder = Path("~/.tradedbmgr").expanduser()
40
- config_folder.mkdir(exist_ok=True)
41
- with open(config_folder.joinpath("config.yaml"), "w") as f:
42
- yaml.dump(config, f)